mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-31 16:56:21 +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.
100 lines
4.3 KiB
Python
100 lines
4.3 KiB
Python
"""Check the torch fallback for `deep_select.interface.topk` against a reference.
|
|
|
|
Covers the parts of the contract the indexer and sampler actually depend on:
|
|
`end` truncation (scores past it are uninitialized garbage), OOB padding when a row has
|
|
fewer than `topk` valid entries, `output_idx_offset`, in-place writes into a non-contiguous
|
|
view, and descending order for `sorted_index=True`.
|
|
"""
|
|
|
|
import torch
|
|
|
|
from deepinfer.h20_ops.deep_select_sm90 import topk
|
|
|
|
|
|
def ref_topk(scores, k, end=None, offset=None, fill=-1, val_fill=float("-inf")):
|
|
"""Reference: consider only [0, end) per row, take k largest, pad, then shift."""
|
|
rows = scores.shape[0]
|
|
idx_rows, val_rows = [], []
|
|
for r in range(rows):
|
|
n = int(end[r]) if end is not None else scores.shape[1]
|
|
row = scores[r, :n]
|
|
kk = min(k, n)
|
|
order = torch.argsort(row, descending=True)[:kk]
|
|
vals = row[order]
|
|
idxs = order.long()
|
|
if offset is not None:
|
|
idxs = idxs + int(offset[r])
|
|
pad = k - kk
|
|
if pad:
|
|
idxs = torch.cat([idxs, torch.full((pad,), fill, dtype=idxs.dtype, device=scores.device)])
|
|
vals = torch.cat([vals, torch.full((pad,), val_fill, dtype=vals.dtype, device=scores.device)])
|
|
idx_rows.append(idxs)
|
|
val_rows.append(vals)
|
|
return torch.stack(val_rows), torch.stack(idx_rows)
|
|
|
|
|
|
def main() -> None:
|
|
dev = "cuda"
|
|
torch.manual_seed(0)
|
|
rows, width, k = 6, 4096, 128
|
|
|
|
scores = torch.randn(rows, width, device=dev, dtype=torch.float32)
|
|
# Garbage past `end` that would win if the implementation ignored the bound.
|
|
end = torch.tensor([4096, 4096, 200, 128, 3, 50], dtype=torch.int32, device=dev)
|
|
for r, n in enumerate(end.tolist()):
|
|
scores[r, n:] = 1e6
|
|
|
|
vals, idxs = topk(scores, k, sorted=False, end=end, indices_type=torch.int32, return_value=True)
|
|
# Default `idx_oob_fill_value` is INT_MAX, so the reference must pad with the same value.
|
|
rv, ri = ref_topk(scores, k, end=end, fill=2147483647)
|
|
assert idxs.dtype == torch.int32
|
|
bad_rows = [r for r in range(rows) if not torch.equal(vals[r], rv[r]) or not torch.equal(idxs[r].long(), ri[r])]
|
|
print(f"[1] end truncation + OOB pad: rows mismatched = {bad_rows}")
|
|
assert not bad_rows
|
|
# Rows shorter than topk must be padded with the fill value, not with garbage indices.
|
|
short = (idxs[4] == 2147483647).sum().item()
|
|
print(f" row 4 (end=3) padded slots = {short} (expect {k - 3})")
|
|
assert short == k - 3
|
|
|
|
# Descending order for the valid prefix, which is what `sorted_index=True` promises.
|
|
lens = torch.clamp(end.long(), max=k)
|
|
for r in range(rows):
|
|
n = int(lens[r])
|
|
assert torch.equal(vals[r, :n], torch.sort(vals[r, :n], descending=True).values)
|
|
print("[2] descending order holds")
|
|
|
|
# In-place write into a non-contiguous view + per-row offset.
|
|
storage = torch.full((rows, k * 2), -7, dtype=torch.int32, device=dev)
|
|
view = storage[:, :k]
|
|
offset = torch.arange(rows, dtype=torch.int32, device=dev) * 1000
|
|
vals2, idxs2 = topk(
|
|
scores, k, end=end, indices_type=torch.int32, output_idx=view, output_idx_offset=offset,
|
|
idx_oob_fill_value=-1, return_value=False,
|
|
)
|
|
assert idxs2.data_ptr() == view.data_ptr()
|
|
assert vals2 is None
|
|
rv2, ri2 = ref_topk(scores, k, end=end, offset=offset, fill=-1)
|
|
print(f"[3] in-place view: match = {torch.equal(view.long(), ri2)}")
|
|
assert torch.equal(view.long(), ri2)
|
|
# The fill value must not be shifted by the offset.
|
|
assert int(view[4].min()) == -1
|
|
|
|
# No `end` at all takes the plain path.
|
|
v3, i3 = topk(scores, k, indices_type=torch.int64)
|
|
rv3, ri3 = ref_topk(scores, k)
|
|
print(f"[4] no end: values match = {torch.allclose(v3, rv3)}, indices match = {torch.equal(i3, ri3)}")
|
|
assert torch.allclose(v3, rv3) and torch.equal(i3, ri3)
|
|
|
|
# bf16 rows (the candidate-block path feeds bf16 block scores).
|
|
bs = torch.randn(3, 512, device=dev, dtype=torch.bfloat16)
|
|
vb, ib = topk(bs, 16, end=torch.tensor([512, 40, 8], dtype=torch.int32, device=dev), indices_type=torch.int32, return_value=False)
|
|
padded = int((ib[2] == 2147483647).sum())
|
|
print(f"[5] bf16: rows={tuple(ib.shape)} row2 padded={padded} (expect 8)")
|
|
assert padded == 16 - 8
|
|
|
|
print("PASS: deep_select torch fallback matches the documented contract")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|