# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """CPU-kernel-vs-eager-reference parity tests for the DeepSeek-V4 CPU port's sgl-kernels ops (csrc/cpu/sgl-kernels/). Each test carries its own reference reimplementation of the kernel's math rather than depending on production model code. """ from unittest import mock import pytest import torch import vllm.model_executor.kernels.mhc as mhc_kernels from tests.kernels.test_fused_deepseek_v4_qnorm_rope_kv_insert import ( HEAD_DIM, NOPE_DIM, QUANT_BLOCK, ROPE_DIM, make_cos_sin_cache, ) from tests.kernels.utils import bf16_ulp_distance, fp8_ulp_distance from vllm._custom_ops import ( compress_norm_rope_store_cpu, compress_norm_rope_store_indexer_cpu, flash_mla_with_kvcache_cpu, fp8_paged_mqa_logits_cpu, fused_indexer_q_rope_quant_cpu, fused_qnorm_rope_kv_insert_cpu, inverse_gptj_rope_o_proj_cpu, save_partial_states_cpu, ) from vllm.models.deepseek_v4.common.ops import fused_indexer_q_rope_quant from vllm.platforms import current_platform from vllm.utils.torch_utils import set_random_seed DEVICE = current_platform.device_type # Main attention cache (fp8_ds_mla): NoPE fp8 in QUANT_BLOCK-wide UE8M0 blocks, # RoPE bf16, one scale byte per block (+1 pad byte) per token. SCALE_BYTES_PER_TOKEN = NOPE_DIM // QUANT_BLOCK + 1 TOKEN_DATA_BYTES = NOPE_DIM + ROPE_DIM * 2 FP8_MAX = 448.0 # Indexer K-cache: all 128 dims in one fp8 block, one raw fp32 scale per token. INDEXER_HEAD_DIM = 128 INDEXER_NOPE_DIM = INDEXER_HEAD_DIM - ROPE_DIM INDEXER_SCALE_BYTES = 4 def _op_available(*names: str) -> bool: return all(hasattr(torch.ops._C, name) for name in names) pytestmark = pytest.mark.skipif( not current_platform.is_cpu() or not _op_available( "save_partial_states_cpu", "compress_norm_rope_store_cpu", "compress_norm_rope_store_indexer_cpu", "flash_mla_with_kvcache_cpu", "fused_qnorm_rope_kv_insert_cpu", "inverse_gptj_rope_o_proj_cpu", "fp8_paged_mqa_logits_cpu", "fused_indexer_q_rope_quant_cpu", ), reason="CPU not available or DeepSeek-V4 CPU kernels not built in", ) def _apply_gptj_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor): x_even = x[..., 0::2] x_odd = x[..., 1::2] out_even = x_even * cos - x_odd * sin out_odd = x_even * sin + x_odd * cos return torch.stack((out_even, out_odd), dim=-1).flatten(-2) def _make_cache(num_blocks: int, block_size: int) -> torch.Tensor: block_bytes = block_size * TOKEN_DATA_BYTES + block_size * SCALE_BYTES_PER_TOKEN return torch.zeros(num_blocks, block_bytes, dtype=torch.uint8) def _decode_token_regions( cache: torch.Tensor, slot_mapping: torch.Tensor, block_size: int ): """Slice out, per valid token, the raw NoPE-fp8 / RoPE-bf16 / scale byte regions this layout defines.""" valid = slot_mapping >= 0 idx = valid.nonzero(as_tuple=True)[0] slots = slot_mapping[idx] block_idx = torch.div(slots, block_size, rounding_mode="floor") pos_in_block = slots % block_size nope, rope, scale = [], [], [] for b, p in zip(block_idx.tolist(), pos_in_block.tolist()): token_off = p * TOKEN_DATA_BYTES nope.append(cache[b, token_off : token_off + NOPE_DIM]) rope.append(cache[b, token_off + NOPE_DIM : token_off + TOKEN_DATA_BYTES]) scale_off = block_size * TOKEN_DATA_BYTES + p * SCALE_BYTES_PER_TOKEN scale.append(cache[b, scale_off : scale_off + SCALE_BYTES_PER_TOKEN - 1]) if not nope: empty = lambda n: torch.empty(0, n, dtype=torch.uint8) return empty(NOPE_DIM), empty(ROPE_DIM * 2), empty(SCALE_BYTES_PER_TOKEN - 1) return torch.stack(nope), torch.stack(rope), torch.stack(scale) def _assert_cache_parity(cache_fused, cache_ref, slot_mapping, block_size): """NoPE fp8 + the UE8M0 scale byte are a deterministic quant of an un-rotated bf16 value, so they match tightly; the RoPE region is rotated in fp32 before its bf16 store and eager/kernel can round a tie to opposite sides, so allow <=1 ULP there.""" nope_ref, rope_ref, scale_ref = _decode_token_regions( cache_ref, slot_mapping, block_size ) nope_fused, rope_fused, scale_fused = _decode_token_regions( cache_fused, slot_mapping, block_size ) if nope_ref.numel() == 0: return max_ulp = int(fp8_ulp_distance(nope_fused, nope_ref).max().item()) assert max_ulp <= 1, f"NoPE fp8 differs by {max_ulp} ULP (>1)" rope_ref_bf16 = rope_ref.contiguous().view(torch.bfloat16) rope_fused_bf16 = rope_fused.contiguous().view(torch.bfloat16) max_ulp_rope = int(bf16_ulp_distance(rope_fused_bf16, rope_ref_bf16).max().item()) assert max_ulp_rope <= 1, f"RoPE bf16 differs by {max_ulp_rope} ULP (>1)" scale_diff = (scale_fused.int() - scale_ref.int()).abs().max().item() assert scale_diff <= 1, f"scale byte differs by {scale_diff} (>1)" def _make_indexer_cache(num_blocks: int, block_size: int) -> torch.Tensor: block_bytes = block_size * INDEXER_HEAD_DIM + block_size * INDEXER_SCALE_BYTES return torch.zeros(num_blocks, block_bytes, dtype=torch.uint8) def _assert_indexer_cache_parity(cache_fused, cache_ref, slot_mapping, block_size): valid = slot_mapping >= 0 idx = valid.nonzero(as_tuple=True)[0] if idx.numel() == 0: return slots = slot_mapping[idx] block_idx = torch.div(slots, block_size, rounding_mode="floor") pos_in_block = slots % block_size fp8_ref, scale_ref = [], [] fp8_fused, scale_fused = [], [] for b, p in zip(block_idx.tolist(), pos_in_block.tolist()): token_off = p * INDEXER_HEAD_DIM scale_off = block_size * INDEXER_HEAD_DIM + p * INDEXER_SCALE_BYTES fp8_ref.append(cache_ref[b, token_off : token_off + INDEXER_HEAD_DIM]) fp8_fused.append(cache_fused[b, token_off : token_off + INDEXER_HEAD_DIM]) scale_ref.append(cache_ref[b, scale_off : scale_off + INDEXER_SCALE_BYTES]) scale_fused.append(cache_fused[b, scale_off : scale_off + INDEXER_SCALE_BYTES]) fp8_ref_t = torch.stack(fp8_ref).view(torch.float8_e4m3fn) fp8_fused_t = torch.stack(fp8_fused).view(torch.float8_e4m3fn) torch.testing.assert_close(fp8_fused_t.float(), fp8_ref_t.float()) scale_ref_t = torch.stack(scale_ref).view(torch.float32) scale_fused_t = torch.stack(scale_fused).view(torch.float32) torch.testing.assert_close(scale_fused_t, scale_ref_t) # --------------------------------------------------------------------------- # save_partial_states_cpu / compress_norm_rope_store_cpu (compressor.cpp) # --------------------------------------------------------------------------- # # The compressor's state cache is pooled with the main KV cache's physical # pages and page-aligned, so it is genuinely 3D with a block-to-block stride # that need not equal block_size * width -- built here via torch.as_strided # over an over-sized flat buffer rather than a plain contiguous tensor, so a # regression back to flattening via .view() would be caught. def _make_padded_block_cache( num_blocks: int, block_size: int, width: int, pad_elems: int ) -> torch.Tensor: page_stride = block_size * width + pad_elems flat = torch.randn(num_blocks, page_stride, dtype=torch.float32) return torch.as_strided( flat, (num_blocks, block_size, width), (page_stride, width, 1) ) def _dup_padded_block_cache( num_blocks: int, block_size: int, width: int, pad_elems: int ) -> tuple[torch.Tensor, torch.Tensor]: """Two independently-stored padded caches with identical values, for ops that mutate the cache in place.""" page_stride = block_size * width + pad_elems template = torch.randn(num_blocks, page_stride, dtype=torch.float32) def _view(flat: torch.Tensor) -> torch.Tensor: return torch.as_strided( flat, (num_blocks, block_size, width), (page_stride, width, 1) ) return _view(template.clone()), _view(template.clone()) def _run_save_partial_states_eager( state_cache, kv, score, ape, positions, slot_mapping, compress_ratio ): """Reference: state_cache[block_idx, pos_in_block, :W] = kv; state_cache[..., W:] = score + ape[position % compress_ratio]; skip slot < 0. Mutates state_cache in place, matching the op's contract.""" state_width = kv.shape[-1] block_size = state_cache.shape[1] valid = slot_mapping >= 0 idx = valid.nonzero(as_tuple=True)[0] slots = slot_mapping[idx] block_idx = slots // block_size pos_in_block = slots % block_size state_cache[block_idx, pos_in_block, :state_width] = kv[idx] ape_row = ape[positions[idx] % compress_ratio] state_cache[block_idx, pos_in_block, state_width:] = score[idx] + ape_row @pytest.mark.parametrize("pad_elems", [0, 24]) @pytest.mark.parametrize("num_tokens", [1, 17]) def test_save_partial_states_cpu_matches_eager(num_tokens: int, pad_elems: int): torch.manual_seed(0) compress_ratio = 4 block_size = 4 state_width = 512 width = 2 * state_width num_blocks = (num_tokens + 4 + block_size - 1) // block_size + 1 num_slots = num_blocks * block_size kv = torch.randn(num_tokens, state_width, dtype=torch.float32) score = torch.randn(num_tokens, state_width, dtype=torch.float32) ape = torch.randn(compress_ratio, state_width, dtype=torch.float32) positions = torch.randint(0, 4096, (num_tokens,), dtype=torch.int64) ref_cache, fused_cache = _dup_padded_block_cache( num_blocks, block_size, width, pad_elems ) slot_mapping = torch.randperm(num_slots)[:num_tokens].to(torch.int64) if num_tokens > 1: slot_mapping[-1] = -1 # exercise the "skip invalid slot" branch _run_save_partial_states_eager( ref_cache, kv, score, ape, positions, slot_mapping, compress_ratio ) save_partial_states_cpu(kv, score, ape, positions, fused_cache, slot_mapping) torch.testing.assert_close(fused_cache, ref_cache) def _run_compress_eager( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, rms_norm_eps, cos_sin_cache, kv_cache, kv_cache_block_size, compress_ratio, ): """Reference for the per-channel-softmax-pooling + RMSNorm + NoPE-UE8M0 quant + GPT-J RoPE math writing the main (head_dim=512) fp8_ds_mla layout.""" num_tokens, window = gather_slots.shape state_width = state_cache.shape[-1] // 2 state_block_size = state_cache.shape[1] num_quant_blocks = NOPE_DIM // QUANT_BLOCK w_idx = torch.arange(window) head_offset = torch.where(w_idx >= compress_ratio, HEAD_DIM, 0) for t in range(num_tokens): position = int(positions[t]) if (position + 1) % compress_ratio != 0: continue kv_slot = int(kv_slot_mapping[t]) if kv_slot < 0: continue slots = gather_slots[t] valid_w = slots >= 0 safe_slots = slots.clamp(min=0) block_idx = safe_slots // state_block_size pos_in_block = safe_slots % state_block_size rows = state_cache[block_idx, pos_in_block] # [window, 2*state_width] col = head_offset.unsqueeze(-1) + torch.arange(HEAD_DIM) kv_vals = torch.gather(rows, 1, col) score_col = state_width + col score_vals = torch.gather(rows, 1, score_col) score_vals = torch.where( valid_w.unsqueeze(-1), score_vals, torch.full_like(score_vals, -torch.inf) ) weights = torch.softmax(score_vals, dim=0) compressed = (kv_vals * weights).sum(dim=0) # [HEAD_DIM] variance = compressed.pow(2).mean() normed = compressed * torch.rsqrt(variance + rms_norm_eps) * rms_norm_weight quant_input = normed.to(torch.bfloat16).to(torch.float32) compressed_pos = (position // compress_ratio) * compress_ratio cos = cos_sin_cache[compressed_pos, : ROPE_DIM // 2] sin = cos_sin_cache[compressed_pos, ROPE_DIM // 2 :] rotated_rope = _apply_gptj_rope(normed[NOPE_DIM:].clone(), cos, sin) nope_vals = quant_input[:NOPE_DIM].view(num_quant_blocks, QUANT_BLOCK) absmax = nope_vals.abs().amax(dim=-1).clamp(min=1e-4) exponent = torch.ceil(torch.log2(absmax / FP8_MAX)) inv_scale = torch.exp2(-exponent) scaled = (nope_vals * inv_scale.unsqueeze(-1)).clamp(-FP8_MAX, FP8_MAX) nope_fp8_bytes = scaled.to(torch.float8_e4m3fn).reshape(-1).view(torch.uint8) scale_bytes = (exponent + 127.0).clamp(0, 255).to(torch.uint8) rope_bf16_bytes = rotated_rope.to(torch.bfloat16).contiguous().view(torch.uint8) block_idx_kv = kv_slot // kv_cache_block_size pos_in_block_kv = kv_slot % kv_cache_block_size token_off = pos_in_block_kv * TOKEN_DATA_BYTES kv_cache[block_idx_kv, token_off : token_off + NOPE_DIM] = nope_fp8_bytes kv_cache[block_idx_kv, token_off + NOPE_DIM : token_off + TOKEN_DATA_BYTES] = ( rope_bf16_bytes ) scale_off = ( kv_cache_block_size * TOKEN_DATA_BYTES + pos_in_block_kv * SCALE_BYTES_PER_TOKEN ) kv_cache[block_idx_kv, scale_off : scale_off + num_quant_blocks] = scale_bytes kv_cache[block_idx_kv, scale_off + num_quant_blocks] = 0 def _make_compress_inputs( num_tokens: int, compress_ratio: int, overlap: bool, pad_elems: int = 24, max_pos: int = 4096, invalid_window_frac: float = 0.1, head_dim: int = HEAD_DIM, ): torch.manual_seed(0) coff = 1 + int(overlap) state_width = coff * head_dim window = coff * compress_ratio state_block_size = 4 if compress_ratio == 4 else 8 num_state_slots = num_tokens * window + 8 num_state_blocks = (num_state_slots + state_block_size - 1) // state_block_size + 1 state_cache = _make_padded_block_cache( num_state_blocks, state_block_size, 2 * state_width, pad_elems ) gather_slots = torch.randint( 0, num_state_blocks * state_block_size, (num_tokens, window) ).to(torch.int64) invalid_mask = torch.rand(num_tokens, window) < invalid_window_frac # Never invalidate every column of a row (would force max=-inf -> NaN). invalid_mask[:, -1] = False gather_slots = torch.where( invalid_mask, torch.full_like(gather_slots, -1), gather_slots ) base = (torch.arange(num_tokens) + 1) * compress_ratio is_boundary = torch.arange(num_tokens) % 2 == 0 positions = torch.where(is_boundary, base - 1, base + 1).to(torch.int64) kv_slot_mapping = torch.arange(num_tokens, dtype=torch.int64) if num_tokens > 1: kv_slot_mapping[1] = -1 # exercise the "skip invalid output slot" branch rms_norm_weight = torch.randn(head_dim, dtype=torch.float32) cos_sin_cache = make_cos_sin_cache(max_pos, ROPE_DIM, torch.float32, "cpu") return ( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, cos_sin_cache, is_boundary, ) @pytest.mark.parametrize("compress_ratio,overlap", [(128, False), (4, True)]) @pytest.mark.parametrize("num_tokens", [1, 17]) def test_compress_norm_rope_store_cpu_matches_eager( compress_ratio: int, overlap: bool, num_tokens: int ): eps = 1e-6 block_size = 16 ( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, cos_sin_cache, is_boundary, ) = _make_compress_inputs(num_tokens, compress_ratio, overlap) num_blocks = (num_tokens + block_size - 1) // block_size + 1 kv_cache_ref = _make_cache(num_blocks, block_size) kv_cache_fused = kv_cache_ref.clone() _run_compress_eager( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, eps, cos_sin_cache, kv_cache_ref, block_size, compress_ratio, ) compress_norm_rope_store_cpu( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, eps, cos_sin_cache, kv_cache_fused, block_size, compress_ratio, ) # Only tokens that hit the compression boundary (and have a valid output # slot) were written -- restrict parity checks to those. written_slot_mapping = torch.where( is_boundary, kv_slot_mapping, torch.full_like(kv_slot_mapping, -1) ) _assert_cache_parity(kv_cache_fused, kv_cache_ref, written_slot_mapping, block_size) def test_compress_norm_rope_store_cpu_non_contiguous_kv_cache_row_stride(): """The main (output) kv_cache is a separate physical buffer from the state cache and can independently be a strided view.""" eps = 1e-6 compress_ratio, overlap = 128, False num_tokens = 5 block_size = 16 cache_pad = 32 ( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, cos_sin_cache, is_boundary, ) = _make_compress_inputs(num_tokens, compress_ratio, overlap) num_blocks = (num_tokens + block_size - 1) // block_size + 1 kv_cache_ref = _make_cache(num_blocks, block_size) block_bytes = kv_cache_ref.shape[1] padded_cache_ref = torch.zeros( num_blocks, block_bytes + cache_pad, dtype=torch.uint8 ) kv_cache_ref_view = padded_cache_ref[:, :block_bytes] kv_cache_ref_view.copy_(kv_cache_ref) padded_cache_fused = padded_cache_ref.clone() kv_cache_fused_view = padded_cache_fused[:, :block_bytes] _run_compress_eager( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, eps, cos_sin_cache, kv_cache_ref_view, block_size, compress_ratio, ) compress_norm_rope_store_cpu( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, eps, cos_sin_cache, kv_cache_fused_view, block_size, compress_ratio, ) written_slot_mapping = torch.where( is_boundary, kv_slot_mapping, torch.full_like(kv_slot_mapping, -1) ) _assert_cache_parity( kv_cache_fused_view, kv_cache_ref_view, written_slot_mapping, block_size ) def _run_compress_indexer_eager( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, rms_norm_eps, cos_sin_cache, kv_cache, kv_cache_block_size, compress_ratio, ): """Same softmax-pool + RMSNorm + GPT-J RoPE prologue as the head=512 path, but the indexer's cache quantizes all 128 post-RoPE dims as one FP8 block with a single raw fp32 scale per token (matching _indexer_k_quant_and_cache_cpu's layout), not 7 separate UE8M0 blocks.""" num_tokens, window = gather_slots.shape state_width = state_cache.shape[-1] // 2 state_block_size = state_cache.shape[1] w_idx = torch.arange(window) head_offset = torch.where(w_idx >= compress_ratio, INDEXER_HEAD_DIM, 0) for t in range(num_tokens): position = int(positions[t]) if (position + 1) % compress_ratio != 0: continue kv_slot = int(kv_slot_mapping[t]) if kv_slot < 0: continue slots = gather_slots[t] valid_w = slots >= 0 safe_slots = slots.clamp(min=0) block_idx = safe_slots // state_block_size pos_in_block = safe_slots % state_block_size rows = state_cache[block_idx, pos_in_block] # [window, 2*state_width] col = head_offset.unsqueeze(-1) + torch.arange(INDEXER_HEAD_DIM) kv_vals = torch.gather(rows, 1, col) score_col = state_width + col score_vals = torch.gather(rows, 1, score_col) score_vals = torch.where( valid_w.unsqueeze(-1), score_vals, torch.full_like(score_vals, -torch.inf) ) weights = torch.softmax(score_vals, dim=0) compressed = (kv_vals * weights).sum(dim=0) # [INDEXER_HEAD_DIM] variance = compressed.pow(2).mean() normed = compressed * torch.rsqrt(variance + rms_norm_eps) * rms_norm_weight compressed_pos = (position // compress_ratio) * compress_ratio cos = cos_sin_cache[compressed_pos, : ROPE_DIM // 2] sin = cos_sin_cache[compressed_pos, ROPE_DIM // 2 :] rotated_rope = _apply_gptj_rope(normed[INDEXER_NOPE_DIM:].clone(), cos, sin) full = torch.cat((normed[:INDEXER_NOPE_DIM], rotated_rope)) quant_input = full.to(torch.bfloat16).to(torch.float32) absmax = quant_input.abs().amax().clamp(min=1e-4) exponent = torch.ceil(torch.log2(absmax / FP8_MAX)) scale = torch.exp2(exponent) scaled = (quant_input / scale).clamp(-FP8_MAX, FP8_MAX) fp8_bytes = scaled.to(torch.float8_e4m3fn).view(torch.uint8) block_idx_kv = kv_slot // kv_cache_block_size pos_in_block_kv = kv_slot % kv_cache_block_size token_off = pos_in_block_kv * INDEXER_HEAD_DIM kv_cache[block_idx_kv, token_off : token_off + INDEXER_HEAD_DIM] = fp8_bytes scale_off = ( kv_cache_block_size * INDEXER_HEAD_DIM + pos_in_block_kv * INDEXER_SCALE_BYTES ) kv_cache[block_idx_kv, scale_off : scale_off + INDEXER_SCALE_BYTES] = ( scale.reshape(1).view(torch.uint8) ) @pytest.mark.parametrize("compress_ratio,overlap", [(128, False), (4, True)]) @pytest.mark.parametrize("num_tokens", [1, 17]) def test_compress_norm_rope_store_indexer_cpu_matches_eager( compress_ratio: int, overlap: bool, num_tokens: int ): eps = 1e-6 block_size = 16 ( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, cos_sin_cache, is_boundary, ) = _make_compress_inputs( num_tokens, compress_ratio, overlap, head_dim=INDEXER_HEAD_DIM ) num_blocks = (num_tokens + block_size - 1) // block_size + 1 kv_cache_ref = _make_indexer_cache(num_blocks, block_size) kv_cache_fused = kv_cache_ref.clone() _run_compress_indexer_eager( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, eps, cos_sin_cache, kv_cache_ref, block_size, compress_ratio, ) compress_norm_rope_store_indexer_cpu( state_cache, gather_slots, positions, kv_slot_mapping, rms_norm_weight, eps, cos_sin_cache, kv_cache_fused, block_size, compress_ratio, ) written_slot_mapping = torch.where( is_boundary, kv_slot_mapping, torch.full_like(kv_slot_mapping, -1) ) _assert_indexer_cache_parity( kv_cache_fused, kv_cache_ref, written_slot_mapping, block_size ) # --------------------------------------------------------------------------- # flash_mla_with_kvcache_cpu (flash_mla.cpp): fused sparse MQA decode attn # --------------------------------------------------------------------------- def _pack_flash_mla_cache(kv_f32: torch.Tensor, block_size: int): """Quantize kv_f32 ([N, HEAD_DIM]) into the fp8_ds_mla byte layout and return (cache, dequant_ref) -- the exact post-quantization value the kernel's dequant should reproduce. Slot id == row index, packed contiguously from block 0.""" n = kv_f32.shape[0] num_blocks = max(1, -(-n // block_size)) cache = _make_cache(num_blocks, block_size) if n == 0: return cache, kv_f32.new_zeros(0, HEAD_DIM) num_quant_blocks = NOPE_DIM // QUANT_BLOCK nope = kv_f32[:, :NOPE_DIM].reshape(n, num_quant_blocks, QUANT_BLOCK) absmax = nope.abs().amax(dim=-1).clamp(min=1e-4) exponent = torch.ceil(torch.log2(absmax / FP8_MAX)) inv_scale = torch.exp2(-exponent) scaled = (nope * inv_scale.unsqueeze(-1)).clamp(-FP8_MAX, FP8_MAX) nope_fp8 = scaled.to(torch.float8_e4m3fn) scale = torch.exp2(exponent) nope_dequant = (nope_fp8.float() * scale.unsqueeze(-1)).reshape(n, NOPE_DIM) rope_bf16 = kv_f32[:, NOPE_DIM:].to(torch.bfloat16) dequant_ref = torch.cat([nope_dequant, rope_bf16.float()], dim=-1) slot_ids = torch.arange(n) block_idx = slot_ids // block_size pos_in_block = slot_ids % block_size nope_fp8_bytes = nope_fp8.reshape(n, NOPE_DIM).contiguous().view(torch.uint8) rope_bf16_bytes = rope_bf16.contiguous().view(torch.uint8) scale_bytes = (exponent + 127.0).clamp(0, 255).to(torch.uint8) arange_nope = torch.arange(NOPE_DIM) arange_rope = torch.arange(ROPE_DIM * 2) arange_scale = torch.arange(num_quant_blocks) token_base = pos_in_block.unsqueeze(-1) * TOKEN_DATA_BYTES row_idx = block_idx.unsqueeze(-1) cache[row_idx, token_base + arange_nope] = nope_fp8_bytes cache[row_idx, token_base + NOPE_DIM + arange_rope] = rope_bf16_bytes scale_base = ( block_size * TOKEN_DATA_BYTES + pos_in_block.unsqueeze(-1) * SCALE_BYTES_PER_TOKEN ) cache[row_idx, scale_base + arange_scale] = scale_bytes cache[block_idx, scale_base.squeeze(-1) + num_quant_blocks] = 0 return cache, dequant_ref def _flash_mla_eager( q: torch.Tensor, window_dequant: torch.Tensor, window_slots: torch.Tensor, compressed_dequant: torch.Tensor, compressed_slots: torch.Tensor, attn_sink: torch.Tensor, scale: float, ) -> torch.Tensor: """MQA attention: gather the (already-dequantized) window+compressed KV rows referenced by *_slots (-1 = invalid/masked), append a learned per-head sink logit with zero value contribution, softmax, weighted sum.""" num_tokens, num_heads, _ = q.shape q_f32 = q.float() def gather(dequant, slots): valid = slots >= 0 safe = slots.clamp(min=0) kv = dequant[safe] # [T, K, D] return kv, valid win_kv, win_valid = gather(window_dequant, window_slots) if compressed_slots.shape[1] > 0: comp_kv, comp_valid = gather(compressed_dequant, compressed_slots) kv = torch.cat([win_kv, comp_kv], dim=1) valid = torch.cat([win_valid, comp_valid], dim=1) else: kv = win_kv valid = win_valid scores = torch.einsum("thd,tkd->thk", q_f32, kv) * scale scores = scores.masked_fill(~valid.unsqueeze(1), float("-inf")) sink = attn_sink.view(1, num_heads, 1).expand(num_tokens, num_heads, 1) scores_full = torch.cat([scores, sink], dim=-1) probs = torch.softmax(scores_full, dim=-1) out = torch.einsum("thk,tkd->thd", probs[..., :-1], kv) return out.to(q.dtype) @pytest.mark.parametrize("num_heads", [1, 64]) @pytest.mark.parametrize("num_tokens", [1, 17]) @pytest.mark.parametrize("swa_only", [True, False]) def test_flash_mla_cpu_kernel_matches_eager( num_heads: int, num_tokens: int, swa_only: bool ): torch.manual_seed(0) dtype = torch.bfloat16 window_block_size = 16 compressed_block_size = 8 num_window = 6 num_compressed = 0 if swa_only else 5 q = torch.randn(num_tokens, num_heads, HEAD_DIM, dtype=dtype) scale = 1.0 / (HEAD_DIM**0.5) attn_sink = torch.randn(num_heads, dtype=torch.float32) num_window_slots = num_tokens * num_window + window_block_size window_kv_f32 = torch.randn(num_window_slots, HEAD_DIM) window_cache, window_dequant = _pack_flash_mla_cache( window_kv_f32, window_block_size ) window_slots = torch.randint(0, num_window_slots, (num_tokens, num_window)) window_slots[:, 0] = -1 # exercise invalid-slot masking if num_compressed > 0: num_compressed_slots = num_tokens * num_compressed + compressed_block_size compressed_kv_f32 = torch.randn(num_compressed_slots, HEAD_DIM) compressed_cache, compressed_dequant = _pack_flash_mla_cache( compressed_kv_f32, compressed_block_size ) compressed_slots = torch.randint( 0, num_compressed_slots, (num_tokens, num_compressed) ) if num_compressed > 1: compressed_slots[:, -1] = -1 else: compressed_cache = window_cache compressed_dequant = window_dequant.new_zeros(0, HEAD_DIM) compressed_slots = window_slots.new_full((num_tokens, 0), -1) out_ref = _flash_mla_eager( q, window_dequant, window_slots, compressed_dequant, compressed_slots, attn_sink, scale, ) out_kernel = q.new_empty(num_tokens, num_heads, HEAD_DIM) flash_mla_with_kvcache_cpu( out_kernel, q, window_cache, window_slots, window_block_size, compressed_cache, compressed_slots, compressed_block_size, attn_sink, scale, ) torch.testing.assert_close( out_kernel.float(), out_ref.float(), rtol=2e-2, atol=2e-2 ) def test_flash_mla_cpu_kernel_all_invalid_falls_back_to_sink(): """When every KV slot is masked out, softmax degenerates to the sink logit alone and the output must be all-zero (no valid value rows).""" torch.manual_seed(2) dtype = torch.bfloat16 num_tokens, num_heads, num_window = 3, 4, 5 window_block_size = 16 q = torch.randn(num_tokens, num_heads, HEAD_DIM, dtype=dtype) scale = 1.0 / (HEAD_DIM**0.5) attn_sink = torch.randn(num_heads, dtype=torch.float32) window_kv_f32 = torch.randn(window_block_size, HEAD_DIM) window_cache, _ = _pack_flash_mla_cache(window_kv_f32, window_block_size) window_slots = torch.full((num_tokens, num_window), -1, dtype=torch.int64) compressed_slots = window_slots.new_full((num_tokens, 0), -1) out = q.new_empty(num_tokens, num_heads, HEAD_DIM) flash_mla_with_kvcache_cpu( out, q, window_cache, window_slots, window_block_size, window_cache, compressed_slots, 1, attn_sink, scale, ) assert out.abs().max().item() == 0.0 # --------------------------------------------------------------------------- # fused_qnorm_rope_kv_insert_cpu (store_cache.cpp) # --------------------------------------------------------------------------- def _qnorm_kv_insert_eager( q, kv, positions, cache, slot_mapping, cos_sin_cache, q_head_padded, eps, bs ): """Reference: per-head weight-free RMSNorm + GPT-J RoPE on Q; GPT-J RoPE + UE8M0 FP8 quant + paged insert on KV.""" num_quant_blocks = NOPE_DIM // QUANT_BLOCK num_tokens_full, num_heads_q, _ = q.shape out_dtype = q.dtype cos = cos_sin_cache[positions, : ROPE_DIM // 2] sin = cos_sin_cache[positions, ROPE_DIM // 2 :] q_f32 = q.float() var = q_f32.square().mean(dim=-1, keepdim=True) q_f32 = q_f32 * torch.rsqrt(var + eps) q_f32[..., NOPE_DIM:] = _apply_gptj_rope( q_f32[..., NOPE_DIM:], cos.unsqueeze(1), sin.unsqueeze(1) ) q_out = q.new_zeros(num_tokens_full, q_head_padded, HEAD_DIM) q_out[:, :num_heads_q, :] = q_f32.to(out_dtype) num_tokens_insert = slot_mapping.shape[0] kv_f32 = kv[:num_tokens_insert].float() kv_f32[..., NOPE_DIM:] = _apply_gptj_rope( kv_f32[..., NOPE_DIM:], cos[:num_tokens_insert], sin[:num_tokens_insert] ) kv_bf16 = kv_f32.to(torch.bfloat16) kv_f32 = kv_bf16.float() valid = slot_mapping >= 0 if bool(valid.any()): idx = valid.nonzero(as_tuple=True)[0] slot_id = slot_mapping[idx] block_idx = torch.div(slot_id, bs, rounding_mode="floor") pos_in_block = slot_id % bs nope = kv_f32[idx, :NOPE_DIM].view(-1, num_quant_blocks, QUANT_BLOCK) absmax = nope.abs().amax(dim=-1).clamp(min=1e-4) exponent = torch.ceil(torch.log2(absmax / FP8_MAX)) inv_scale = torch.exp2(-exponent) scaled = (nope * inv_scale.unsqueeze(-1)).clamp(-FP8_MAX, FP8_MAX) # Flush fp8-subnormal magnitudes to zero, matching the kernel's # AVX512 cvt_fp32x16_to_fp8x16, which doesn't preserve exact # subnormal encodings the way .to(torch.float8_e4m3fn) does. fp8_min_normal = 2.0**-6 scaled = torch.where(scaled.abs() < fp8_min_normal, 0.0, scaled) nope_fp8_bytes = ( scaled.to(torch.float8_e4m3fn).view(-1, NOPE_DIM).view(torch.uint8) ) scale_bytes = (exponent + 127.0).clamp(0, 255).to(torch.uint8) rope_bf16_bytes = kv_bf16[idx, NOPE_DIM:].contiguous().view(torch.uint8) arange_nope = torch.arange(NOPE_DIM) arange_rope = torch.arange(ROPE_DIM * 2) arange_scale = torch.arange(num_quant_blocks) row_idx = block_idx.unsqueeze(-1) token_base = pos_in_block.unsqueeze(-1) * TOKEN_DATA_BYTES cache[row_idx, token_base + arange_nope] = nope_fp8_bytes cache[row_idx, token_base + NOPE_DIM + arange_rope] = rope_bf16_bytes scale_base = ( bs * TOKEN_DATA_BYTES + pos_in_block.unsqueeze(-1) * SCALE_BYTES_PER_TOKEN ) cache[row_idx, scale_base + arange_scale] = scale_bytes cache[block_idx, scale_base.squeeze(-1) + num_quant_blocks] = 0 return q_out @pytest.mark.parametrize("num_tokens", [1, 64]) @pytest.mark.parametrize("n_heads,padded_heads", [(1, 8), (8, 8), (8, 16), (64, 128)]) def test_qnorm_rope_kv_insert_cpu_kernel_matches_eager( num_tokens: int, n_heads: int, padded_heads: int ): torch.manual_seed(0) dtype = torch.bfloat16 eps = 1e-6 max_pos = 4096 block_size = 16 q = torch.randn(num_tokens, n_heads, HEAD_DIM, dtype=dtype) kv = torch.randn(num_tokens, HEAD_DIM, dtype=dtype) positions = torch.randint(0, max_pos, (num_tokens,), dtype=torch.int64) cos_sin_cache = make_cos_sin_cache(max_pos, ROPE_DIM, torch.float32, "cpu") num_blocks = (num_tokens + block_size - 1) // block_size + 1 slot_mapping = torch.arange(num_tokens, dtype=torch.int64) if num_tokens > 1: slot_mapping[-1] = -1 # exercise the "skip invalid slot" branch cache_ref = _make_cache(num_blocks, block_size) cache_fused = cache_ref.clone() q_ref = _qnorm_kv_insert_eager( q, kv, positions, cache_ref, slot_mapping, cos_sin_cache, padded_heads, eps, block_size, ) q_fused = fused_qnorm_rope_kv_insert_cpu( q, kv, positions, cache_fused, slot_mapping, cos_sin_cache, padded_heads, eps, block_size, ) torch.testing.assert_close(q_fused.float(), q_ref.float(), rtol=1e-2, atol=1e-2) if n_heads < padded_heads: pad_region = q_fused[:, n_heads:padded_heads] assert pad_region.abs().max().item() == 0.0, ( "padded head slots must be exact zero" ) _assert_cache_parity(cache_fused, cache_ref, slot_mapping, block_size) @pytest.mark.parametrize("pad", [1, 5]) def test_qnorm_rope_kv_insert_cpu_kernel_dp_padding(pad: int): """slot_mapping shorter than q/kv rows: the KV branch must only touch the first num_tokens rows while Q-norm+RoPE still runs on all rows.""" torch.manual_seed(1) dtype = torch.bfloat16 eps = 1e-6 max_pos = 4096 block_size = 16 num_tokens = 17 total = num_tokens + pad padded_heads = 8 q = torch.randn(total, padded_heads, HEAD_DIM, dtype=dtype) kv = torch.randn(total, HEAD_DIM, dtype=dtype) positions = torch.randint(0, max_pos, (total,), dtype=torch.int64) cos_sin_cache = make_cos_sin_cache(max_pos, ROPE_DIM, torch.float32, "cpu") num_blocks = (num_tokens + block_size - 1) // block_size + 1 slot_mapping = torch.arange(num_tokens, dtype=torch.int64) cache_ref = _make_cache(num_blocks, block_size) cache_fused = cache_ref.clone() q_ref = _qnorm_kv_insert_eager( q, kv, positions, cache_ref, slot_mapping, cos_sin_cache, padded_heads, eps, block_size, ) q_fused = fused_qnorm_rope_kv_insert_cpu( q, kv, positions, cache_fused, slot_mapping, cos_sin_cache, padded_heads, eps, block_size, ) torch.testing.assert_close(q_fused.float(), q_ref.float(), rtol=1e-2, atol=1e-2) _assert_cache_parity(cache_fused, cache_ref, slot_mapping, block_size) # --------------------------------------------------------------------------- # inverse_gptj_rope_o_proj_cpu (store_cache.cpp): DeepseekV4CPUAttention's # _o_proj de-rotation kernel # --------------------------------------------------------------------------- def _inverse_gptj_rope_o_proj_eager( o: torch.Tensor, positions: torch.Tensor, cos_sin_cache: torch.Tensor, rope_dim: int, ) -> torch.Tensor: """NoPE dims pass through as fp32; the RoPE segment is de-rotated by R(-pos) in GPT-J (interleaved even/odd) convention.""" nope_dim = o.shape[-1] - rope_dim half_dim = rope_dim // 2 o_f32 = o.float() out = torch.empty_like(o_f32) out[..., :nope_dim] = o_f32[..., :nope_dim] cos = cos_sin_cache[positions, :half_dim].unsqueeze(1) sin = cos_sin_cache[positions, half_dim:].unsqueeze(1) e = o_f32[..., nope_dim::2] od = o_f32[..., nope_dim + 1 :: 2] out[..., nope_dim::2] = e * cos + od * sin out[..., nope_dim + 1 :: 2] = od * cos - e * sin return out @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16]) @pytest.mark.parametrize("num_tokens", [1, 17]) @pytest.mark.parametrize( "head_dim,rope_dim,num_heads", [ (512, 64, 8), # DeepseekV4CPUAttention._o_proj's actual shape (64, 64, 1), # nope_dim=0: no NoPE region at all ], ) def test_inverse_gptj_rope_o_proj_cpu_kernel_matches_eager( dtype: torch.dtype, num_tokens: int, head_dim: int, rope_dim: int, num_heads: int, ): torch.manual_seed(0) max_pos = 4096 o = torch.randn(num_tokens, num_heads, head_dim, dtype=dtype) positions = torch.randint(0, max_pos, (num_tokens,), dtype=torch.int64) cos_sin_cache = make_cos_sin_cache(max_pos, rope_dim, torch.float32, "cpu") out_ref = _inverse_gptj_rope_o_proj_eager(o, positions, cos_sin_cache, rope_dim) out_fused = inverse_gptj_rope_o_proj_cpu(o, positions, cos_sin_cache, rope_dim) torch.testing.assert_close(out_fused, out_ref, rtol=1e-2, atol=1e-2) # --------------------------------------------------------------------------- # fp8_paged_mqa_logits_cpu (paged_mqa_logits.cpp): sparse indexer's # decode-path MQA-logits kernel # --------------------------------------------------------------------------- def _rand_fp8(*shape: int) -> torch.Tensor: return torch.randn(shape, dtype=torch.float32).clamp(-3, 3).to(torch.float8_e4m3fn) def _pages_per_request(block_size: int, seq_lens: list[int]) -> list[int]: return [-(-seq_len // block_size) for seq_len in seq_lens] def _write_page( page_row: torch.Tensor, block_size: int, k_fp8: torch.Tensor, scale: torch.Tensor ) -> None: """Write k_fp8 ([block_size, INDEXER_HEAD_DIM]) and scale ([block_size] fp32) into one page row: K-region then scale-region, matching compress_norm_rope_store_indexer_cpu's layout.""" k_region = page_row[: block_size * INDEXER_HEAD_DIM] k_region.copy_(k_fp8.view(torch.uint8).flatten()) scale_region = page_row[block_size * INDEXER_HEAD_DIM :].view(torch.float32) scale_region[:].copy_(scale) def _reference_paged_mqa_logits( q_fp8: torch.Tensor, weight: torch.Tensor, ref_k: list[torch.Tensor], ref_scale: list[torch.Tensor], max_seq_len: int, ) -> torch.Tensor: batch_size, _num_heads, _ = q_fp8.shape out = torch.zeros((batch_size, max_seq_len), dtype=torch.float32) for b in range(batch_size): k = ref_k[b].float() # [seq_len, head_dim] scale = ref_scale[b] # [seq_len] q = q_fp8[b].float() # [heads, head_dim] w = weight[b] # [heads] dot = torch.einsum("jd,hd->jh", k, q).clamp(min=0.0) score = (dot * w.unsqueeze(0)).sum(dim=1) out[b, : k.shape[0]] = score * scale return out def _build_paged_mqa_logits_batch( block_size: int, seq_lens: list[int], num_heads: int, pages: torch.Tensor, seed: int, ): """Fill pages (a pre-sized [n_pages, buf_width] uint8 tensor, possibly non-contiguous in dim 0) with deterministic-per-seed random K/scale data.""" torch.manual_seed(seed) batch_size = len(seq_lens) max_seq_len = max(seq_lens) per_request = _pages_per_request(block_size, seq_lens) pages_per_batch = max(per_request) page_table = torch.full((batch_size, pages_per_batch), -1, dtype=torch.int32) ref_k: list[torch.Tensor] = [] ref_scale: list[torch.Tensor] = [] next_page = 0 for b, seq_len in enumerate(seq_lens): k_chunks = [] scale_chunks = [] for p in range(per_request[b]): page_table[b, p] = next_page k = _rand_fp8(block_size, INDEXER_HEAD_DIM) scale = torch.rand(block_size, dtype=torch.float32) * 0.05 + 0.01 _write_page(pages[next_page], block_size, k, scale) tokens_here = min(block_size, seq_len - p * block_size) k_chunks.append(k[:tokens_here]) scale_chunks.append(scale[:tokens_here]) next_page += 1 ref_k.append(torch.cat(k_chunks, dim=0)) ref_scale.append(torch.cat(scale_chunks, dim=0)) q_fp8 = _rand_fp8(batch_size, num_heads, INDEXER_HEAD_DIM) weight = torch.rand(batch_size, num_heads, dtype=torch.float32) * 0.1 seq_lens_t = torch.tensor(seq_lens, dtype=torch.int32) return q_fp8, weight, seq_lens_t, page_table, ref_k, ref_scale, max_seq_len def _paged_mqa_logits_page_layout( block_size: int, seq_lens: list[int] ) -> tuple[int, int]: n_pages = sum(_pages_per_request(block_size, seq_lens)) buf_width = block_size * INDEXER_HEAD_DIM + block_size * INDEXER_SCALE_BYTES return n_pages, buf_width @pytest.mark.parametrize("block_size", [64, 256]) def test_fp8_paged_mqa_logits_cpu_matches_eager(block_size: int): num_heads = 4 seq_lens = [1, block_size + 7, 3 * block_size] n_pages, buf_width = _paged_mqa_logits_page_layout(block_size, seq_lens) pages = torch.zeros((n_pages, buf_width), dtype=torch.uint8) q_fp8, weight, seq_lens_t, page_table, ref_k, ref_scale, max_seq_len = ( _build_paged_mqa_logits_batch(block_size, seq_lens, num_heads, pages, seed=0) ) logits = fp8_paged_mqa_logits_cpu( q_fp8, pages, weight, seq_lens_t, page_table, block_size, max_seq_len ) ref = _reference_paged_mqa_logits(q_fp8, weight, ref_k, ref_scale, max_seq_len) for b, seq_len in enumerate(seq_lens): torch.testing.assert_close( logits[b, :seq_len], ref[b, :seq_len], atol=1e-2, rtol=1e-2 ) def test_fp8_paged_mqa_logits_cpu_non_contiguous_kv_cache_row_stride(): """The paged K-cache in production is a per-layer view into a shared multi-layer allocation, so its page stride generally exceeds the page byte width -- check the kernel reads a padded/strided buffer identically to an equivalent tightly-packed one.""" block_size = 64 num_heads = 4 seq_lens = [1, block_size + 7, 3 * block_size] n_pages, buf_width = _paged_mqa_logits_page_layout(block_size, seq_lens) cache_pad = 48 tight_pages = torch.zeros((n_pages, buf_width), dtype=torch.uint8) q_fp8, weight, seq_lens_t, page_table, _, _, max_seq_len = ( _build_paged_mqa_logits_batch( block_size, seq_lens, num_heads, tight_pages, seed=0 ) ) padded = torch.zeros((n_pages, buf_width + cache_pad), dtype=torch.uint8) strided_pages = padded[:, :buf_width] _build_paged_mqa_logits_batch( block_size, seq_lens, num_heads, strided_pages, seed=0 ) assert strided_pages.stride(0) != strided_pages.size(1) logits_tight = fp8_paged_mqa_logits_cpu( q_fp8, tight_pages, weight, seq_lens_t, page_table, block_size, max_seq_len ) logits_strided = fp8_paged_mqa_logits_cpu( q_fp8, strided_pages, weight, seq_lens_t, page_table, block_size, max_seq_len ) # Only [:seq_len] is ever written (and ever read downstream by # topk_transform_512_cpu) -- positions beyond that are uninitialized, so # comparing the full row would be comparing unrelated garbage. for b, seq_len in enumerate(seq_lens): torch.testing.assert_close( logits_tight[b, :seq_len], logits_strided[b, :seq_len] ) # --------------------------------------------------------------------------- # fused_indexer_q_rope_quant_cpu (indexer.cpp): indexer Q-side RoPE + FP8 # quant, and the dispatcher wiring that routes to it on CPU # --------------------------------------------------------------------------- # # Independent eager reference: this kernel's absmax reduction is over an # unusual mix (raw NoPE values plus a bf16-round-tripped RoPE half), which # per_token_group_quant_fp8 does not replicate. Allow <=1 fp8 ULP: the RoPE # halves round a value sitting exactly on a tie to either side. INDEXER_Q_N_HEAD = 64 INDEXER_Q_MAX_POS = 4096 def _indexer_q_rope_quant_reference( positions: torch.Tensor, q: torch.Tensor, cos_sin_cache: torch.Tensor, weights: torch.Tensor, softmax_scale: float, head_scale: float, ) -> tuple[torch.Tensor, torch.Tensor]: _num_tokens, _num_heads, head_dim = q.shape half_rot_dim = cos_sin_cache.shape[-1] // 2 rot_dim = 2 * half_rot_dim nope_dim = head_dim - rot_dim q_f32 = q.float() cos_sin_f32 = cos_sin_cache.float() pos_cos_sin = cos_sin_f32[positions] # [T, rot_dim] cos = pos_cos_sin[:, :half_rot_dim].unsqueeze(1) sin = pos_cos_sin[:, half_rot_dim:].unsqueeze(1) nope = q_f32[..., :nope_dim] rot = q_f32[..., nope_dim:] x_even = rot[..., 0::2] x_odd = rot[..., 1::2] r_even = (x_even * cos - x_odd * sin).to(torch.bfloat16).to(torch.float32) r_odd = (x_odd * cos + x_even * sin).to(torch.bfloat16).to(torch.float32) rope_out = torch.stack((r_even, r_odd), dim=-1).flatten(-2) combined = torch.cat((nope, rope_out), dim=-1) # [T, H, head_dim] amax = combined.abs().amax(dim=-1).clamp(min=1e-4) # [T, H] exponent = torch.ceil(torch.log2(amax / FP8_MAX)) scale = torch.exp2(exponent) inv_scale = scale.reciprocal().unsqueeze(-1) q_fp8 = (combined * inv_scale).clamp(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn) weights_out = weights.to(torch.float32) * scale * softmax_scale * head_scale return q_fp8, weights_out @pytest.mark.parametrize("num_tokens", [1, 257]) def test_fused_indexer_q_rope_quant_cpu_matches_eager(num_tokens: int): torch.manual_seed(0) q = torch.randn(num_tokens, INDEXER_Q_N_HEAD, INDEXER_HEAD_DIM, dtype=torch.float32) positions = torch.randint(0, INDEXER_Q_MAX_POS, (num_tokens,), dtype=torch.int64) cos_sin_cache = torch.randn(INDEXER_Q_MAX_POS, ROPE_DIM, dtype=torch.float32) weights = torch.randn(num_tokens, INDEXER_Q_N_HEAD, dtype=torch.float32) softmax_scale = INDEXER_HEAD_DIM**-0.5 head_scale = INDEXER_Q_N_HEAD**-0.5 q_fp8_ref, weights_ref = _indexer_q_rope_quant_reference( positions, q, cos_sin_cache, weights, softmax_scale, head_scale ) q_fp8_fused = torch.empty_like(q, dtype=torch.float8_e4m3fn) weights_fused = torch.empty_like(weights, dtype=torch.float32) fused_indexer_q_rope_quant_cpu( positions, q, cos_sin_cache, q_fp8_fused, weights, softmax_scale, head_scale, weights_fused, ) max_ulp = int(fp8_ulp_distance(q_fp8_fused, q_fp8_ref).max().item()) assert max_ulp <= 1, f"q_fp8 differs by {max_ulp} ULP (>1)" torch.testing.assert_close(weights_fused, weights_ref, rtol=1e-5, atol=1e-6) def test_fused_indexer_q_rope_quant_dispatches_to_cpu_kernel(): """End-to-end wiring check: the production dispatcher must route to the CPU op on a CPU tensor, not silently fall through to the Triton kernel (which would also "work" under triton-cpu, masking a dispatch regression). The dispatcher does its CPU import inside the CPU branch, so the patch target is the source module.""" torch.manual_seed(0) num_tokens = 1 q = torch.randn(num_tokens, INDEXER_Q_N_HEAD, INDEXER_HEAD_DIM, dtype=torch.float32) positions = torch.randint(0, INDEXER_Q_MAX_POS, (num_tokens,), dtype=torch.int64) cos_sin_cache = torch.randn(INDEXER_Q_MAX_POS, ROPE_DIM, dtype=torch.float32) weights = torch.randn(num_tokens, INDEXER_Q_N_HEAD, dtype=torch.float32) softmax_scale = INDEXER_HEAD_DIM**-0.5 head_scale = INDEXER_Q_N_HEAD**-0.5 q_fp8_ref, weights_ref = _indexer_q_rope_quant_reference( positions, q, cos_sin_cache, weights, softmax_scale, head_scale ) with mock.patch("vllm._custom_ops.fused_indexer_q_rope_quant_cpu") as mocked: fused_indexer_q_rope_quant( positions, q.clone(), cos_sin_cache, weights, softmax_scale, head_scale, use_fp4=False, ) assert mocked.called, ( "fused_indexer_q_rope_quant did not dispatch to the CPU kernel " "on a CPU tensor" ) q_fp8_fused, weights_fused = fused_indexer_q_rope_quant( positions, q.clone(), cos_sin_cache, weights, softmax_scale, head_scale, use_fp4=False, ) max_ulp = int(fp8_ulp_distance(q_fp8_fused, q_fp8_ref).max().item()) assert max_ulp <= 1, f"q_fp8 differs by {max_ulp} ULP (>1)" torch.testing.assert_close(weights_fused, weights_ref, rtol=1e-5, atol=1e-6) # --------------------------------------------------------------------------- # mhc_pre_cpu / mhc_post_cpu / hc_head_fused_cpu (mhc.cpp): CPU ports of the # mHC gating kernels # --------------------------------------------------------------------------- def _rmsnorm_nw_torch(x: torch.Tensor, eps: float) -> torch.Tensor: """Weight-free RMSNorm over the last dim, matching hc_head_fused_cpu.""" x_f32 = x.float() var = x_f32.square().mean(dim=-1, keepdim=True) return (x_f32 * torch.rsqrt(var + eps)).to(x.dtype) def _hc_head_torch( hidden_states: torch.Tensor, hc_fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, rms_norm_eps: float, hc_eps: float, ) -> torch.Tensor: """Eager reference for hc_head_fused_cpu: round-trips the RMSNorm output through bf16 before the linear, matching the kernel's bf16 arithmetic.""" hc_mult, hidden_size = hidden_states.shape[-2:] outer_shape = hidden_states.shape[:-2] hs_flat = hidden_states.reshape(-1, hc_mult, hidden_size) x_flat = hs_flat.flatten(-2) x_normed = _rmsnorm_nw_torch(x_flat, rms_norm_eps) mixes = torch.nn.functional.linear(x_normed.float(), hc_fn) pre = torch.sigmoid(mixes * hc_scale + hc_base) + hc_eps out = torch.sum(pre.unsqueeze(-1) * hs_flat.float(), dim=1).to(hidden_states.dtype) return out.view(*outer_shape, hidden_size) @pytest.mark.parametrize("num_tokens", [1, 128]) @pytest.mark.parametrize("hidden_size", [4096, 7168]) def test_mhc_pre_cpu(num_tokens: int, hidden_size: int): """mhc_pre_cpu vs. the mhc_pre_torch eager reference it replaces.""" torch.set_default_device(DEVICE) set_random_seed(0) hc_mult = 4 hc_mult3 = 2 * hc_mult + hc_mult * hc_mult residual = torch.randn((num_tokens, hc_mult, hidden_size), dtype=torch.bfloat16) fn = torch.randn((hc_mult3, hc_mult * hidden_size), dtype=torch.float32) * 1e-4 hc_scale = torch.randn((3,), dtype=torch.float32) * 0.1 hc_base = torch.randn((hc_mult3,), dtype=torch.float32) * 0.1 rms_eps = hc_eps = 1e-6 hc_post_mult_value = 2.0 sinkhorn_repeat = 8 ref = mhc_kernels.mhc_pre_torch( residual, fn, hc_scale, hc_base, rms_eps, hc_eps, hc_eps, hc_post_mult_value, sinkhorn_repeat, ) out = mhc_kernels.mhc_pre_cpu( residual, fn, hc_scale, hc_base, rms_eps, hc_eps, hc_eps, hc_post_mult_value, sinkhorn_repeat, ) for actual, expected in zip(out, ref, strict=True): torch.testing.assert_close(actual, expected, atol=5e-2, rtol=1e-2) @pytest.mark.parametrize("num_tokens", [1, 128]) @pytest.mark.parametrize("hidden_size", [4096, 7168]) def test_mhc_post_cpu(num_tokens: int, hidden_size: int): """mhc_post_cpu vs. the mhc_post_torch eager reference it replaces.""" torch.set_default_device(DEVICE) set_random_seed(0) hc_mult = 4 x = torch.randn((num_tokens, hidden_size), dtype=torch.bfloat16) residual = torch.randn((num_tokens, hc_mult, hidden_size), dtype=torch.bfloat16) post_layer_mix = torch.randn((num_tokens, hc_mult, 1), dtype=torch.float32) comb_res_mix = torch.randn((num_tokens, hc_mult, hc_mult), dtype=torch.float32) ref = mhc_kernels.mhc_post_torch(x, residual, post_layer_mix, comb_res_mix) out = mhc_kernels.mhc_post_cpu(x, residual, post_layer_mix, comb_res_mix) torch.testing.assert_close(out, ref, atol=5e-2, rtol=1e-2) @pytest.mark.parametrize("num_tokens", [1, 128]) @pytest.mark.parametrize("hidden_size", [4096, 7168]) def test_hc_head_cpu(num_tokens: int, hidden_size: int): """hc_head_fused_cpu vs. the _hc_head_torch eager reference it replaces.""" torch.set_default_device(DEVICE) set_random_seed(0) hc_mult = 4 residual = torch.randn((num_tokens, hc_mult, hidden_size), dtype=torch.bfloat16) fn = torch.randn((hc_mult, hc_mult * hidden_size), dtype=torch.float32) * 1e-4 hc_scale = torch.randn((1,), dtype=torch.float32) * 0.1 hc_base = torch.randn((hc_mult,), dtype=torch.float32) * 0.1 rms_eps = hc_eps = 1e-6 ref = _hc_head_torch(residual, fn, hc_scale, hc_base, rms_eps, hc_eps) out = mhc_kernels.hc_head_fused_cpu( residual, fn, hc_scale, hc_base, rms_eps, hc_eps ) torch.testing.assert_close(out, ref, atol=5e-2, rtol=1e-2)