forked from vllm-project/vllm
-
Notifications
You must be signed in to change notification settings - Fork 22
[Bugfix] Cherry-pick upstream #51113 and #51812 into dev/gilded-gnosis #393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
malaiwah
wants to merge
2
commits into
local-inference-lab:dev/gilded-gnosis
Choose a base branch
from
malaiwah:fix/cherry-pick-51113-51812
base: dev/gilded-gnosis
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """Mamba "align" prefill chunk splitting (`_mamba_block_aligned_split`). | ||
|
|
||
| Invariant: slot `p` holds the state after exactly `(p + 1) * block_size` tokens. | ||
| State is written at chunk ends, so a chunk ending mid-block leaves its slot at | ||
| the wrong offset, and a later chunk crossing that boundary publishes it anyway. | ||
| Requests resuming from it then restore a truncated state (#43559). | ||
| """ | ||
|
|
||
| from types import SimpleNamespace | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from vllm.utils.math_utils import cdiv | ||
| from vllm.v1.core.kv_cache_manager import KVCacheManager | ||
| from vllm.v1.core.sched.scheduler import Scheduler | ||
| from vllm.v1.kv_cache_interface import ( | ||
| FullAttentionSpec, | ||
| KVCacheConfig, | ||
| KVCacheGroupSpec, | ||
| MambaSpec, | ||
| ) | ||
| from vllm.v1.request import Request | ||
|
|
||
| from .utils import create_requests | ||
|
|
||
| pytestmark = pytest.mark.cpu_test | ||
|
|
||
| # Mirrors the deployment where the poisoning was observed (Qwen3.6-27B): mamba | ||
| # block 1600, MTP with 3 draft tokens, prompts shorter than 2 mamba blocks. | ||
| ATTN_BLOCK_SIZE = 16 | ||
| MAMBA_BLOCK_SIZE = 1600 | ||
| NUM_SPEC = 3 | ||
| PROMPT_LEN = 2002 | ||
| MAMBA_GROUP_ID = 1 | ||
|
|
||
|
|
||
| def _make_hybrid_kv_cache_manager() -> KVCacheManager: | ||
| config = KVCacheConfig( | ||
| num_blocks=10000, | ||
| kv_cache_tensors=[], | ||
| kv_cache_groups=[ | ||
| KVCacheGroupSpec( | ||
| ["full_layer"], | ||
| FullAttentionSpec( | ||
| block_size=ATTN_BLOCK_SIZE, | ||
| num_kv_heads=1, | ||
| head_size=1, | ||
| dtype=torch.float32, | ||
| ), | ||
| ), | ||
| KVCacheGroupSpec( | ||
| ["mamba_layer"], | ||
| MambaSpec( | ||
| block_size=MAMBA_BLOCK_SIZE, | ||
| shapes=((1, 1),), | ||
| dtypes=(torch.float32,), | ||
| mamba_cache_mode="align", | ||
| num_speculative_blocks=NUM_SPEC, | ||
| ), | ||
| ), | ||
| ], | ||
| ) | ||
| return KVCacheManager( | ||
| config, | ||
| max_model_len=262144, | ||
| scheduler_block_size=MAMBA_BLOCK_SIZE, | ||
| hash_block_size=ATTN_BLOCK_SIZE, | ||
| enable_caching=True, | ||
| use_eagle=True, | ||
| ) | ||
|
|
||
|
|
||
| def _split( | ||
| request: Request, | ||
| num_new_tokens: int, | ||
| use_eagle: bool = True, | ||
| partial_hit: bool = False, | ||
| ) -> int: | ||
| """Call the real `Scheduler._mamba_block_aligned_split` on a stub self.""" | ||
| stub = SimpleNamespace( | ||
| cache_config=SimpleNamespace(block_size=MAMBA_BLOCK_SIZE), | ||
| use_eagle=use_eagle, | ||
| max_num_scheduled_tokens=16384, | ||
| scheduler_config=SimpleNamespace(long_prefill_token_threshold=0), | ||
| # `prefix_match_unit` finer than the block size (#46384). | ||
| mamba_partial_cache_hit=partial_hit, | ||
| hash_block_size=ATTN_BLOCK_SIZE, | ||
| ) | ||
| return Scheduler._mamba_block_aligned_split(stub, request, num_new_tokens) | ||
|
|
||
|
|
||
| def _run_chunked_prefill( | ||
| manager: KVCacheManager, request: Request, budgets: list[int] | ||
| ) -> dict[int, int]: | ||
| """Prefill `request`, one step per entry in `budgets`. | ||
|
|
||
| A zero-token split means the budget cannot fund an aligned chunk; the | ||
| scheduler defers the request to a later step, so this does too. | ||
|
|
||
| Returns physical mamba block id -> the token offset of the state it holds, | ||
| mirroring the GDN kernel: the running slot ends up at the chunk end. | ||
| """ | ||
| mamba_manager = manager.coordinator.single_type_managers[MAMBA_GROUP_ID] | ||
| state_at: dict[int, int] = {} | ||
| # `budgets` fragments the first steps; afterwards the request is alone and | ||
| # gets as much as it can use, so the prefill always finishes. | ||
| for step in range(len(budgets) + 64): | ||
| computed = request.num_computed_tokens | ||
| if computed >= request.num_tokens: | ||
| break | ||
| budget = budgets[step] if step < len(budgets) else request.num_tokens | ||
| num_new = _split(request, min(request.num_tokens - computed, budget)) | ||
| if num_new == 0: | ||
| continue | ||
| assert ( | ||
| manager.allocate_slots(request, num_new, num_lookahead_tokens=NUM_SPEC) | ||
| is not None | ||
| ) | ||
| request.num_computed_tokens = computed + num_new | ||
| blocks = mamba_manager.req_to_blocks[request.request_id] | ||
| running = cdiv(request.num_computed_tokens, MAMBA_BLOCK_SIZE) - 1 | ||
| state_at[blocks[running].block_id] = request.num_computed_tokens | ||
| return state_at | ||
|
|
||
|
|
||
| def _count_cached_boundary_states( | ||
| manager: KVCacheManager, request: Request, state_at: dict[int, int] | ||
| ) -> int: | ||
| """Assert every hash-cached mamba slot holds the state its hash claims. | ||
|
|
||
| Covers both full-block snapshots (`(p + 1) * block_size`) and the | ||
| partial-tail entries align mode registers at an exact token count. | ||
|
|
||
| Returns the number of cached slots checked. | ||
| """ | ||
| mamba_manager = manager.coordinator.single_type_managers[MAMBA_GROUP_ID] | ||
| checked = 0 | ||
| for pos, block in enumerate(mamba_manager.req_to_blocks[request.request_id]): | ||
| if block.is_null or block.block_hash is None: | ||
| continue | ||
| claimed = block.block_hash_num_tokens | ||
| assert state_at.get(block.block_id) == claimed, ( | ||
| f"mamba slot {pos} is hashed as state@{claimed} but holds " | ||
| f"state@{state_at.get(block.block_id)}" | ||
| ) | ||
| checked += 1 | ||
| return checked | ||
|
|
||
|
|
||
| def _prefill(prompt_len: int, budgets: list[int]) -> int: | ||
| manager = _make_hybrid_kv_cache_manager() | ||
| (request,) = create_requests(1, num_tokens=prompt_len, block_size=ATTN_BLOCK_SIZE) | ||
| state_at = _run_chunked_prefill(manager, request, budgets) | ||
| assert request.num_computed_tokens == prompt_len, "prefill did not complete" | ||
| return _count_cached_boundary_states(manager, request, state_at) | ||
|
|
||
|
|
||
| def test_fragmented_first_chunk_does_not_poison_mamba_prefix_cache() -> None: | ||
| """EAGLE zeroes `last_cache_position` for prompts under two blocks. | ||
|
|
||
| Past that point any chunk end used to be accepted, so a short first chunk | ||
| (concurrent prefills sharing the budget) left slot 0 at state@364 while the | ||
| next chunk crossed 1600 and published slot 0 as state@1600. | ||
| """ | ||
| _prefill(PROMPT_LEN, budgets=[364, PROMPT_LEN]) | ||
|
|
||
|
|
||
| def test_fragmented_tail_chunk_does_not_poison_mamba_prefix_cache() -> None: | ||
| """Same poisoning one block in, where a hit is still cacheable. | ||
|
|
||
| `last_cache_position` is 1600, so the chunk ending there is cached. The next | ||
| chunk used to be free to stop mid-block (slot 1 at state@2600) and the one | ||
| after it crossed 3200, publishing slot 1 as state@3200. | ||
| """ | ||
| assert _prefill(3602, budgets=[1600, 1000, 3602]) > 0 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("first_chunk", [800, 900, 1599, 1601, 2000]) | ||
| def test_intermediate_chunk_ends_stay_block_aligned(first_chunk: int) -> None: | ||
| """Every non-final prefill chunk must end on a mamba block boundary.""" | ||
| _prefill(PROMPT_LEN, budgets=[first_chunk, PROMPT_LEN, PROMPT_LEN]) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("block_size", "prompt_len", "budgets"), | ||
| [ | ||
| # Kimi-K3-scale mamba blocks: TP8 shards the recurrent state 8 ways | ||
| # (~1.5k block), DEP16 keeps it whole (~12k block). The first budget | ||
| # walks the request up to `last_cache_position`; the second is the | ||
| # sub-block fragment that lands in the unguarded tail region. | ||
| (1536, 30000, [27648, 1024, 30000]), | ||
| (12288, 30000, [12288, 4000, 30000]), | ||
| (12288, 41000, [24576, 8000, 41000]), | ||
| ], | ||
| ) | ||
| def test_poisoning_is_block_size_independent( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| block_size: int, | ||
| prompt_len: int, | ||
| budgets: list[int], | ||
| ) -> None: | ||
| """The invariant is per-block, so large mamba blocks are not safer.""" | ||
| import sys | ||
|
|
||
| monkeypatch.setattr(sys.modules[__name__], "MAMBA_BLOCK_SIZE", block_size) | ||
| assert _prefill(prompt_len, budgets=budgets) > 0 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("partial_hit", [False, True]) | ||
| @pytest.mark.parametrize("resume_at", [331, 1599, 1601, 2531, 3011]) | ||
| def test_unaligned_resume_never_runs_past_its_block( | ||
| partial_hit: bool, resume_at: int | ||
| ) -> None: | ||
| """A prefill resuming mid-block must re-align before crossing a boundary. | ||
|
|
||
| Reachable with a finer `prefix_match_unit` (its partial-tail stop ends a | ||
| chunk off-grid by design) and with unaligned external tokens from a KV | ||
| connector. | ||
| """ | ||
| prompt_len = 3602 | ||
| (request,) = create_requests(1, num_tokens=prompt_len, block_size=ATTN_BLOCK_SIZE) | ||
| tail_boundary = prompt_len // ATTN_BLOCK_SIZE * ATTN_BLOCK_SIZE | ||
|
|
||
| pos, ends = resume_at, [] | ||
| while pos < prompt_len: | ||
| request.num_computed_tokens = pos | ||
| num_new = _split(request, prompt_len - pos, partial_hit=partial_hit) | ||
| assert num_new > 0, f"no progress at {pos}" | ||
| if pos % MAMBA_BLOCK_SIZE != 0: | ||
| block_end = (pos // MAMBA_BLOCK_SIZE + 1) * MAMBA_BLOCK_SIZE | ||
| assert pos + num_new <= block_end, ( | ||
| f"chunk [{pos}, {pos + num_new}) starts mid-block and runs past " | ||
| f"{block_end}; the slot holding state@{pos} gets hashed as " | ||
| f"state@{block_end}" | ||
| ) | ||
| pos += num_new | ||
| ends.append(pos) | ||
|
|
||
| for end in ends[:-1]: | ||
| aligned = end % MAMBA_BLOCK_SIZE == 0 | ||
| assert aligned or (partial_hit and end == tail_boundary), ( | ||
| f"intermediate chunk end {end} is neither block-aligned nor the " | ||
| f"partial-tail boundary" | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use Google-style sections in helper docstrings.
These helper docstrings omit required
Args:and/orReturns:sections. AddReturns:for_make_hybrid_kv_cache_manager. AddArgs:andReturns:for_split,_run_chunked_prefill,_count_cached_boundary_states, and_prefill.As per coding guidelines, “Use Google-style docstrings in Python code, with
Args:/Returns:/Raises:sections.”Also applies to: 76-82, 95-105, 129-138, 153-158
🤖 Prompt for AI Agents
Source: Coding guidelines