mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-31 00:36:20 +08:00
Two closed-loop checks, both clean, which finally retires the two standing suspects.
P4.3 KV cache. The repo's own store/load tests pass for the fp32-scale index layout SM90 uses
(6 each). Running every layout gives 48 passed / 12 failed, and every failure is a ue8m0 index
layout -- `FP4_INDEX_KV_FORMAT` or `MXFP8_INDEX_KV_FORMAT`, neither of which SM90 selects. Those
failures are in the test's reference, not the kernel: it reinterprets ue8m0 bytes as
`PLATFORM.fp8_sf_type`, which is float32 here, so `index_copy_` hits a dtype mismatch.
`scripts/h20_kv_roundtrip.py` then closes the loop without any reference at all -- write known
BF16 through the paged cache, read it back, compare. Both SM90 layouts, block 32 and 64, 1/233/
1029 tokens:
V41_FORMAT (main/swa KV) rel 0.0266-0.0295 cos 0.9996
FP8_FLOAT32_SF_INDEX_KV_FORMAT (132B) rel 0.0266-0.0327 cos 0.9996
That is the fp8 floor, the same order as the 2.7e-2 on weights, so the cache is not where the
decode error comes from and the 132-byte layout question from P3.9 is closed.
One trap: the first version of the script compared raw fp8 values against BF16 and reported
rel=126. Index layouts are deliberately *not* dequantized by `load_paged_kv_cache` -- the
consumer is a kernel that wants bytes plus scales. Multiplying by the scale gives 0.027. The
127x was the test, not the implementation.
P4.4 the decode indexer. `_decode_p_sum_sm90` reimplements DeepGEMM's paged MQA logits in torch
because Hopper's kernel requires 64-token pages; nothing else exercised it. Compared against a
float64 reference that shares no code -- explicit per-token offsets instead of strided views --
it agrees to rel~1e-7 / cos 1.0 across page sizes 32 and 64, boundary-crossing lengths,
multi-page contexts and 32 heads. The `q_sf[..., :1]` dequantization is also sound: head_dim is
128, equal to the granularity, so one scale per (row, head) is correct.
What this changes. `logits_prompt` from the max_tokens=1 run puts the prefill error at cos
0.9846 with an absolute error of 3.30 on the winning token, against a reference margin of 3.112:
the margin is thin because the error is the same size as the margin, not by coincidence. The
reference's own prefill-to-decode transition also costs about 2x margin (3.112 -> 1.638) purely
from the quantized KV round trip. Our 0.256 halved would still be positive, yet it flips, so
roughly 0.5-1.0 of logit-level error in decode remains unexplained -- now narrowed to attention
over the cached main KV and the SWA-window / compressed-position mapping.
Also corrects an earlier overstatement: "the difference is introduced by decode" does not mean
decode has a discrete bug, and P4.3/P4.4 show its two main suspects do not.
One more dump trap worth recording: with max_tokens > 1, `logits_prompt` is contaminated too.
mysteps6 (max_tokens=7) gives cos 0.68 and a 16.5 top-1 error for the same input where mylogits3
(max_tokens=1) gives 0.9846 and 3.30. Prefill logits are only trustworthy from a
max_tokens=1 run.
128 lines
5.9 KiB
Python
128 lines
5.9 KiB
Python
"""Numerically test the SM90 decode-time indexer score against a reference formula.
|
|
|
|
`_decode_p_sum_sm90` reimplements DeepGEMM's `fp8_fp4_paged_mqa_logits` in PyTorch, because
|
|
Hopper's paged kernel hard-requires 64-token pages while the index cache uses a different page
|
|
size. Nothing else exercises it: prefill goes through the real kernel, so a defect here would be
|
|
decode-only -- which is exactly where the remaining drift lives.
|
|
|
|
The reference below deliberately shares no code with the implementation: it reads the cache with
|
|
explicit per-token slicing in float64 rather than strided views, so an offset or stride mistake
|
|
in one does not cancel against the other.
|
|
|
|
python scripts/h20_index_decode_check.py
|
|
"""
|
|
|
|
import torch
|
|
|
|
from deepinfer.model_executor.layers.tilelang_ops.cache_common import FP8_FLOAT32_SF_INDEX_KV_FORMAT
|
|
from deepinfer.model_executor.models.v4_modules.index_attn import MQA
|
|
|
|
DEV = "cuda"
|
|
HEAD_DIM = 128
|
|
|
|
|
|
def build_cache(num_blocks: int, page_tokens: int, scales: torch.Tensor, keys: torch.Tensor) -> torch.Tensor:
|
|
"""Cache in the store kernel's layout: block-wide values, then block-wide scales."""
|
|
fmt = FP8_FLOAT32_SF_INDEX_KV_FORMAT
|
|
raw = torch.zeros((num_blocks, page_tokens, 1, fmt.bytes_per_token), dtype=torch.uint8, device=DEV)
|
|
flat = raw.view(-1)
|
|
for block in range(num_blocks):
|
|
base = block * fmt.bytes_per_token * page_tokens
|
|
values = keys[block].to(torch.float8_e4m3fn).view(torch.uint8).reshape(-1)
|
|
flat[base : base + page_tokens * HEAD_DIM] = values
|
|
sf = scales[block].float().reshape(-1).view(torch.uint8)
|
|
flat[base + page_tokens * HEAD_DIM : base + fmt.bytes_per_token * page_tokens] = sf
|
|
return raw
|
|
|
|
|
|
def reference_p_sum(
|
|
q: torch.Tensor,
|
|
kv_cache: torch.Tensor,
|
|
weights: torch.Tensor,
|
|
klen: torch.Tensor,
|
|
block_table: torch.Tensor,
|
|
page_tokens: int,
|
|
) -> torch.Tensor:
|
|
"""float64 reference: gather by explicit offsets, dequantize, `sum_h relu(q.k) w`."""
|
|
num_rows = q.shape[0]
|
|
max_len = int(klen.max())
|
|
out = torch.full((num_rows, max_len), float("-inf"), dtype=torch.float64, device=DEV)
|
|
fmt_bytes = FP8_FLOAT32_SF_INDEX_KV_FORMAT.bytes_per_token
|
|
for row in range(num_rows):
|
|
ctx = int(klen[row])
|
|
keys = []
|
|
for pos in range(ctx):
|
|
page = pos // page_tokens
|
|
offset = pos % page_tokens
|
|
block = int(block_table[row, page])
|
|
base = block * fmt_bytes * page_tokens
|
|
raw = kv_cache.view(-1)[base + offset * HEAD_DIM : base + (offset + 1) * HEAD_DIM]
|
|
scale = kv_cache.view(-1)[base + page_tokens * HEAD_DIM + offset * 4 : base + page_tokens * HEAD_DIM + offset * 4 + 4]
|
|
keys.append(raw.view(torch.float8_e4m3fn).double() * scale.view(torch.float32).double())
|
|
key = torch.stack(keys) # [ctx, head_dim]
|
|
scores = q[row].double() @ key.t()
|
|
out[row, :ctx] = (scores.relu() * weights[row].double().unsqueeze(-1)).sum(dim=0)
|
|
return out
|
|
|
|
|
|
def run_case(page_tokens: int, num_blocks: int, num_rows: int, num_heads: int, lens: list[int], seed: int) -> None:
|
|
torch.manual_seed(seed)
|
|
total_tokens = num_blocks * page_tokens
|
|
keys = torch.randn(num_blocks, page_tokens, HEAD_DIM, dtype=torch.float32, device=DEV) * 0.5
|
|
# One fp32 scale per token, as `elements_per_sf == quantized_elements == 128` implies.
|
|
scales = (keys.abs().amax(dim=-1) / 448.0).clamp_min(1e-8)
|
|
cache = build_cache(num_blocks, page_tokens, scales, keys)
|
|
|
|
q = torch.randn(num_rows, num_heads, HEAD_DIM, dtype=torch.float32, device=DEV) * 0.5
|
|
q_sf = (q.abs().amax(dim=-1, keepdim=True) / 448.0).clamp_min(1e-8) # one scale per (row, head)
|
|
q_fp8 = (q / q_sf).to(torch.float8_e4m3fn)
|
|
q_deq = q_fp8.float() * q_sf
|
|
|
|
weights = torch.rand(num_rows, num_heads, dtype=torch.float32, device=DEV)
|
|
klen = torch.tensor(lens, dtype=torch.int32, device=DEV)
|
|
max_pages = (max(lens) + page_tokens - 1) // page_tokens
|
|
block_table = torch.zeros(num_rows, max_pages, dtype=torch.int32, device=DEV)
|
|
for row in range(num_rows):
|
|
pages = (lens[row] + page_tokens - 1) // page_tokens
|
|
block_table[row, :pages] = torch.randperm(num_blocks, device=DEV)[:pages]
|
|
|
|
class _Stub:
|
|
max_kv_len = total_tokens
|
|
|
|
got = MQA._decode_p_sum_sm90(_Stub(), q_fp8, q_sf, cache, weights, klen, block_table)
|
|
ref = reference_p_sum(q_deq, cache, weights, klen, block_table, page_tokens)
|
|
|
|
got = got[:, : ref.shape[1]].double()
|
|
# Positions beyond each row's context are `-inf` by design on both sides; compare only the
|
|
# live window, which is what top-k actually sees.
|
|
live = torch.arange(ref.shape[1], device=DEV).unsqueeze(0) < klen.double().unsqueeze(1)
|
|
rel = (got[live] - ref[live]).norm() / ref[live].norm()
|
|
cos = torch.nn.functional.cosine_similarity(got[live], ref[live], dim=0)
|
|
worst = (got[live] - ref[live]).abs().max().item()
|
|
|
|
ok = "OK" if rel < 1e-5 else "MISMATCH"
|
|
print(
|
|
f"page={page_tokens:<4} blocks={num_blocks:<3} rows={num_rows} heads={num_heads:<3} "
|
|
f"lens={lens} rel={rel:9.2e} cos={cos:8.5f} max|d|={worst:9.2e} {ok}"
|
|
)
|
|
if rel >= 1e-5:
|
|
# Localize: which row/position disagrees first.
|
|
diff = (got - ref).abs().masked_fill(~live, -1)
|
|
row, col = divmod(int(diff.argmax()), diff.shape[1])
|
|
print(f"{'':4}worst at row {row} pos {col}: got={got[row, col].item():.6f} ref={ref[row, col].item():.6f}")
|
|
raise SystemExit("decode indexer disagrees with the reference")
|
|
|
|
|
|
def main() -> None:
|
|
print(f"device: {torch.cuda.get_device_name(0)}\n")
|
|
for page_tokens in (32, 64):
|
|
run_case(page_tokens, 4, 2, 8, [1, page_tokens * 2], seed=0)
|
|
run_case(page_tokens, 4, 2, 8, [page_tokens - 1, page_tokens], seed=1)
|
|
run_case(page_tokens, 4, 2, 8, [page_tokens * 3, page_tokens * 2 + 1], seed=2)
|
|
run_case(page_tokens, 8, 3, 32, [7, 33, page_tokens * 5 - 1], seed=3)
|
|
print("\nINDEX_DECODE_CHECK_DONE")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|