# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """A declaration must match the cache that actually gets allocated. Two earlier versions of this file could not fail. The first asserted hand-typed constants against each other. The second built real `transformers` caches but compared them to specs written in the test, so no production declaration was ever executed. What matters here is that a real declarer is called and its numbers are checked against a real cache built the way the model builds it. """ import ast import pathlib from types import SimpleNamespace import pytest import torch from transformers import Qwen2Config from transformers.cache_utils import DynamicCache, StaticCache from vllm_omni.model_executor.models.model_local_kv import ( HasModelLocalKV, ModelLocalKVScope, ModelLocalKVSpec, RowDriver, collect_model_local_kv_specs, spec_from_hf_config, ) pytestmark = [pytest.mark.core_model, pytest.mark.cpu] MODELS_DIR = pathlib.Path(__file__).resolve().parents[3] / "vllm_omni" / "model_executor" / "models" def _total_bytes(model: object, max_num_seqs: int = 1) -> int: """Sum declared peak bytes across a model's declarations.""" return sum(spec.peak_bytes(max_num_seqs) for _, spec in collect_model_local_kv_specs(model)) def _cache_bytes(cache) -> int: total = 0 for layer in cache.layers: for tensor in (layer.keys, layer.values): if tensor is not None: total += tensor.numel() * tensor.element_size() return total def _qwen2_config(layers=4, kv_heads=2, heads=8, hidden=256) -> Qwen2Config: return Qwen2Config( num_hidden_layers=layers, num_attention_heads=heads, num_key_value_heads=kv_heads, hidden_size=hidden, intermediate_size=hidden * 2, vocab_size=1000, ) # -------------------------------------------------------------------------- # A real declarer, called, checked against a real cache. # -------------------------------------------------------------------------- def test_ming_declaration_matches_the_cache_it_describes(): """Calls MingAudioGenerator.model_local_kv_specs and checks the bytes. Builds the declarer without its weights -- only `_llm_config` and `_model` are read -- then builds the `StaticCache` that `_init_kv_cache` builds from the same config and compares. A wrong layer count, kv-head count, dtype or capacity in the production declaration fails here. """ from vllm_omni.model_executor.models.ming_flash_omni.talker_module import MingAudioGenerator config = _qwen2_config() generator = object.__new__(MingAudioGenerator) generator._llm_config = config generator._model = torch.nn.Linear(1, 1).to(torch.float16) (spec,) = generator.model_local_kv_specs() cache = StaticCache( config=config, max_batch_size=1, max_cache_len=MingAudioGenerator._STATIC_CACHE_LEN, device="cpu", dtype=torch.float16, ) head_dim = config.hidden_size // config.num_attention_heads for layer_idx in range(config.num_hidden_layers): keys = torch.zeros(1, config.num_key_value_heads, 1, head_dim, dtype=torch.float16) cache.update(keys, keys.clone(), layer_idx) assert spec.peak_bytes(1) == _cache_bytes(cache) def test_ming_does_not_scale_with_max_num_seqs(): """forward() takes runtime_additional_information[0] and loops inline. One worker holds one of these no matter how many sequences the scheduler admits. A previous revision derived the multiplier from scope and reported `max_num_seqs` times the real figure. """ from vllm_omni.model_executor.models.ming_flash_omni.talker_module import MingAudioGenerator generator = object.__new__(MingAudioGenerator) generator._llm_config = _qwen2_config() generator._model = torch.nn.Linear(1, 1).to(torch.float16) (spec,) = generator.model_local_kv_specs() assert spec.peak_bytes(64) == spec.peak_bytes(1) def test_growing_cache_declaration_matches_a_real_dynamic_cache(): """The declared positions are the extent the tensor reaches when full.""" layers, kv_heads, head_dim, positions = 4, 2, 32, 11 config = _qwen2_config(layers=layers, kv_heads=kv_heads, heads=kv_heads, hidden=kv_heads * head_dim) spec = spec_from_hf_config( config, name="local_transformer", dtype=torch.bfloat16, physical_capacity_positions=positions, capacity_source="group_size + max(delay_pattern)", scope=ModelLocalKVScope.INVOCATION, rows=RowDriver.FIXED, rows_reason="test", ) cache = DynamicCache() for layer_idx in range(layers): keys = torch.zeros(1, kv_heads, positions, head_dim, dtype=torch.bfloat16) cache.update(keys, torch.zeros_like(keys), layer_idx) assert spec.peak_bytes() == _cache_bytes(cache) def test_multiplicity_does_not_claim_an_object_layout(): """One allocation B rows wide costs the same as B one-row allocations. The protocol reports bytes and deliberately does not distinguish the two. An earlier revision carried a separate `allocations` count and got the object topology wrong in three of four declarers, which is worse than not claiming it: the layout belongs in `allocation_note`, where it cannot be mistaken for arithmetic. """ config = _qwen2_config(layers=4, kv_heads=2, heads=2, hidden=64) common = dict( config=config, name="c", dtype=torch.bfloat16, physical_capacity_positions=11, capacity_source="test", scope=ModelLocalKVScope.INVOCATION, ) batched = spec_from_hf_config(rows=RowDriver.MAX_NUM_SEQS, **common) assert batched.row_count(64) == 64 assert batched.peak_bytes(64) == batched.bytes_per_row * 64 assert not hasattr(batched, "allocations"), "the object-count axis was removed on purpose" def test_a_fixed_driver_ignores_concurrency(): """A serialized or capture-pinned cache does not widen with max_num_seqs.""" spec = spec_from_hf_config( _qwen2_config(), name="graph_pool", dtype=torch.bfloat16, physical_capacity_positions=11, capacity_source="test", scope=ModelLocalKVScope.MODEL, rows=RowDriver.FIXED, rows_fixed=261, rows_reason="sum of captured buckets", ) assert spec.row_count(1) == 261 assert spec.row_count(64) == 261 assert spec.peak_bytes(64) == spec.bytes_per_row * 261 def test_qwen3_tts_declares_nothing_on_the_stateless_path(): """Calls the real decoder declarer with no cache-holding mode configured. `_forward_exact` runs `pre_transformer` without `use_cache`, so stateless decoding allocates no KV. A revision that declared a per-request cache unconditionally invented over a gigabyte at max_num_seqs=256, and the revision before it declared nothing at all under enforce_eager, where the eager async-chunk path does allocate. Both are regressions this pins. """ from vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz import ( modeling_qwen3_tts_tokenizer_v2 as mod, ) decoder = object.__new__(mod.Qwen3TTSTokenizerV2Decoder) # nn.Module.__setattr__ needs the machinery __init__ would have built, and # this instance deliberately has none of it. setattr_ = object.__setattr__ setattr_( decoder, "config", SimpleNamespace( sliding_window=72, num_hidden_layers=8, num_key_value_heads=16, head_dim=64, hidden_size=1024, num_attention_heads=16, ), ) setattr_(decoder, "parameters", lambda: iter([torch.zeros(1, dtype=torch.float32)])) setattr_(decoder, "_cudagraph_wrapper", None) assert decoder.model_local_kv_specs() == [] setattr_(decoder, "_cudagraph_wrapper", SimpleNamespace(async_chunk=False, model_local_kv_specs=lambda: [])) assert decoder.model_local_kv_specs() == [] setattr_(decoder, "_cudagraph_wrapper", SimpleNamespace(async_chunk=True, model_local_kv_specs=lambda: [])) names = [s.name for s in decoder.model_local_kv_specs()] assert names == ["codec_decoder_request", "codec_decoder_working_copy"], names def test_fixed_rejects_unreviewable_values(): """A bare or unexplained constant cannot be reviewed, so it is rejected.""" common = dict( name="x", layers=1, kv_heads=1, head_dim=1, dtype=torch.float16, physical_capacity_positions=1, capacity_source="", scope=ModelLocalKVScope.REQUEST, ) with pytest.raises(ValueError, match="rows_reason"): ModelLocalKVSpec(rows=RowDriver.FIXED, rows_fixed=1, rows_reason=" ", **common) with pytest.raises(ValueError, match=">= 1"): ModelLocalKVSpec(rows=RowDriver.FIXED, rows_fixed=0, rows_reason="test", **common) def test_rows_is_required(): """Defaulting the batch extent is how the previous guarantee died. The dataclass declared it required while the helper every declarer uses defaulted it to 1, so nothing enforced it where it mattered. """ with pytest.raises(TypeError): spec_from_hf_config( # type: ignore[call-arg] _qwen2_config(), name="x", dtype=torch.float16, physical_capacity_positions=1, capacity_source="", scope=ModelLocalKVScope.REQUEST, ) def test_spec_from_hf_config_raises_rather_than_guessing(): class Empty: pass with pytest.raises(ValueError, match="layer count"): spec_from_hf_config( Empty(), name="x", dtype=torch.float16, physical_capacity_positions=1, capacity_source="", scope=ModelLocalKVScope.REQUEST, rows=RowDriver.FIXED, rows_reason="test", ) # -------------------------------------------------------------------------- # Collection # -------------------------------------------------------------------------- def _spec(**overrides) -> ModelLocalKVSpec: kwargs = dict( name="c", layers=2, kv_heads=2, head_dim=4, dtype=torch.float16, physical_capacity_positions=8, capacity_source="test", scope=ModelLocalKVScope.REQUEST, rows=RowDriver.FIXED, rows_reason="test", ) kwargs.update(overrides) return ModelLocalKVSpec(**kwargs) class _Owner(torch.nn.Module): def model_local_kv_specs(self): return [_spec()] class _CUDAGraphWrapperLike: """Mirrors vllm.compilation.cuda_graph.CUDAGraphWrapper. Not an nn.Module, and its __getattr__ forwards everything the runnable has. That forwarding is why a previous revision's "unwrap fix" was unnecessary, and why a walk that does not unwrap can enter the tree twice. """ def __init__(self, runnable): self.runnable = runnable def unwrap(self): return self.runnable def __getattr__(self, key): return getattr(self.runnable, key) def test_collector_finds_owners_nested_in_the_module_tree(): class Mid(torch.nn.Module): def __init__(self): super().__init__() self.inner = _Owner() class Top(torch.nn.Module): def __init__(self): super().__init__() self.mid = Mid() model = Top() assert [path for path, _ in collect_model_local_kv_specs(model)] == ["mid.inner"] # 2 layers * 2 kv * 4 head_dim * 8 positions * 2 bytes * 2 (K and V) = 512 assert _total_bytes(model) == 512 def test_a_root_declarer_behind_a_wrapper_is_counted_once(): """The wrapper forwards attributes, so a naive walk sees it twice.""" class Root(_Owner): pass wrapped = _CUDAGraphWrapperLike(Root()) collected = collect_model_local_kv_specs(wrapped) assert len(collected) == 1, collected assert _total_bytes(wrapped) == 512 def test_collector_reaches_owners_through_a_wrapper(): class Top(torch.nn.Module): def __init__(self): super().__init__() self.owner = _Owner() wrapped = _CUDAGraphWrapperLike(Top()) assert [path for path, _ in collect_model_local_kv_specs(wrapped)] == ["owner"] def test_collector_survives_an_owner_that_raises(): class Broken(torch.nn.Module): def model_local_kv_specs(self): raise RuntimeError("checkpoint field missing") class Top(torch.nn.Module): def __init__(self): super().__init__() self.broken = Broken() self.ok = _Owner() assert _total_bytes(Top()) == 512 def test_undeclared_models_report_zero_not_an_error(): assert _total_bytes(object()) == 0 assert _total_bytes(torch.nn.Linear(2, 2)) == 0 def test_protocol_is_structural(): assert isinstance(_Owner(), HasModelLocalKV) assert not isinstance(torch.nn.Linear(2, 2), HasModelLocalKV) @pytest.mark.parametrize( ("relative_path", "owner"), [ ("qwen3_tts/segmented_graph_wrapper.py", "CUDAGraphDecoderWrapper"), ("qwen3_tts/tokenizer_12hz/modeling_qwen3_tts_tokenizer_v2.py", "Qwen3TTSTokenizerV2Decoder"), ("mimo_audio/mimo_audio_llm.py", "MiMoAudioLLMForConditionalGeneration"), ("minicpmo_4_5/minicpmo_4_5_omni_llm.py", "MiniCPMWhisperEncoder"), ("ming_flash_omni/talker_module.py", "MingAudioGenerator"), ("ming_flash_omni/ming_flash_omni_talker.py", "MingFlashOmniTalkerForConditionalGeneration"), ], ) def test_known_cache_owners_still_declare(relative_path: str, owner: str): """Parsed rather than imported: these modules need CUDA and real weights. Weak by construction -- it only proves the method exists. The ming tests above are the ones that check a declaration's contents. """ tree = ast.parse((MODELS_DIR / relative_path).read_text()) classes = [node for node in ast.walk(tree) if isinstance(node, ast.ClassDef) and node.name == owner] assert classes, f"{owner} not found in {relative_path}" methods = {child.name for cls in classes for child in cls.body if isinstance(child, ast.FunctionDef)} assert "model_local_kv_specs" in methods, f"{owner} no longer declares its model-local KV"