mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-11-02 17:56:22 +08:00
DeepInfer targets Blackwell: the vendored FlashMLA fork only ships SM100 kernels (`CUDA_ARCHS = ["sm_100a", "sm_103a"]`, every entry point asserts `arch.is_sm100f()`), and several DeepGEMM attention APIs plus the whole quantization recipe assume FP4 tensor cores, packed UE8M0 scales and per-32 blocks. None of that exists on Hopper, so this branch adds an SM90 backend and gets an 8x H20 node to produce tokens end to end. Scope and status: the pipeline runs (prefill, decode, sampling) and matches the reference on the first token, but numerics still diverge from position ~12 onward, so the output is not yet correct. H20-DEBUG-PLAN.md tracks the remaining work with the exact reproduction case; see H20-SM90-FINDINGS.md for the analysis. Quantization - `Platform.sf_pack_factor` separates two meanings that `fp8_sf_type` conflated: the GEMM scale type (FP32 on SM90) and the number of scales packed per element (1, instead of 4 UE8M0 exponents per int32). KV caches keep the ue8m0 byte layout. - Requantize FP4 experts and ue8m0 dense weights to FP8 E4M3 with FP32 per-128 scales while loading (`hugging_face/weights.py`, `h20_ops/quant.py`), so the runtime sees a plain FP8 checkpoint: `mxfp4` is disabled at runtime, expert weights are repacked as 2D, and `use_fp4_sparse_kv` is off since the FP4 cache kernels do not exist on Hopper. - GEMM recipes switch to `(1, 128, 128)` with `disable_ue8m0_cast=True`, which is the only shape SM90 DeepGEMM accepts. Attention - `h20_ops/sparse_mla_sm90.py` adapts TileLang's `examples/deepseek_v32` sparse MLA kernel (which is Hopper-targeted) to the V4.1 layout: RoPE lives inside the 512-wide latent, so the V3.2 `dim`/`tail` split is removed and QK/PV are both 512. Measured on H20: 104 TFLOP/s, bf16-noise accuracy. It takes no causal mask, because the indices are already causal -- deriving causality from the row number drops every index once the KV buffer is offset, which produced an all-NaN layer 0. - `h20_ops/attn_sm90.py` re-implements `prefill_attn_varlen_sparse` and `decode_mla_mqa_sparse` with the same signatures as `attention_backend.py`, and registers them through `patch_imports`, so call sites are untouched and the FlashMLA-only module is never imported. - Softmax normalisation clamps `sumexp`, so a padded row with no valid index yields zeros instead of 0/0 and cannot poison the whole hidden state. Engram - `h20_ops/engram_local.py` replaces `deep_ep.EngramBuffer.fetch`, which needs NCCL GIN (RDMA) and is unavailable on a single node, with a distributed gather over NVLink built from two `all_to_all`s. Row ownership follows the C++ layout (`owner = idx // num_local`). Verified against a full reference table on 8 ranks. Other Blackwell assumptions removed - DeepSelect's top-k kernel faults at launch on SM90, so `h20_ops/deep_select_sm90.py` reproduces its contract with `torch.topk` (end truncation, OOB fill, in-place output views). The shared-layer candidate-block optimisation is disabled because `fp8_fp4_sparse_mqa_logits` has no SM90 implementation, and the dense MQA-logits path is used instead; its fp32 output is narrowed for the block-max kernel. - Decode-time index scoring uses `_decode_p_sum_sm90`: Hopper's paged kernel hard-requires 64-token pages while the cache uses 128, and the pages are strided views that cannot be split without a copy. - `mega_mhc` / `mega_moe`, the FlashAttention-4 CuTe DSL kernels and the fused norm+rope+cast kernel are all SM100-only and take a Hopper fallback. - `allow_cuda_sync()` is used for the few places that genuinely need a host read (all-to-all sizing, the ViT fallback, the NaN diagnostic) instead of disabling the sync guard globally. Vendored submodules (not part of this commit; submodule gitlinks are unchanged) - `vendor/DeepSelect/setup.py`: re-enable the `sm_90a` gencode that is commented out, otherwise top-k fails with "no kernel image". Scripts: `h20_attn_check.py`, `h20_engram_check.py`, `h20_topk_check.py`, `h20_smoke_check.py`, `h20_real_requant_check.py` are unit checks that run on 8x H20; `h20_ask.py` is a one-prompt harness and `ref_next_token.py` produces the official reference next-token for arbitrary ids, so both sides can be compared on identical input.
160 lines
7.2 KiB
Python
160 lines
7.2 KiB
Python
"""Validate the SM90 attention path (TileLang sparse MLA) against a torch reference.
|
|
|
|
Shapes follow DeepSeek-V4.1-Flash: head_dim 512 with RoPE inside, 64 query heads, MQA (kv_group 1).
|
|
|
|
python scripts/h20_attn_check.py # correctness + perf
|
|
"""
|
|
|
|
import torch
|
|
|
|
from tilelang.profiler import do_bench
|
|
|
|
from deepinfer.h20_ops.attn_sm90 import (
|
|
CACHE_ELEMENTS,
|
|
CACHE_ELEMENTS_PER_SF,
|
|
decode_mla_mqa_sparse,
|
|
prefill_attn_varlen_sparse,
|
|
)
|
|
|
|
DEV = "cuda"
|
|
HEADS = 64
|
|
DIM = 512
|
|
|
|
|
|
def ref_attention(q, kv, indices, scale, sink=None):
|
|
"""q [n, h, d], kv [m, 1, d], indices [n, topk] -> [n, h, d] float32."""
|
|
n, h, d = q.shape
|
|
topk = indices.shape[1]
|
|
valid = indices >= 0
|
|
idx = indices.clamp_min(0)
|
|
k = kv[idx.reshape(-1), 0, :].view(n, topk, d).float() # [n, topk, d]
|
|
v = k
|
|
scores = torch.einsum("nhd,nkd->nhk", q.float(), k) * scale # [n, h, topk]
|
|
scores = scores.masked_fill(~valid.unsqueeze(1), float("-inf"))
|
|
probs = scores.softmax(dim=-1)
|
|
out = torch.einsum("nhk,nkd->nhd", probs, v)
|
|
lse = torch.logsumexp(scores, dim=-1)
|
|
if sink is not None:
|
|
out = out * torch.sigmoid(lse - sink.float().unsqueeze(0)).unsqueeze(-1)
|
|
return out, lse
|
|
|
|
|
|
def make_indices(n, m, topk, gen):
|
|
"""Causal top-k like the indexer produces: random subset of [0, position], -1 padded."""
|
|
indices = torch.full((n, topk), -1, dtype=torch.int32, device=DEV)
|
|
for t in range(n):
|
|
hi = min(t + 1, m)
|
|
take = min(topk, hi)
|
|
picked = torch.randperm(hi, generator=gen)[:take]
|
|
indices[t, :take] = picked.to(torch.int32)
|
|
return indices
|
|
|
|
|
|
def quantize_cache(kv, block_size=64):
|
|
"""Build a V41-format paged cache ([rows, 528] uint8) from bf16 kv [m, 1, d]."""
|
|
m = kv.shape[0]
|
|
values = kv[:, 0, :].float()
|
|
groups = values.view(m, DIM // CACHE_ELEMENTS_PER_SF, CACHE_ELEMENTS_PER_SF)
|
|
amax = groups.abs().amax(dim=-1).clamp_min(torch.finfo(torch.float32).tiny)
|
|
# UE8M0: round the per-32 scale up to a power of two so the byte encoding is exact.
|
|
exp = torch.ceil(torch.log2(amax / 448.0))
|
|
exp = exp.clamp_min(-126)
|
|
scale = torch.pow(2.0, exp)
|
|
q = (groups / scale.unsqueeze(-1)).to(torch.float8_e4m3fn)
|
|
packed = torch.cat(
|
|
[q.reshape(m, DIM).view(torch.uint8), (exp + 127).to(torch.uint8)], dim=1
|
|
) # [m, 528]
|
|
deq = (q.float() * scale.unsqueeze(-1)).reshape(m, DIM).to(torch.bfloat16)
|
|
rows = packed.contiguous()
|
|
n_blocks = (m + block_size - 1) // block_size
|
|
pad = n_blocks * block_size - m
|
|
if pad:
|
|
rows = torch.cat([rows, rows.new_zeros(pad, rows.shape[1])], dim=0)
|
|
deq = torch.cat([deq, deq.new_zeros(pad, deq.shape[1])], dim=0)
|
|
return rows.view(n_blocks, block_size, 1, -1), deq
|
|
|
|
|
|
def main():
|
|
torch.manual_seed(0)
|
|
gen = torch.Generator(device="cpu").manual_seed(0)
|
|
scale = DIM**-0.5
|
|
|
|
# ---------------- prefill: n query tokens packed in one sequence ----------------
|
|
n, m, topk = 512, 1024, 512
|
|
q = torch.randn(n, HEADS, DIM, device=DEV, dtype=torch.bfloat16)
|
|
kv = torch.randn(m, 1, DIM, device=DEV, dtype=torch.bfloat16)
|
|
indices = make_indices(n, m, topk, gen)
|
|
sink = torch.randn(HEADS, device=DEV, dtype=torch.float32) * 2
|
|
|
|
out, lse = prefill_attn_varlen_sparse(q, kv, indices, scale, attn_sink=sink)
|
|
ref, _ = ref_attention(q, kv, indices, scale, sink=sink)
|
|
err = (out.float() - ref).abs().max().item()
|
|
print(f"prefill (n={n}, m={m}, topk={topk}, heads={HEADS}): max_abs_err={err:.4f}")
|
|
|
|
# ---------------- absolute positions: row index must not act as a causal bound --------
|
|
# The engine's KV buffer is `[cached swa | current swa | compressed]`, so indices are
|
|
# absolute positions that can exceed the query's row number. Deriving causality from the
|
|
# row index (the original TileLang example did) silently drops every index here.
|
|
n_abs, m_abs, topk_abs = 16, 4096, 256
|
|
pad = 3000 # stand-in for `total_k_swa_cached + total_k_swa_cur`
|
|
q_abs = torch.randn(n_abs, HEADS, DIM, device=DEV, dtype=torch.bfloat16)
|
|
kv_abs = torch.randn(m_abs, 1, DIM, device=DEV, dtype=torch.bfloat16)
|
|
gen_abs = torch.Generator(device="cpu").manual_seed(7)
|
|
idx_abs = torch.full((n_abs, topk_abs), -1, dtype=torch.int32, device=DEV)
|
|
for t in range(n_abs):
|
|
lo = pad + t
|
|
picked = lo + torch.randperm(min(64, m_abs - lo - 1), generator=gen_abs)[:64]
|
|
idx_abs[t, : picked.numel()] = picked.to(torch.int32)
|
|
assert (idx_abs >= 0).sum() > 0 and int(idx_abs.max()) > n_abs
|
|
out_abs, _ = prefill_attn_varlen_sparse(q_abs, kv_abs, idx_abs, scale)
|
|
ref_abs, _ = ref_attention(q_abs, kv_abs, idx_abs, scale)
|
|
err_abs = (out_abs.float() - ref_abs).abs().max().item()
|
|
print(f"absolute positions (indices up to {int(idx_abs.max())} vs rows {n_abs}): max_abs_err={err_abs:.4f}")
|
|
assert err_abs < 0.2, "row-index causal bound regression"
|
|
|
|
# ---------------- empty window: all-`-1` indices must yield zeros, not NaN ----------------
|
|
# A padded batch looks exactly like this (no valid top-k for any row); a 0/0 softmax here
|
|
# used to turn the entire hidden state into NaN on the next layer.
|
|
q_empty = torch.randn(8, HEADS, DIM, device=DEV, dtype=torch.bfloat16)
|
|
kv_empty = torch.randn(64, 1, DIM, device=DEV, dtype=torch.bfloat16)
|
|
idx_empty = torch.full((8, 128), -1, dtype=torch.int32, device=DEV)
|
|
out_e, lse_e = prefill_attn_varlen_sparse(q_empty, kv_empty, idx_empty, scale)
|
|
print(
|
|
f"empty window: finite={bool(torch.isfinite(out_e).all())} "
|
|
f"zero={bool((out_e == 0).all())} lse_max={lse_e.max().item():.3e}"
|
|
)
|
|
assert torch.isfinite(out_e).all() and (out_e == 0).all()
|
|
|
|
# ---------------- decode: paged fp8 cache + gather ----------------
|
|
n_dec, m_dec, topk_dec = 8, 4096, 1024
|
|
q2 = torch.randn(n_dec, HEADS, DIM, device=DEV, dtype=torch.bfloat16)
|
|
kv2 = torch.randn(m_dec, 1, DIM, device=DEV, dtype=torch.bfloat16)
|
|
cache, kv_deq = quantize_cache(kv2)
|
|
|
|
idx2 = torch.randint(0, m_dec, (n_dec, topk_dec), device=DEV, dtype=torch.int32)
|
|
dec, _ = decode_mla_mqa_sparse(
|
|
q2, cache, DIM, idx2, sched_meta=None, softmax_scale=scale, attn_sink=sink
|
|
)
|
|
ref2, _ = ref_attention(q2, kv_deq.unsqueeze(1), idx2, scale, sink=sink)
|
|
err2 = (dec.float() - ref2).abs().max().item()
|
|
corr2 = torch.cosine_similarity(dec.float().flatten(), ref2.flatten(), dim=0).item()
|
|
print(f"decode (n={n_dec}, cache={m_dec}, topk={topk_dec}): max_abs_err={err2:.4f} corr={corr2:.6f}")
|
|
|
|
ms2 = do_bench(lambda: decode_mla_mqa_sparse(q2, cache, DIM, idx2, sched_meta=None, softmax_scale=scale, attn_sink=sink), warmup=5, rep=20)
|
|
print(f"decode perf: batch={n_dec} topk={topk_dec} -> {ms2:.2f} ms/step")
|
|
|
|
# ---------------- perf: full-size prefill ----------------
|
|
n_big, m_big, topk_big = 4096, 8192, 2048
|
|
qb = torch.randn(n_big, HEADS, DIM, device=DEV, dtype=torch.bfloat16)
|
|
kvb = torch.randn(m_big, 1, DIM, device=DEV, dtype=torch.bfloat16)
|
|
ib = make_indices(n_big, m_big, topk_big, gen)
|
|
ms = do_bench(lambda: prefill_attn_varlen_sparse(qb, kvb, ib, scale), warmup=10, rep=50)
|
|
tflops = n_big * 2 * 2 * topk_big * DIM * HEADS / (ms * 1e-3) / 1e12
|
|
print(f"prefill perf: {ms:.2f} ms -> {tflops:.1f} TFLOP/s (H20 bf16 peak ~148)")
|
|
|
|
print("ATTN_CHECK_DONE")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|