# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from collections.abc import Callable from dataclasses import dataclass from typing import Any from unittest.mock import MagicMock, patch import numpy as np import pytest import torch from vllm.model_executor.layers.mamba.mamba_utils import ( get_conv_copy_spec, get_temporal_copy_spec, ) from vllm.v1.core.sched.output import CachedRequestData, SchedulerOutput from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheGroupSpec, MambaSpec from vllm.v1.worker.mamba_utils import ( MambaCopyBuffers, MambaSpecDecodeGPUContext, collect_mamba_copy_meta, do_mamba_copy_block, preprocess_mamba, ) MambaStateCopyFunc = Callable[..., Any] # Conv + temporal copy specs, in the order the tests' MambaSpec shapes expect. _COPY_FUNCS: tuple[MambaStateCopyFunc, ...] = ( get_conv_copy_spec, get_temporal_copy_spec, ) def postprocess_mamba( scheduler_output: "SchedulerOutput", kv_cache_config: "KVCacheConfig", input_batch: Any, requests: dict[str, Any], forward_context: dict[str, Any], mamba_state_copy_funcs: tuple[MambaStateCopyFunc, ...], copy_bufs: "MambaCopyBuffers", ): """CPU reference for the align-mode postprocess. Used as a golden against the GPU fused kernel (``postprocess_mamba_align_gpu``). Mirrors what the production code did before the fused kernel replaced it; kept here because production no longer has a CPU implementation. """ assert input_batch.mamba_state_idx_cpu is not None num_scheduled_tokens_dict = scheduler_output.num_scheduled_tokens scheduled_spec_decode_tokens_dict = scheduler_output.scheduled_spec_decode_tokens num_accepted_tokens_cpu = input_batch.num_accepted_tokens_cpu mamba_state_idx_cpu = input_batch.mamba_state_idx_cpu mamba_group_ids = copy_bufs.mamba_group_ids mamba_spec = copy_bufs.mamba_spec copy_bufs.offset = 0 for i, req_id in enumerate(input_batch.req_ids): req_state = requests[req_id] num_computed_tokens = req_state.num_computed_tokens num_draft_tokens = len(scheduled_spec_decode_tokens_dict.get(req_id, [])) num_scheduled_tokens = num_scheduled_tokens_dict[req_id] num_accepted_tokens = num_accepted_tokens_cpu[i] num_tokens_running_state = ( num_computed_tokens + num_scheduled_tokens - num_draft_tokens ) new_num_computed_tokens = num_tokens_running_state + num_accepted_tokens - 1 aligned_new_computed_tokens = ( new_num_computed_tokens // mamba_spec.block_size * mamba_spec.block_size ) if aligned_new_computed_tokens >= num_tokens_running_state: accept_token_bias = aligned_new_computed_tokens - num_tokens_running_state src_block_idx = mamba_state_idx_cpu[i] dest_block_idx = aligned_new_computed_tokens // mamba_spec.block_size - 1 collect_mamba_copy_meta( copy_bufs, kv_cache_config, mamba_state_copy_funcs, mamba_group_ids, src_block_idx, dest_block_idx, accept_token_bias, req_state, forward_context, ) if src_block_idx == dest_block_idx: num_accepted_tokens_cpu[i] = 1 do_mamba_copy_block(copy_bufs) def _make_scheduler_output( finished_req_ids: set[str], preempted_req_ids: set[str] | None, resumed_req_ids: set[str], ) -> SchedulerOutput: cached = CachedRequestData.make_empty() cached.resumed_req_ids = resumed_req_ids return SchedulerOutput( scheduled_new_reqs=[], scheduled_cached_reqs=cached, num_scheduled_tokens={}, total_num_scheduled_tokens=0, scheduled_spec_decode_tokens={}, scheduled_encoder_inputs={}, num_common_prefix_blocks=[], finished_req_ids=finished_req_ids, free_encoder_mm_hashes=[], preempted_req_ids=preempted_req_ids, ) def test_resumed_req_ids_cleared_from_mamba_state_idx(): """When a request is force-preempted (e.g. reset_prefix_cache), it appears in resumed_req_ids but NOT in preempted_req_ids. preprocess_mamba must still clear its mamba_state_idx entry, otherwise stale indices can point beyond the new block allocation. """ spec = MagicMock(block_size=64, num_speculative_blocks=0) cache_config = MagicMock(enable_prefix_caching=True) input_batch = MagicMock(req_ids=[]) copy_bufs = MagicMock(mamba_group_ids=[0], mamba_spec=spec) mamba_state_idx: dict[str, int] = { "finished": 1, "preempted": 2, "resumed": 3, # only in resumed_req_ids, NOT in preempted "keep": 99, } sched = _make_scheduler_output( finished_req_ids={"finished"}, preempted_req_ids={"preempted"}, resumed_req_ids={"resumed"}, ) with patch( "vllm.v1.worker.mamba_utils.get_mamba_groups", return_value=([0], spec), ): preprocess_mamba( sched, MagicMock(), # kv_cache_config cache_config, mamba_state_idx, input_batch, {}, # requests {}, # forward_context (), # mamba_state_copy_funcs copy_bufs, ) assert mamba_state_idx == {"keep": 99} # ----------------------------------------------------------------------------- # Golden tests for postprocess_mamba_fused_kernel # ----------------------------------------------------------------------------- @dataclass class _TestConfig: """Common test configuration for fused kernel tests.""" block_size: int = 16 num_blocks: int = 32 num_layers: int = 2 num_reqs: int = 4 max_num_reqs: int = 8 # Conv state shape: [num_blocks, conv_width, inner_dim] conv_width: int = 4 conv_inner_dim: int = 64 # Temporal state shape: [num_blocks, state_dim] temporal_state_dim: int = 128 dtype: torch.dtype = torch.float16 class _MockCpuGpuBuffer: """Mock CpuGpuBuffer for testing without pinned memory.""" def __init__(self, size: int, dtype: torch.dtype, device: torch.device): self.cpu = torch.zeros(size, dtype=dtype, device="cpu") self.gpu = torch.zeros(size, dtype=dtype, device=device) self.np = self.cpu.numpy() def copy_to_gpu(self, n: int | None = None) -> torch.Tensor: if n is None: return self.gpu.copy_(self.cpu, non_blocking=True) return self.gpu[:n].copy_(self.cpu[:n], non_blocking=True) def _make_postprocess_scheduler_output( req_ids: list[str], num_scheduled_tokens: dict[str, int], scheduled_spec_decode_tokens: dict[str, list] | None = None, ) -> SchedulerOutput: """Create a minimal SchedulerOutput for postprocess testing.""" cached = CachedRequestData.make_empty() return SchedulerOutput( scheduled_new_reqs=[], scheduled_cached_reqs=cached, num_scheduled_tokens=num_scheduled_tokens, total_num_scheduled_tokens=sum(num_scheduled_tokens.values()), scheduled_spec_decode_tokens=scheduled_spec_decode_tokens or {}, scheduled_encoder_inputs={}, num_common_prefix_blocks=[], finished_req_ids=set(), free_encoder_mm_hashes=[], preempted_req_ids=set(), ) def _make_mock_attention( conv_state: torch.Tensor, temporal_state: torch.Tensor ) -> MagicMock: """Create a mock attention object with kv_cache.""" attention = MagicMock() attention.kv_cache = [conv_state, temporal_state] return attention def _make_dual_states( cfg: "_TestConfig", layer_names: list[str], device: torch.device, *, num_blocks: int | None = None, ) -> tuple[ list[torch.Tensor], list[torch.Tensor], list[torch.Tensor], list[torch.Tensor], dict[str, MagicMock], dict[str, MagicMock], ]: """Allocate conv+temporal state tensors for the Python path, clone them for the GPU path, and build matching ``forward_context`` dicts for both. Returns ``(conv_py, temporal_py, conv_gpu, temporal_gpu, fwd_py, fwd_gpu)`` where the four state lists are parallel to ``layer_names``. """ n_blocks = num_blocks if num_blocks is not None else cfg.num_blocks conv_py = [ torch.randn( n_blocks, cfg.conv_width, cfg.conv_inner_dim, dtype=cfg.dtype, device=device, ) for _ in layer_names ] temporal_py = [ torch.randn(n_blocks, cfg.temporal_state_dim, dtype=cfg.dtype, device=device) for _ in layer_names ] conv_gpu = [s.clone() for s in conv_py] temporal_gpu = [s.clone() for s in temporal_py] fwd_py = { name: _make_mock_attention(c, t) for name, c, t in zip(layer_names, conv_py, temporal_py) } fwd_gpu = { name: _make_mock_attention(c, t) for name, c, t in zip(layer_names, conv_gpu, temporal_gpu) } return conv_py, temporal_py, conv_gpu, temporal_gpu, fwd_py, fwd_gpu def _make_dual_layer_state( cfg: "_TestConfig", device: torch.device, *, num_blocks: int | None = None, layer_name: str = "layer_0", ) -> tuple[ torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, dict[str, MagicMock], dict[str, MagicMock], ]: """Single-layer convenience form of ``_make_dual_states``.""" conv_py, temporal_py, conv_gpu, temporal_gpu, fwd_py, fwd_gpu = _make_dual_states( cfg, [layer_name], device, num_blocks=num_blocks ) return conv_py[0], temporal_py[0], conv_gpu[0], temporal_gpu[0], fwd_py, fwd_gpu def _make_kv_cache_config(cfg: _TestConfig, layer_names: list[str]) -> KVCacheConfig: """Create a KVCacheConfig with mamba groups.""" mamba_spec = MambaSpec( block_size=cfg.block_size, shapes=( (cfg.conv_width, cfg.conv_inner_dim), (cfg.temporal_state_dim,), ), dtypes=(cfg.dtype, cfg.dtype), mamba_cache_mode="all", ) group = KVCacheGroupSpec( layer_names=layer_names, kv_cache_spec=mamba_spec, ) return KVCacheConfig( num_blocks=cfg.num_blocks, kv_cache_tensors=[], kv_cache_groups=[group], ) def _make_input_batch( req_ids: list[str], num_accepted_tokens: list[int], mamba_state_idx: list[int], ) -> MagicMock: """Create a mock GPUInputBatch.""" batch = MagicMock() batch.req_ids = req_ids batch.req_id_to_index = {rid: i for i, rid in enumerate(req_ids)} # Use numpy arrays so modifications persist batch.num_accepted_tokens_cpu = np.array(num_accepted_tokens, dtype=np.int32) batch.mamba_state_idx_cpu = np.array(mamba_state_idx, dtype=np.int32) return batch def _make_requests( req_ids: list[str], num_computed_tokens: list[int], block_ids_per_req: list[list[int]], ) -> dict[str, MagicMock]: """Create mock CachedRequestState objects.""" requests = {} for i, req_id in enumerate(req_ids): req = MagicMock() req.num_computed_tokens = num_computed_tokens[i] req.block_ids = {0: block_ids_per_req[i]} # group_id=0 requests[req_id] = req return requests def _make_copy_bufs( cfg: _TestConfig, kv_cache_config: KVCacheConfig, device: torch.device ) -> MambaCopyBuffers: """Create MambaCopyBuffers for the Python path.""" def make_buffer(n, dtype): return _MockCpuGpuBuffer(n, dtype, device) return MambaCopyBuffers.create( max_num_reqs=cfg.max_num_reqs, kv_cache_config=kv_cache_config, copy_funcs=(get_conv_copy_spec, get_temporal_copy_spec), make_buffer=make_buffer, ) def _make_gpu_ctx( cfg: _TestConfig, kv_cache_config: KVCacheConfig, device: torch.device ) -> MambaSpecDecodeGPUContext: """Create MambaSpecDecodeGPUContext for the GPU path.""" def make_buffer(n, dtype): return _MockCpuGpuBuffer(n, dtype, device) return MambaSpecDecodeGPUContext.create( max_num_reqs=cfg.max_num_reqs, kv_cache_config=kv_cache_config, num_state_types=2, device=device, make_buffer=make_buffer, ) def _run_gpu_postprocess( gpu_ctx: MambaSpecDecodeGPUContext, *, kv_cache_config: KVCacheConfig, forward_context: dict[str, Any], copy_funcs: tuple, block_table: torch.Tensor, req_ids: list[str], num_accepted_tokens: list[int], mamba_state_idx: list[int], num_scheduled_tokens: dict[str, int], num_computed_tokens: list[int], num_draft_tokens: dict[str, int], device: torch.device, ) -> None: """Initialize the GPU context against `block_table`, run the fused postprocess kernel for `req_ids`, and synchronize.""" def t(values): return torch.tensor(values, dtype=torch.int32, device=device) gpu_ctx.initialize_from_forward_context( kv_cache_config, forward_context, copy_funcs, [block_table] ) gpu_ctx.run_fused_postprocess( num_reqs=len(req_ids), num_accepted_tokens_gpu=t(num_accepted_tokens), mamba_state_idx_gpu=t(mamba_state_idx), num_scheduled_tokens_gpu=t([num_scheduled_tokens[r] for r in req_ids]), num_computed_tokens_gpu=t(num_computed_tokens), num_draft_tokens_gpu=t([num_draft_tokens.get(r, 0) for r in req_ids]), ) torch.accelerator.synchronize() @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") class TestPostprocessMambaFusedKernel: """Tests for postprocess_mamba_fused_kernel comparing GPU vs CPU paths.""" @pytest.fixture def device(self): return torch.device("cuda:0") @pytest.fixture def test_config(self): return _TestConfig() def test_matches_python_postprocess_mamba(self, device, test_config): """ Golden test: GPU kernel produces identical results to Python impl. This test: 1. Sets up identical initial state for both paths 2. Runs Python postprocess_mamba (modifies states via batch_memcpy) 3. Runs GPU fused kernel (modifies states directly) 4. Compares resulting state tensors and num_accepted_tokens """ cfg = test_config torch.manual_seed(42) # Test scenario: 4 requests with different copy conditions # Copy needed when: aligned_new_computed >= num_tokens_running_state # where: num_tokens_running_state = num_computed + num_scheduled - num_draft # new_num_computed = num_tokens_running_state + num_accepted - 1 # aligned_new_computed = (new_num_computed // block_size) * block_size req_ids = ["req_0", "req_1", "req_2", "req_3"] # Configure requests so some need copies, some don't # block_size = 16 # req_0: running=60+5-2=63, new=63+3-1=65, aligned=64 >= 63 -> COPY # req_1: running=30+3-0=33, new=33+2-1=34, aligned=32 < 33 -> NO COPY # req_2: running=45+8-3=50, new=50+4-1=53, aligned=48 < 50 -> NO COPY # req_3: running=10+6-0=16, new=16+2-1=17, aligned=16 >= 16 -> COPY num_computed_tokens = [60, 30, 45, 10] num_scheduled_tokens = {"req_0": 5, "req_1": 3, "req_2": 8, "req_3": 6} num_draft_tokens = {"req_0": 2, "req_1": 0, "req_2": 3, "req_3": 0} num_accepted_tokens = [3, 2, 4, 2] mamba_state_idx = [3, 1, 2, 0] # source block indices # Block IDs for each request (simulate block table) block_ids_per_req = [ list(range(8)), # req_0: blocks 0-7 list(range(8, 16)), # req_1: blocks 8-15 list(range(16, 24)), # req_2: blocks 16-23 list(range(24, 32)), # req_3: blocks 24-31 ] layer_names = [f"layer_{i}" for i in range(cfg.num_layers)] kv_cache_config = _make_kv_cache_config(cfg, layer_names) ( conv_states_py, temporal_states_py, conv_states_gpu, temporal_states_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_states(cfg, layer_names, device) # --- Run Python path --- scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # --- Run GPU path --- gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) num_reqs = len(req_ids) max_blocks = max(len(b) for b in block_ids_per_req) block_table_gpu = torch.zeros( num_reqs, max_blocks, dtype=torch.int32, device=device ) for i, block_ids in enumerate(block_ids_per_req): block_table_gpu[i, : len(block_ids)] = torch.tensor( block_ids, dtype=torch.int32 ) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # --- Compare results --- # 1. Compare state tensors for i in range(cfg.num_layers): torch.testing.assert_close( conv_states_gpu[i], conv_states_py[i], msg=f"Conv state mismatch at layer {i}", ) torch.testing.assert_close( temporal_states_gpu[i], temporal_states_py[i], msg=f"Temporal state mismatch at layer {i}", ) # 2. Compare num_accepted_tokens updates expected_accepted = torch.tensor( input_batch_py.num_accepted_tokens_cpu[:num_reqs], dtype=torch.int32, device=device, ) torch.testing.assert_close( gpu_ctx.num_accepted_tokens_out[:num_reqs], expected_accepted, msg="num_accepted_tokens mismatch", ) def test_no_copy_when_not_needed(self, device, test_config): """Kernel should not modify state when no copy is needed.""" cfg = test_config torch.manual_seed(123) # Single request where no copy is needed: # running = 30 + 3 = 33, new = 33 + 1 - 1 = 33, aligned = 32 < 33 req_ids = ["req_0"] num_computed_tokens = [30] num_scheduled_tokens = {"req_0": 3} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [1] mamba_state_idx = [1] block_ids_per_req = [list(range(8))] layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) # Create state tensor conv_state = torch.randn( cfg.num_blocks, cfg.conv_width, cfg.conv_inner_dim, dtype=cfg.dtype, device=device, ) temporal_state = torch.randn( cfg.num_blocks, cfg.temporal_state_dim, dtype=cfg.dtype, device=device ) # Clone to verify no modification conv_state_orig = conv_state.clone() temporal_state_orig = temporal_state.clone() forward_context = {"layer_0": _make_mock_attention(conv_state, temporal_state)} gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) num_reqs = len(req_ids) block_table_gpu = torch.zeros(num_reqs, 8, dtype=torch.int32, device=device) block_table_gpu[0, :8] = torch.tensor(block_ids_per_req[0], dtype=torch.int32) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # State should be unchanged torch.testing.assert_close(conv_state, conv_state_orig) torch.testing.assert_close(temporal_state, temporal_state_orig) @pytest.mark.parametrize("num_reqs", [1, 2, 8, 16]) def test_various_batch_sizes(self, device, test_config, num_reqs): """Verify kernel works correctly with different batch sizes.""" cfg = _TestConfig(max_num_reqs=max(16, num_reqs)) torch.manual_seed(456) req_ids = [f"req_{i}" for i in range(num_reqs)] # All requests will trigger a copy num_computed_tokens = [60] * num_reqs num_scheduled_tokens = {r: 5 for r in req_ids} num_draft_tokens = {r: 0 for r in req_ids} num_accepted_tokens = [3] * num_reqs mamba_state_idx = [3] * num_reqs # Each request gets unique blocks block_ids_per_req = [list(range(i * 8, (i + 1) * 8)) for i in range(num_reqs)] # Ensure we have enough blocks total_blocks = num_reqs * 8 cfg = _TestConfig(num_blocks=total_blocks, max_num_reqs=max(16, num_reqs)) layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) ( conv_state_py, temporal_state_py, conv_state_gpu, temporal_state_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_layer_state(cfg, device) # Run Python path scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # Run GPU path gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) max_blocks_per_req = 8 block_table_gpu = torch.zeros( num_reqs, max_blocks_per_req, dtype=torch.int32, device=device ) for i, block_ids in enumerate(block_ids_per_req): block_table_gpu[i, : len(block_ids)] = torch.tensor( block_ids, dtype=torch.int32 ) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # Compare results torch.testing.assert_close( conv_state_gpu, conv_state_py, msg="Conv state mismatch" ) torch.testing.assert_close( temporal_state_gpu, temporal_state_py, msg="Temporal state mismatch" ) def test_block_table_with_realistic_stride(self, device, test_config): """ Test kernel with realistic block table strides. In real usage, the block table is pre-allocated with shape [max_num_reqs, max_num_blocks_per_req] and then sliced to [:num_reqs]. This means stride(0) = max_num_blocks_per_req, which is typically much larger than the actual blocks used. This test verifies the kernel handles non-tight strides correctly, catching bugs where stride is incorrectly treated as bytes vs elements. """ cfg = test_config torch.manual_seed(789) # Use multiple requests to exercise stride-based indexing num_reqs = 4 req_ids = [f"req_{i}" for i in range(num_reqs)] # All requests trigger copies (same setup as test_various_batch_sizes) num_computed_tokens = [60] * num_reqs num_scheduled_tokens = {r: 5 for r in req_ids} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [3] * num_reqs mamba_state_idx = [3] * num_reqs # Each request uses only 8 blocks, but we allocate much more blocks_used_per_req = 8 block_ids_per_req = [ list(range(i * blocks_used_per_req, (i + 1) * blocks_used_per_req)) for i in range(num_reqs) ] total_blocks = num_reqs * blocks_used_per_req cfg = _TestConfig(num_blocks=total_blocks, max_num_reqs=max(16, num_reqs)) layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) ( conv_state_py, temporal_state_py, conv_state_gpu, temporal_state_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_layer_state(cfg, device) # Run Python path scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # Run GPU path with REALISTIC block table stride gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) # KEY DIFFERENCE: Create a large block table like real code does # Real system has max_num_blocks_per_req >> blocks actually used max_num_reqs_full = 16 max_blocks_per_req_full = 512 # Much larger than blocks_used_per_req=8 # Allocate full-size table (simulates pre-allocated CpuGpuBuffer) block_table_full = torch.zeros( max_num_reqs_full, max_blocks_per_req_full, dtype=torch.int32, device=device ) # Fill in actual block IDs (only first few columns used) for i, block_ids in enumerate(block_ids_per_req): block_table_full[i, : len(block_ids)] = torch.tensor( block_ids, dtype=torch.int32 ) # Slice like real code: block_table.gpu[:num_reqs] # This preserves stride(0) = 512, not 8! block_table_gpu = block_table_full[:num_reqs] # Verify stride is large (the key property we're testing) assert block_table_gpu.stride(0) == max_blocks_per_req_full, ( f"Expected stride {max_blocks_per_req_full}, " f"got {block_table_gpu.stride(0)}" ) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # Compare results - this will fail if stride handling is incorrect torch.testing.assert_close( conv_state_gpu, conv_state_py, msg="Conv state mismatch - possible stride bug in kernel", ) torch.testing.assert_close( temporal_state_gpu, temporal_state_py, msg="Temporal state mismatch - possible stride bug in kernel", ) def test_src_addr_equals_dst_addr_skips_copy_and_sets_accepted_to_1( self, device, test_config ): """ Test the ``src_addr == dst_addr`` early-return path in postprocess_mamba_fused_kernel matches Python behavior. When src_addr == dst_addr (source and destination memory addresses are identical), both implementations should: 1. Skip the copy (state unchanged) 2. Set num_accepted_tokens to 1 This condition occurs when: - src_block_idx == dest_block_idx (same logical block) - accept_token_bias == 0 (no offset within the block) Python reference (collect_mamba_copy_meta): if src_block_idx == dest_block_idx and accept_token_bias == 0: return # No copy added Python reference (postprocess_mamba): if src_block_idx == dest_block_idx: num_accepted_tokens_cpu[i] = 1 Test setup (block_size=16): - num_tokens_running_state = 30 + 2 - 0 = 32 - new_num_computed = 32 + 1 - 1 = 32 - aligned_new_computed = 32 - accept_token_bias = 32 - 32 = 0 - dest_block_idx = 32 // 16 - 1 = 1 - src_block_idx = 1 (set explicitly) """ cfg = test_config torch.manual_seed(1001) req_ids = ["req_0"] num_computed_tokens = [30] num_scheduled_tokens = {"req_0": 2} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [5] # Initial value, should become 1 mamba_state_idx = [1] # src_block_idx = 1 = dest_block_idx block_ids_per_req = [list(range(8))] layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) ( conv_state_py, temporal_state_py, conv_state_gpu, temporal_state_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_layer_state(cfg, device) # Also clone to verify no modification conv_state_orig = conv_state_py.clone() temporal_state_orig = temporal_state_py.clone() # --- Run Python path --- scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # --- Run GPU path --- gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) num_reqs = len(req_ids) block_table_gpu = torch.zeros(num_reqs, 8, dtype=torch.int32, device=device) block_table_gpu[0, :8] = torch.tensor(block_ids_per_req[0], dtype=torch.int32) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # --- Verify Python behavior (ground truth) --- # State should be unchanged (no copy when src_addr == dst_addr) torch.testing.assert_close( conv_state_py, conv_state_orig, msg="Python: Conv state should be unchanged when src==dst", ) torch.testing.assert_close( temporal_state_py, temporal_state_orig, msg="Python: Temporal state should be unchanged when src==dst", ) # num_accepted_tokens should be 1 assert input_batch_py.num_accepted_tokens_cpu[0] == 1, ( f"Python: num_accepted_tokens should be 1, " f"got {input_batch_py.num_accepted_tokens_cpu[0]}" ) # --- Verify GPU matches Python --- torch.testing.assert_close( conv_state_gpu, conv_state_py, msg="GPU conv state should match Python", ) torch.testing.assert_close( temporal_state_gpu, temporal_state_py, msg="GPU temporal state should match Python", ) expected_accepted = torch.tensor( input_batch_py.num_accepted_tokens_cpu[:num_reqs], dtype=torch.int32, device=device, ) torch.testing.assert_close( gpu_ctx.num_accepted_tokens_out[:num_reqs], expected_accepted, msg="GPU num_accepted_tokens should match Python", ) def test_same_block_idx_with_offset_copies_then_sets_accepted_to_1( self, device, test_config ): """ Test the ``src_block_idx == dest_block_idx`` post-copy update in postprocess_mamba_fused_kernel matches Python behavior. When src_block_idx == dest_block_idx but accept_token_bias > 0, both implementations should: 1. Perform the copy (src_addr != dst_addr due to offset) 2. Set num_accepted_tokens to 1 AFTER the copy Python reference (postprocess_mamba): if src_block_idx == dest_block_idx: num_accepted_tokens_cpu[i] = 1 For conv states: copies state[block, offset:] to state[block, :] (shifted window) For temporal states: copies state[block_ids[src_idx + offset]] to state[block_ids[dest_idx]] Test setup (block_size=16): - num_tokens_running_state = 30 + 1 - 0 = 31 - new_num_computed = 31 + 2 - 1 = 32 - aligned_new_computed = 32 - accept_token_bias = 32 - 31 = 1 (> 0, so copy happens) - dest_block_idx = 32 // 16 - 1 = 1 - src_block_idx = 1 (set explicitly, == dest_block_idx) """ cfg = test_config torch.manual_seed(1002) req_ids = ["req_0"] num_computed_tokens = [30] num_scheduled_tokens = {"req_0": 1} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [2] # Results in accept_token_bias = 1 mamba_state_idx = [1] # src_block_idx = 1 = dest_block_idx block_ids_per_req = [list(range(8))] layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) ( conv_state_py, temporal_state_py, conv_state_gpu, temporal_state_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_layer_state(cfg, device) # Clone to verify modification conv_state_orig = conv_state_py.clone() temporal_state_orig = temporal_state_py.clone() # --- Run Python path --- scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # --- Run GPU path --- gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) num_reqs = len(req_ids) block_table_gpu = torch.zeros(num_reqs, 8, dtype=torch.int32, device=device) block_table_gpu[0, :8] = torch.tensor(block_ids_per_req[0], dtype=torch.int32) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # --- Verify Python behavior (ground truth) --- dest_block_id = block_ids_per_req[0][1] # dest_block_idx = 1 # Conv state should be modified (shifted copy within block) conv_changed = not torch.allclose( conv_state_py[dest_block_id], conv_state_orig[dest_block_id] ) assert conv_changed, ( "Python: Conv state should be modified when accept_token_bias > 0" ) # Temporal state should be modified (copy from different block) src_block_id_temporal = block_ids_per_req[0][2] # actual_src_block_idx = 2 dest_block_id_temporal = block_ids_per_req[0][1] # dest_block_idx = 1 torch.testing.assert_close( temporal_state_py[dest_block_id_temporal], temporal_state_orig[src_block_id_temporal], msg="Python: Temporal state copy should have happened", ) # num_accepted_tokens should be 1 assert input_batch_py.num_accepted_tokens_cpu[0] == 1, ( f"Python: num_accepted_tokens should be 1, " f"got {input_batch_py.num_accepted_tokens_cpu[0]}" ) # --- Verify GPU matches Python --- torch.testing.assert_close( conv_state_gpu, conv_state_py, msg="GPU conv state should match Python", ) torch.testing.assert_close( temporal_state_gpu, temporal_state_py, msg="GPU temporal state should match Python", ) expected_accepted = torch.tensor( input_batch_py.num_accepted_tokens_cpu[:num_reqs], dtype=torch.int32, device=device, ) torch.testing.assert_close( gpu_ctx.num_accepted_tokens_out[:num_reqs], expected_accepted, msg="GPU num_accepted_tokens should match Python", ) def test_different_block_idx_copies_without_setting_accepted_to_1( self, device, test_config ): """ Test that neither special-case path triggers when src_block_idx != dest_block_idx, and GPU matches Python behavior. When copying between different blocks: 1. src_addr != dst_addr (different blocks = different addresses) 2. src_block_idx != dest_block_idx Therefore: - The ``src_addr == dst_addr`` early-return does NOT trigger - The ``src_block_idx == dest_block_idx`` post-copy update does NOT trigger - Copy happens normally - num_accepted_tokens remains UNCHANGED Test setup (block_size=16): - num_tokens_running_state = 60 + 3 - 0 = 63 - new_num_computed = 63 + 3 - 1 = 65 - aligned_new_computed = 64 - accept_token_bias = 64 - 63 = 1 - dest_block_idx = 64 // 16 - 1 = 3 - src_block_idx = 2 (set explicitly, != dest_block_idx) """ cfg = test_config torch.manual_seed(1003) req_ids = ["req_0"] num_computed_tokens = [60] num_scheduled_tokens = {"req_0": 3} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [3] # Should remain 3, NOT set to 1 mamba_state_idx = [2] # src_block_idx = 2, dest_block_idx will be 3 block_ids_per_req = [list(range(8))] layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) ( conv_state_py, temporal_state_py, conv_state_gpu, temporal_state_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_layer_state(cfg, device) # Clone to verify modification conv_state_orig = conv_state_py.clone() # --- Run Python path --- scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # --- Run GPU path --- gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) num_reqs = len(req_ids) block_table_gpu = torch.zeros(num_reqs, 8, dtype=torch.int32, device=device) block_table_gpu[0, :8] = torch.tensor(block_ids_per_req[0], dtype=torch.int32) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # --- Verify Python behavior (ground truth) --- dest_block_id = block_ids_per_req[0][3] # dest_block_idx = 3 # Copy DID happen (dest block should be modified) conv_changed = not torch.allclose( conv_state_py[dest_block_id], conv_state_orig[dest_block_id] ) assert conv_changed, "Python: Conv state copy should have happened" # num_accepted_tokens should NOT be changed to 1 assert input_batch_py.num_accepted_tokens_cpu[0] == num_accepted_tokens[0], ( f"Python: num_accepted_tokens should remain {num_accepted_tokens[0]}, " f"got {input_batch_py.num_accepted_tokens_cpu[0]}" ) # --- Verify GPU matches Python --- torch.testing.assert_close( conv_state_gpu, conv_state_py, msg="GPU conv state should match Python", ) torch.testing.assert_close( temporal_state_gpu, temporal_state_py, msg="GPU temporal state should match Python", ) expected_accepted = torch.tensor( input_batch_py.num_accepted_tokens_cpu[:num_reqs], dtype=torch.int32, device=device, ) torch.testing.assert_close( gpu_ctx.num_accepted_tokens_out[:num_reqs], expected_accepted, msg="GPU num_accepted_tokens should match Python", ) def test_prefix_caching_shared_block_does_not_set_accepted_to_1( self, device, test_config ): """ Regression test: with prefix caching, different logical block indices can map to the same physical block. The kernel must NOT set num_accepted_tokens to 1 in that case. When src_block_idx != dest_block_idx but block_table maps both to the same physical block ID, src_addr == dst_addr. The copy is correctly skipped (self-copy is a no-op), but num_accepted_tokens must be preserved — only logical-index equality justifies setting it to 1. Test setup (block_size=16): - num_tokens_running_state = 30 + 2 - 0 = 32 - new_num_computed = 32 + 3 - 1 = 34 - aligned_new_computed = 32 - accept_token_bias = 32 - 32 = 0 - dest_block_idx = 32 // 16 - 1 = 1 - src_block_idx = 0 (set explicitly, != dest_block_idx) - block_ids = [5, 5, ...] — prefix caching: both logical indices map to the same physical block 5 """ cfg = test_config torch.manual_seed(2001) req_ids = ["req_0"] num_computed_tokens = [30] num_scheduled_tokens = {"req_0": 2} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [3] # Must stay 3, NOT become 1 mamba_state_idx = [0] # src_block_idx = 0, dest_block_idx will be 1 # Prefix caching: logical blocks 0 and 1 share physical block 5 block_ids_per_req = [[5, 5, 2, 3, 4, 6, 7, 8]] layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) ( conv_state_py, temporal_state_py, conv_state_gpu, temporal_state_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_layer_state(cfg, device) conv_state_orig = conv_state_py.clone() temporal_state_orig = temporal_state_py.clone() # --- Run Python path --- scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # --- Run GPU path --- gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) num_reqs = len(req_ids) block_table_gpu = torch.zeros(num_reqs, 8, dtype=torch.int32, device=device) block_table_gpu[0, :8] = torch.tensor(block_ids_per_req[0], dtype=torch.int32) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # --- Verify Python behavior (ground truth) --- # Copy is self-to-self (same physical block), state unchanged torch.testing.assert_close( conv_state_py, conv_state_orig, msg="Python: Conv state should be unchanged (self-copy)", ) torch.testing.assert_close( temporal_state_py, temporal_state_orig, msg="Python: Temporal state should be unchanged (self-copy)", ) # num_accepted_tokens must NOT be set to 1 (src_block_idx != dest_block_idx) assert input_batch_py.num_accepted_tokens_cpu[0] == num_accepted_tokens[0], ( f"Python: num_accepted_tokens should remain {num_accepted_tokens[0]}, " f"got {input_batch_py.num_accepted_tokens_cpu[0]}" ) # --- Verify GPU matches Python --- torch.testing.assert_close( conv_state_gpu, conv_state_py, msg="GPU conv state should match Python", ) torch.testing.assert_close( temporal_state_gpu, temporal_state_py, msg="GPU temporal state should match Python", ) expected_accepted = torch.tensor( input_batch_py.num_accepted_tokens_cpu[:num_reqs], dtype=torch.int32, device=device, ) torch.testing.assert_close( gpu_ctx.num_accepted_tokens_out[:num_reqs], expected_accepted, msg="GPU num_accepted_tokens should match Python (must NOT be 1)", ) def test_prefix_caching_nonsequential_block_ids_boundary(self, device, test_config): """ Regression test: non-sequential physical block IDs under prefix caching with the needs_copy boundary at exact equality. Under PC, the block allocator assigns physical block IDs in arbitrary order (e.g., [17, 3, 42, 9] instead of [0, 1, 2, 3]). The needs_copy condition is purely token-count based and must evaluate identically regardless of the physical block IDs assigned. This test verifies that the kernel's address arithmetic (block_table lookup, stride computation) produces correct copies when physical IDs are non-sequential. Two requests exercise different boundary behaviors: - req_0: aligned_new_computed == num_tokens_running_state (exact boundary) This is the tightest edge: one fewer accepted token and no copy needed. - req_1: aligned_new_computed == num_tokens_running_state (exact boundary) Different block layout, src!=dest, real copy happens. Both use non-sequential block IDs typical of PC reuse patterns. Test setup (block_size=16): req_0: - num_tokens_running_state = 48 + 0 - 0 = 48 - new_num_computed = 48 + 1 - 1 = 48 - aligned_new_computed = 48 - needs_copy = (48 >= 48) = True (exact boundary!) - accept_token_bias = 48 - 48 = 0 - dest_block_idx = 48 // 16 - 1 = 2 - src_block_idx = 2 (same as dest -> num_accepted = 1) req_1: - num_tokens_running_state = 31 + 1 - 0 = 32 - new_num_computed = 32 + 3 - 1 = 34 - aligned_new_computed = 32 - needs_copy = (32 >= 32) = True (exact boundary!) - accept_token_bias = 32 - 32 = 0 - dest_block_idx = 32 // 16 - 1 = 1 - src_block_idx = 0 (diff from dest -> num_accepted unchanged) """ cfg = test_config torch.manual_seed(4001) req_ids = ["req_0", "req_1"] num_computed_tokens = [48, 31] num_scheduled_tokens = {"req_0": 0, "req_1": 1} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [1, 3] mamba_state_idx = [2, 0] # Non-sequential block IDs typical of prefix caching allocation block_ids_per_req = [ [17, 3, 42, 9, 25, 11, 30, 2], # req_0: scattered physical blocks [41, 7, 22, 15, 38, 19, 4, 28], # req_1: different scattered blocks ] layer_names = [f"layer_{i}" for i in range(cfg.num_layers)] # Need enough physical blocks for the scattered IDs num_blocks = 50 local_cfg = _TestConfig(num_blocks=num_blocks, max_num_reqs=cfg.max_num_reqs) kv_cache_config = _make_kv_cache_config(local_cfg, layer_names) ( conv_states_py, temporal_states_py, conv_states_gpu, temporal_states_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_states(local_cfg, layer_names, device) # --- Run Python path --- scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(local_cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # --- Run GPU path --- gpu_ctx = _make_gpu_ctx(local_cfg, kv_cache_config, device) num_reqs = len(req_ids) max_blocks = max(len(b) for b in block_ids_per_req) block_table_gpu = torch.zeros( num_reqs, max_blocks, dtype=torch.int32, device=device ) for i, block_ids in enumerate(block_ids_per_req): block_table_gpu[i, : len(block_ids)] = torch.tensor( block_ids, dtype=torch.int32 ) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # --- Compare results --- for i in range(cfg.num_layers): torch.testing.assert_close( conv_states_gpu[i], conv_states_py[i], msg=f"Conv state mismatch at layer {i} with non-sequential block IDs", ) torch.testing.assert_close( temporal_states_gpu[i], temporal_states_py[i], msg=( f"Temporal state mismatch at layer {i} " f"with non-sequential block IDs" ), ) expected_accepted = torch.tensor( input_batch_py.num_accepted_tokens_cpu[:num_reqs], dtype=torch.int32, device=device, ) torch.testing.assert_close( gpu_ctx.num_accepted_tokens_out[:num_reqs], expected_accepted, msg="num_accepted_tokens mismatch with non-sequential block IDs", ) # Verify req_0 had num_accepted set to 1 (src==dest) and req_1 unchanged assert input_batch_py.num_accepted_tokens_cpu[0] == 1 assert input_batch_py.num_accepted_tokens_cpu[1] == num_accepted_tokens[1] def test_prefix_caching_mixed_shared_and_distinct_blocks(self, device, test_config): """ Regression test: mixed batch under prefix caching where some requests have shared physical blocks (aliased) and others have distinct blocks, with the needs_copy boundary at various positions. This tests the interaction between: 1. PC block aliasing (src and dest map to same physical block) 2. The needs_copy boundary (exact equality vs well-past vs no-copy) 3. Non-sequential physical block IDs Batch of 4 requests: - req_0: needs_copy=True, src!=dest, shared physical block (PC aliased) -> copy skipped (src_addr==dst_addr), num_accepted PRESERVED - req_1: needs_copy=True, src!=dest, distinct blocks, non-sequential IDs -> real copy happens, num_accepted PRESERVED - req_2: needs_copy=False (below boundary) -> no action at all - req_3: needs_copy=True, src==dest (exact boundary, zero bias) -> copy skipped (self-copy), num_accepted SET TO 1 Test setup (block_size=16): req_0: running=30+2-0=32, new=32+3-1=34, aligned=32, 32>=32 -> COPY bias=0, dest=32//16-1=1, src=0 (!=dest) block_ids=[5,5,...] -> same physical -> skip, keep accepted=3 req_1: running=60+5-2=63, new=63+3-1=65, aligned=64, 64>=63 -> COPY bias=1, dest=64//16-1=3, src=2 (!=dest) block_ids=[41,7,22,15,...] -> distinct -> real copy, keep accepted=3 req_2: running=30+3-0=33, new=33+1-1=33, aligned=32, 32<33 -> NO COPY req_3: running=48+0-0=48, new=48+1-1=48, aligned=48, 48>=48 -> COPY bias=0, dest=48//16-1=2, src=2 (==dest) block_ids=[10,20,30,...] -> distinct IDs, same logical idx -> self-copy (src_addr==dst_addr), set accepted=1 """ cfg = test_config torch.manual_seed(5001) req_ids = ["req_0", "req_1", "req_2", "req_3"] num_computed_tokens = [30, 60, 30, 48] num_scheduled_tokens = {"req_0": 2, "req_1": 5, "req_2": 3, "req_3": 0} num_draft_tokens = {"req_1": 2} num_accepted_tokens = [3, 3, 1, 1] mamba_state_idx = [0, 2, 1, 2] # Block IDs with various PC patterns: # req_0: shared blocks (PC alias: logical 0 and 1 -> physical 5) # req_1: distinct non-sequential blocks # req_2: doesn't matter (no copy) # req_3: distinct sequential blocks (no aliasing) block_ids_per_req = [ [5, 5, 12, 18, 23, 31, 44, 2], # req_0: blocks 0,1 share phys 5 [41, 7, 22, 15, 38, 19, 4, 28], # req_1: all distinct [10, 20, 30, 40, 1, 6, 8, 14], # req_2: irrelevant [10, 20, 30, 40, 1, 6, 8, 14], # req_3: distinct, dest=src=idx 2 ] layer_names = [f"layer_{i}" for i in range(cfg.num_layers)] num_blocks = 50 local_cfg = _TestConfig(num_blocks=num_blocks, max_num_reqs=cfg.max_num_reqs) kv_cache_config = _make_kv_cache_config(local_cfg, layer_names) ( conv_states_py, temporal_states_py, conv_states_gpu, temporal_states_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_states(local_cfg, layer_names, device) # --- Run Python path --- scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(local_cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # --- Run GPU path --- gpu_ctx = _make_gpu_ctx(local_cfg, kv_cache_config, device) num_reqs = len(req_ids) max_blocks = max(len(b) for b in block_ids_per_req) block_table_gpu = torch.zeros( num_reqs, max_blocks, dtype=torch.int32, device=device ) for i, block_ids in enumerate(block_ids_per_req): block_table_gpu[i, : len(block_ids)] = torch.tensor( block_ids, dtype=torch.int32 ) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # --- Compare all state tensors --- for i in range(cfg.num_layers): torch.testing.assert_close( conv_states_gpu[i], conv_states_py[i], msg=( f"Conv state mismatch at layer {i} — " f"mixed PC batch with shared/distinct blocks" ), ) torch.testing.assert_close( temporal_states_gpu[i], temporal_states_py[i], msg=( f"Temporal state mismatch at layer {i} — " f"mixed PC batch with shared/distinct blocks" ), ) # --- Compare num_accepted_tokens --- expected_accepted = torch.tensor( input_batch_py.num_accepted_tokens_cpu[:num_reqs], dtype=torch.int32, device=device, ) torch.testing.assert_close( gpu_ctx.num_accepted_tokens_out[:num_reqs], expected_accepted, msg="num_accepted_tokens mismatch in mixed PC batch", ) # Verify per-request expectations: # req_0: src!=dest, shared block -> preserved (3) assert input_batch_py.num_accepted_tokens_cpu[0] == 3 # req_1: src!=dest, distinct blocks -> preserved (3) assert input_batch_py.num_accepted_tokens_cpu[1] == 3 # req_2: no copy -> preserved (1) assert input_batch_py.num_accepted_tokens_cpu[2] == 1 # req_3: src==dest -> set to 1 assert input_batch_py.num_accepted_tokens_cpu[3] == 1 def test_pc_aliased_blocks_skip_must_use_logical_idx_not_addr( self, device, test_config ): """ Regression test for 6466ce0d vs 959ca0fd: the kernel's early-return guard must compare logical block indices, not physical addresses. Under prefix caching, different logical blocks (src_block_idx=0, dest_block_idx=1) can map to the same physical block. When accept_token_bias=0, this makes src_addr == dst_addr for BOTH conv and temporal states. A buggy guard `if src_addr == dst_addr` would incorrectly set num_accepted_tokens=1; the correct guard is `if src_block_idx == dest_block_idx and accept_token_bias == 0`. The Python reference only sets num_accepted_tokens=1 when src_block_idx == dest_block_idx (line 79 of postprocess_mamba). With src_block_idx=0, dest_block_idx=1, num_accepted_tokens must be preserved even though the physical addresses match. Test setup (block_size=16): - num_tokens_running_state = 30 + 2 - 0 = 32 - new_num_computed = 32 + 3 - 1 = 34 - aligned_new_computed = 32 - needs_copy = (32 >= 32) = True - accept_token_bias = 32 - 32 = 0 - dest_block_idx = 32 // 16 - 1 = 1 - src_block_idx = 0 (explicitly, != dest_block_idx) - block_ids = [7, 7, ...] -> physical aliasing via prefix caching Expected: num_accepted_tokens stays 3 (not set to 1). Bug (959ca0fd): kernel saw src_addr == dst_addr, set it to 1. """ cfg = test_config torch.manual_seed(6001) req_ids = ["req_0"] num_computed_tokens = [30] num_scheduled_tokens = {"req_0": 2} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [3] mamba_state_idx = [0] # src_block_idx = 0 # Prefix caching: logical blocks 0 and 1 both map to physical block 7. block_ids_per_req = [[7, 7, 10, 11, 12, 13, 14, 15]] layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) ( conv_state_py, temporal_state_py, conv_state_gpu, temporal_state_gpu, forward_context_py, forward_context_gpu, ) = _make_dual_layer_state(cfg, device) # --- Run Python path --- scheduler_output = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) input_batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( scheduler_output, kv_cache_config, input_batch_py, requests, forward_context_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # Python reference: src_block_idx(0) != dest_block_idx(1) -> no change assert input_batch_py.num_accepted_tokens_cpu[0] == 3, ( f"Python: num_accepted_tokens should remain 3, " f"got {input_batch_py.num_accepted_tokens_cpu[0]}" ) # --- Run GPU path --- gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) num_reqs = len(req_ids) block_table_gpu = torch.zeros(num_reqs, 8, dtype=torch.int32, device=device) block_table_gpu[0, :8] = torch.tensor(block_ids_per_req[0], dtype=torch.int32) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=forward_context_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # The critical assertion: kernel must NOT set num_accepted_tokens to 1 # when src_block_idx != dest_block_idx, even though src_addr == dst_addr # due to prefix caching aliasing. # # Old kernel (959ca0fd): `if src_addr == dst_addr` -> FAILS here (sets 1) # Fixed kernel (6466ce0d): `if src_block_idx == dest_block_idx and # accept_token_bias == 0` -> PASSES (preserves 3) kernel_accepted = gpu_ctx.num_accepted_tokens_out[0].item() assert kernel_accepted == 3, ( f"Kernel set num_accepted_tokens to {kernel_accepted} but expected 3. " f"The early-return guard likely compared physical addresses " f"(src_addr == dst_addr) instead of logical block indices " f"(src_block_idx == dest_block_idx). Under prefix caching, " f"different logical blocks can share the same physical block." ) # Also verify state tensors match Python torch.testing.assert_close( conv_state_gpu, conv_state_py, msg="GPU conv state should match Python", ) torch.testing.assert_close( temporal_state_gpu, temporal_state_py, msg="GPU temporal state should match Python", ) def test_as_strided_temporal_copy_size(self, device, test_config): """ Regression test for 240723d46: temporal copy_size must be inner_size * elem_size, not state_block_stride. In production (gpu_model_runner.py), conv and temporal states share a raw buffer via torch.as_strided where stride(0) equals page_size_bytes / elem_size — larger than either state's natural element count. Using stride(0) as copy_size for temporal states overwrites into the next block's conv region. Layout per page (384 float16 elements = 768 bytes): [conv: 256 elems | temporal: 128 elems] The test triggers a temporal copy from block 4 to block 3. With the bug the kernel copies 768 bytes (page stride) instead of 256 bytes (128 * 2), overwriting conv_state[4] with conv_state[5]'s data. Test setup (block_size=16): - running = 60 + 5 - 2 = 63 - new = 63 + 3 - 1 = 65 - aligned = 64 >= 63 -> COPY needed - accept_token_bias = 64 - 63 = 1 - dest_block_idx = 64 // 16 - 1 = 3 - temporal: actual_src_block_idx = 3 + 1 = 4 (block_ids[4] = 4) """ cfg = test_config torch.manual_seed(7001) req_ids = ["req_0"] num_computed_tokens = [60] num_scheduled_tokens = {"req_0": 5} num_draft_tokens = {"req_0": 2} num_accepted_tokens = [3] mamba_state_idx = [3] block_ids_per_req = [list(range(8))] layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) # --- Production-like packed layout (mirrors gpu_model_runner.py) --- conv_shape = (cfg.conv_width, cfg.conv_inner_dim) temporal_shape = (cfg.temporal_state_dim,) dtype = cfg.dtype elem_size = torch.tensor([], dtype=dtype).element_size() conv_natural_elems = cfg.conv_width * cfg.conv_inner_dim temporal_natural_elems = cfg.temporal_state_dim page_size_bytes = (conv_natural_elems + temporal_natural_elems) * elem_size num_element_per_page = page_size_bytes // elem_size assert num_element_per_page > temporal_natural_elems, ( "Test requires padded stride; page must be larger than one state" ) raw_py = torch.randn( cfg.num_blocks * num_element_per_page, dtype=dtype, device=device ) raw_gpu = raw_py.clone() def make_views(raw): conv_tgt = (cfg.num_blocks, *conv_shape) conv_nat_stride = torch.empty(conv_tgt).stride() conv = torch.as_strided( raw, size=conv_tgt, stride=(num_element_per_page, *conv_nat_stride[1:]), storage_offset=0, ) temp_tgt = (cfg.num_blocks, *temporal_shape) temp_nat_stride = torch.empty(temp_tgt).stride() temp = torch.as_strided( raw, size=temp_tgt, stride=(num_element_per_page, *temp_nat_stride[1:]), storage_offset=conv_natural_elems, ) return conv, temp conv_py, temp_py = make_views(raw_py) conv_gpu, temp_gpu = make_views(raw_gpu) fwd_py = {"layer_0": _make_mock_attention(conv_py, temp_py)} fwd_gpu = {"layer_0": _make_mock_attention(conv_gpu, temp_gpu)} # --- Python reference --- sched = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( sched, kv_cache_config, batch_py, requests, fwd_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # --- GPU fused kernel --- gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) num_reqs = 1 block_table = torch.zeros(num_reqs, 8, dtype=torch.int32, device=device) block_table[0, :8] = torch.tensor(block_ids_per_req[0], dtype=torch.int32) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=fwd_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # --- Assertions --- # With the bug (pre-240723d46), the kernel copies page_size_bytes # (768) for temporal state instead of 256 bytes, overwriting # conv_state[4] with conv_state[5]'s data. torch.testing.assert_close( conv_gpu, conv_py, msg=( "Conv state corrupted: temporal copy_size was likely " "state_block_stride instead of inner_size * elem_size" ), ) torch.testing.assert_close( temp_gpu, temp_py, msg="Temporal state mismatch", ) expected_accepted = torch.tensor( batch_py.num_accepted_tokens_cpu[:num_reqs], dtype=torch.int32, device=device, ) torch.testing.assert_close( gpu_ctx.num_accepted_tokens_out[:num_reqs], expected_accepted, msg="num_accepted_tokens mismatch", ) def test_temporal_copy_with_bias_ge_2(self, device, test_config): """ Coverage test for the temporal-state block-table stride arithmetic when ``accept_token_bias >= 2``. The kernel computes, for temporal (non-conv) states:: actual_src_block_idx = src_block_idx + accept_token_bias actual_src_block_id = block_table[req, actual_src_block_idx] All prior regression tests exercise only ``bias == 1``, i.e. they only ever read one slot ahead of ``src_block_idx`` in the block table. An off-by-one (or missing scale) in the address computation on line 143 of ``mamba_utils.py`` would be invisible to every existing test but would silently read the wrong physical block on any speculative-decode cycle that accepts multiple tokens across a block boundary, feeding a stale hidden state forward one step. Setup (block_size=16): - running = 28 + 2 - 0 = 30 - new = 30 + 3 - 1 = 32 - aligned = 32 >= 30 -> COPY needed - bias = 32 - 30 = 2 (key: >= 2) - dest_idx = 32 // 16 - 1 = 1 - src_idx = 1 (same as dest -> exercises post-copy accepted=1 write) - temporal actual_src_block_idx = 1 + 2 = 3 (reads block_table[0, 3]) With identity block_ids = [0,1,2,3,...], an off-by-one that used bias=1 would copy from block_ids[2]=2 instead of block_ids[3]=3, producing a clear state-value mismatch against the Python reference. """ cfg = test_config torch.manual_seed(7002) req_ids = ["req_0"] num_computed_tokens = [28] num_scheduled_tokens = {"req_0": 2} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [3] # -> accept_token_bias = 2 mamba_state_idx = [1] # src_block_idx = 1 = dest_block_idx block_ids_per_req = [list(range(8))] layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) ( conv_state_py, temporal_state_py, conv_state_gpu, temporal_state_gpu, fwd_py, fwd_gpu, ) = _make_dual_layer_state(cfg, device) temporal_state_orig = temporal_state_py.clone() # --- Python reference --- sched = _make_postprocess_scheduler_output( req_ids, num_scheduled_tokens, {k: [None] * v for k, v in num_draft_tokens.items() if v > 0}, ) batch_py = _make_input_batch( req_ids, num_accepted_tokens.copy(), mamba_state_idx.copy() ) requests = _make_requests(req_ids, num_computed_tokens, block_ids_per_req) copy_bufs = _make_copy_bufs(cfg, kv_cache_config, device) postprocess_mamba( sched, kv_cache_config, batch_py, requests, fwd_py, _COPY_FUNCS, copy_bufs, ) torch.accelerator.synchronize() # --- GPU fused kernel --- gpu_ctx = _make_gpu_ctx(cfg, kv_cache_config, device) num_reqs = 1 block_table = torch.zeros(num_reqs, 8, dtype=torch.int32, device=device) block_table[0, :8] = torch.tensor(block_ids_per_req[0], dtype=torch.int32) _run_gpu_postprocess( gpu_ctx, kv_cache_config=kv_cache_config, forward_context=fwd_gpu, copy_funcs=_COPY_FUNCS, block_table=block_table, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) # --- Ground truth: Python must have sourced temporal from block 3 --- actual_src_block_id = block_ids_per_req[0][3] # == 3 dest_block_id = block_ids_per_req[0][1] # == 1 torch.testing.assert_close( temporal_state_py[dest_block_id], temporal_state_orig[actual_src_block_id], msg=( "Python reference did not copy from block_ids[src+bias]=3; " "test preconditions are wrong" ), ) # --- GPU kernel must match Python byte-for-byte --- torch.testing.assert_close( conv_state_gpu, conv_state_py, msg="Conv state mismatch at accept_token_bias=2", ) torch.testing.assert_close( temporal_state_gpu, temporal_state_py, msg=( "Temporal state mismatch at accept_token_bias=2: the kernel " "likely read the wrong slot of the block table " "(actual_src_block_idx stride arithmetic)" ), ) expected_accepted = torch.tensor( batch_py.num_accepted_tokens_cpu[:num_reqs], dtype=torch.int32, device=device, ) torch.testing.assert_close( gpu_ctx.num_accepted_tokens_out[:num_reqs], expected_accepted, msg="num_accepted_tokens mismatch at accept_token_bias=2", ) def test_ds_conv_layout_bias_gt_0_byte_equal_to_sd( self, device, test_config, monkeypatch ): """DS conv postprocess should match SD when accept_token_bias > 0.""" from vllm.model_executor.layers.mamba import mamba_utils as model_mamba_utils cfg = test_config torch.manual_seed(38898) req_ids = ["req_0"] num_computed_tokens = [30] num_scheduled_tokens = {"req_0": 1} num_draft_tokens: dict[str, int] = {} num_accepted_tokens = [2] # Results in accept_token_bias = 1 mamba_state_idx = [1] # src_block_idx = 1 = dest_block_idx block_ids_per_req = [list(range(8))] layer_names = ["layer_0"] kv_cache_config = _make_kv_cache_config(cfg, layer_names) num_reqs = len(req_ids) block_table_gpu = torch.zeros(num_reqs, 8, dtype=torch.int32, device=device) block_table_gpu[0, :8] = torch.tensor(block_ids_per_req[0], dtype=torch.int32) # Same logical conv state in SD and DS layouts. sd_source_conv = torch.randn( cfg.num_blocks, cfg.conv_width, cfg.conv_inner_dim, dtype=cfg.dtype, device=device, ) ds_source_conv = sd_source_conv.permute(0, 2, 1).contiguous() sd_source_temporal = torch.randn( cfg.num_blocks, cfg.temporal_state_dim, dtype=cfg.dtype, device=device ) # SD GPU path. Default layout is SD. model_mamba_utils.get_conv_state_layout.cache_clear() sd_conv = sd_source_conv.clone() sd_temporal = sd_source_temporal.clone() forward_context_sd = { "layer_0": _make_mock_attention(sd_conv, sd_temporal), } gpu_ctx_sd = _make_gpu_ctx(cfg, kv_cache_config, device) _run_gpu_postprocess( gpu_ctx_sd, kv_cache_config=kv_cache_config, forward_context=forward_context_sd, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) torch.accelerator.synchronize() # Sanity: SD path actually modified the state (copy was performed). assert not torch.equal(sd_conv, sd_source_conv), ( "SD baseline did not modify conv state; test setup is wrong" ) # DS GPU path on the DS twin. monkeypatch.setenv("VLLM_SSM_CONV_STATE_LAYOUT", "DS") model_mamba_utils.get_conv_state_layout.cache_clear() try: ds_conv = ds_source_conv.clone() ds_temporal = sd_source_temporal.clone() forward_context_ds = { "layer_0": _make_mock_attention(ds_conv, ds_temporal), } gpu_ctx_ds = _make_gpu_ctx(cfg, kv_cache_config, device) _run_gpu_postprocess( gpu_ctx_ds, kv_cache_config=kv_cache_config, forward_context=forward_context_ds, copy_funcs=_COPY_FUNCS, block_table=block_table_gpu, req_ids=req_ids, num_accepted_tokens=num_accepted_tokens, mamba_state_idx=mamba_state_idx, num_scheduled_tokens=num_scheduled_tokens, num_computed_tokens=num_computed_tokens, num_draft_tokens=num_draft_tokens, device=device, ) torch.accelerator.synchronize() finally: # Reset the lru cache so other tests see the default layout again. model_mamba_utils.get_conv_state_layout.cache_clear() # DS bytes, un-permuted, should match the SD result. torch.testing.assert_close( ds_conv.permute(0, 2, 1).contiguous(), sd_conv, msg=( "DS conv post-kernel does not match SD baseline; the DS " "row-loop in postprocess_mamba_fused_kernel is wrong." ), ) torch.testing.assert_close( ds_temporal, sd_temporal, msg="DS temporal state diverged from SD", ) torch.testing.assert_close( gpu_ctx_ds.num_accepted_tokens_out[:num_reqs], gpu_ctx_sd.num_accepted_tokens_out[:num_reqs], msg="DS num_accepted_tokens diverged from SD", )