Skip to content

Commit feba4e1

Browse files
authored
Fix Mamba state progression across cached prefill continuations (#7714)
Signed-off-by: wdykas <wdykas@nvidia.com>
1 parent 2f35578 commit feba4e1

2 files changed

Lines changed: 78 additions & 4 deletions

File tree

‎megatron/core/inference/contexts/dynamic_context.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3223,9 +3223,10 @@ def _compute_prefix_match(
32233223
else:
32243224
prefix_skip_tokens = 0
32253225

3226-
# Hybrid models with Mamba caching: skip based on Mamba match count.
3227-
# Only applies to the first chunk (finished == 0); continuation chunks
3228-
# already had Mamba state restored during the first chunk.
3226+
# Hybrid models can skip tokens only when the first chunk restores a
3227+
# matching Mamba state. Continuation chunks carry live state from the
3228+
# previous chunk and must compute every subsequent recurrent transition,
3229+
# even when their attention KV blocks are already cached.
32293230
if self.is_hybrid_model and self.mamba_slot_allocator is not None and finished == 0:
32303231
num_mamba_matched = self._find_mamba_match_count(
32313232
req=req,
@@ -3249,7 +3250,7 @@ def _compute_prefix_match(
32493250
prefix_skip_tokens = raw_skip
32503251
else:
32513252
prefix_skip_tokens = 0
3252-
elif self.is_hybrid_model and finished == 0:
3253+
elif self.is_hybrid_model:
32533254
if record_mamba_match:
32543255
req._mamba_num_matched_blocks = 0
32553256
prefix_skip_tokens = 0

‎tests/unit_tests/inference/contexts/test_dynamic_prefix_caching.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,6 +1747,79 @@ def test_unaligned_continuation_redirects_inherited_partial_block(self):
17471747
assert ctx.kv_block_allocator.block_ref_counts[block_id].item() == 2
17481748

17491749

1750+
@pytest.mark.internal
1751+
@pytest.mark.parametrize("record_mamba_match", [False, True])
1752+
@pytest.mark.parametrize(
1753+
"mamba_boundaries",
1754+
[None, [], [2], [2, 6, 9]],
1755+
ids=["memory_only", "snapshots_evicted", "earlier_snapshot", "later_snapshots"],
1756+
)
1757+
def test_hybrid_cached_continuation_preserves_recurrent_position(
1758+
mamba_boundaries, record_mamba_match
1759+
):
1760+
"""KV reuse must not skip transitions in a continuation's live Mamba state."""
1761+
ctx = object.__new__(DynamicInferenceContext)
1762+
ctx.block_size_tokens = 256
1763+
ctx.enable_prefix_caching = True
1764+
ctx.enable_mtp_kv_cache = False
1765+
ctx.is_hybrid_model = True
1766+
hashes = list(range(1, 11))
1767+
ctx.kv_block_allocator = SimpleNamespace(kv_hash_to_block_id={h: h for h in hashes})
1768+
ctx.mamba_slot_allocator = (
1769+
None
1770+
if mamba_boundaries is None
1771+
else SimpleNamespace(hash_to_block_id={h: h for h in mamba_boundaries})
1772+
)
1773+
req = SimpleNamespace(finished_chunk_token_count=0, precomputed_block_hashes=hashes)
1774+
bs = ctx.block_size_tokens
1775+
recurrent_position = 0
1776+
1777+
for chunk_length in (4 * bs, 4 * bs, 2 * bs + 2):
1778+
finished = req.finished_chunk_token_count
1779+
match = ctx._compute_prefix_match(req, chunk_length, record_mamba_match=record_mamba_match)
1780+
allocated = match.already_allocated_blocks
1781+
required = match.overall_required_blocks
1782+
assert match.matched_block_ids == hashes[allocated:required]
1783+
assert match.num_blocks_from_pool == required - allocated - len(match.matched_block_ids)
1784+
if finished == 0:
1785+
# A valid first-chunk snapshot still permits its matching prefix skip.
1786+
assert match.prefix_skip_tokens == (2 * bs if mamba_boundaries else 0)
1787+
recurrent_position = match.prefix_skip_tokens
1788+
else:
1789+
# No Mamba restore occurs on continuation admission. Sharing KV must
1790+
# not advance the logical position past uncomputed recurrent state.
1791+
assert match.prefix_skip_tokens == 0
1792+
assert match.effective_prefill_chunk_length == chunk_length
1793+
recurrent_position += match.effective_prefill_chunk_length
1794+
req.finished_chunk_token_count += chunk_length
1795+
assert recurrent_position == req.finished_chunk_token_count
1796+
1797+
1798+
@pytest.mark.internal
1799+
def test_attention_only_cached_continuation_still_skips():
1800+
"""Attention-only continuations can still skip matching KV blocks."""
1801+
ctx = object.__new__(DynamicInferenceContext)
1802+
ctx.block_size_tokens = 256
1803+
ctx.enable_prefix_caching = True
1804+
ctx.enable_mtp_kv_cache = False
1805+
ctx.is_hybrid_model = False
1806+
ctx.mamba_slot_allocator = None
1807+
hashes = list(range(1, 11))
1808+
ctx.kv_block_allocator = SimpleNamespace(kv_hash_to_block_id={h: h for h in hashes})
1809+
req = SimpleNamespace(
1810+
finished_chunk_token_count=4 * ctx.block_size_tokens, precomputed_block_hashes=hashes
1811+
)
1812+
1813+
match = ctx._compute_prefix_match(req, 6 * ctx.block_size_tokens + 2)
1814+
1815+
assert match.matched_block_ids == hashes[4:]
1816+
assert match.num_blocks_from_pool == 1
1817+
assert match.already_allocated_blocks == 4
1818+
assert match.overall_required_blocks == 11
1819+
assert match.prefix_skip_tokens == 6 * ctx.block_size_tokens
1820+
assert match.effective_prefill_chunk_length == 2
1821+
1822+
17501823
def _make_cpu_mamba_slot_allocator(
17511824
monkeypatch, *, total_blocks: int, max_slots: int
17521825
) -> MambaSlotAllocator:

0 commit comments

Comments
 (0)