Skip to content

[Bug] DeepSeek-V4 on SM12x: NaN MQA logits drive top_k_per_row_prefill to emit uninitialized smem as indices -> illegal memory access #49896

Description

@linjiapro

Environment

  • vLLM 0.26.0 (release wheel), flashinfer-python 0.6.14, torch 2.11.0+cu130, CUDA 13.0, driver 580.126.09
  • 4× NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition (SM120), TP=4
  • Model: deepseek-ai/DeepSeek-V4-Flash, --kv-cache-dtype fp8 --block-size 256 --tokenizer-mode deepseek_v4 --reasoning-parser deepseek_v4 --trust-remote-code, OpenAI-compatible server

Symptom

DeepSeek-V4-Flash on SM120 (enabled by #43477) crashes with CUDA error: an illegal memory access was encountered on all TP ranks → EngineDeadError, on the first prefill batch containing a request longer than ~2048 tokens (e.g. 16 concurrent structured-output requests with 0.5–8K-token prompts; also a single ~5K-token prompt). Short prompts serve perfectly. We reproduced the crash 9 times across vLLM 0.25.1/0.26.0 and flashinfer 0.6.13/0.6.14/0.6.15.

Under CUDA_LAUNCH_BLOCKING=1, the fault is raised in the Triton kernel _compute_global_topk_indices_and_lens_kernel (vllm/models/deepseek_v4/common/ops/cache_utils.py), reached from DeepseekV4FlashInferSM120Attention._forward_prefill (compress-ratio-4 branch).

Root-cause chain (instrumented at the tensor level)

1. fp8_fp4_mqa_logits produces NaN-dominated logits on SM120.
Auditing the valid [ks, ke) region of the prefill MQA logits per C4A layer on a single 4,972-token prefill: the first C4A layer is 83% NaN (2,560,388 of 3,088,855 entries; the finite remainder looks plausible, range ≈ −2…4), and every subsequent layer is 100% NaN (cascade through the model). This is presumably the SM12x scale-handling issue (cf. the e8m0→fp32 upcast in #41834).

2. top_k_per_row_prefill mishandles NaN input on its histogram path and emits uninitialized shared memory as indices.
Auditing topk_indices_buffer immediately after ops.top_k_per_row_prefill returns: rows whose candidate count rowLen = ke−ks ≤ topk (the short-row path) are clean (-1-padded as documented); rows with rowLen > topk (histogram path) contain garbage starting at exactly the first such row, e.g. rows 2051…4868 of a 4,869-token request at compress ratio 4 (2051/4 ≈ 513 > 512 = index_topk). Garbage row counts differ across TP ranks on identical input (1923/2151/2120/1987) — nondeterministic, consistent with uninitialized dynamic shared memory (smemOutput is never pre-initialized and the final copy loop writes all topK slots).

This reproduces in isolation, without serving (25 lines, below): with clean random logits the kernel is correct on SM120 (0 garbage rows, and tests/kernels/test_top_k_per_row.py passes 12/12 here); with 83%-NaN logits, ~40% of histogram-path rows contain garbage. Two distinct failure signatures:

  • indices equal to 2143289344 = 0x7FC00000 — a float32 NaN bit pattern reinterpreted as an int32 index;
  • output slots that keep their pre-launch canary value — i.e. never written at all (in serving these become whatever is in shared memory).

Since the repro is data-driven (NaN input) rather than SM120-specific, the kernel's NaN mishandling likely affects all architectures; SM120 is where NaN logits actually occur in practice.

import torch, vllm  # importing vllm registers the _C ops
torch.manual_seed(0); torch.set_default_device("cuda:0")

rows, cols, top_k = 4869, 1243, 512
ks = torch.zeros(rows, dtype=torch.int32)
ke = torch.clamp(torch.arange(rows, dtype=torch.int32) // 4 + 1, max=cols)

def probe(tag, logits):
    out = torch.full((rows, top_k), -7, dtype=torch.int32)  # -7 = canary
    torch.ops._C.top_k_per_row_prefill(logits, ks, ke, out, rows,
                                       logits.stride(0), logits.stride(1), top_k)
    torch.cuda.synchronize()
    garb = (out >= 1_000_000) | ((out < -1) & (out != -7))
    print(f"{tag}: garbage_rows={int(garb.any(dim=1).sum())}/{rows} "
          f"unwritten_rows={int((out == -7).any(dim=1).sum())} max={int(out.max())}")

logits = torch.randn(rows, cols)
probe("clean ", logits.clone())                                  # 0 garbage
nanl = logits.clone(); nanl[torch.rand(rows, cols) < 0.83] = float("nan")
probe("83%NaN", nanl)   # ~1800 garbage rows, ~500 unwritten, max=2143289344

3. compute_global_topk_indices_and_lens dereferences the garbage.
Its validity mask is only local_idx >= 0, so INT32_MAX-class indices pass and it gathers block_table[req, idx // block_size] far out of bounds → the illegal memory access that kills the engine.

Behavioral confirmation

Needle-in-haystack on the same server: a prompt below the sparse threshold retrieves the needle perfectly ("The secret code word is BLUEBERRY."); the identical needle in a ~4.5K-token prompt yields output completely unrelated to the prompt (the model hallucinates arbitrary code/text — its selected KV set is effectively random). Short prompts mask defect (1) entirely because when rowLen ≤ topk the top-k selects all candidates and scores never matter.

Suggested fixes (layered)

  1. Kernel hardening (csrc/libtorch_stable/sampler.cu): the histogram path should never emit unwritten smem or NaN bit patterns as indices — pad under-filled output with -1 like the short-row path, and exclude NaN scores from selection. Data-driven robustness fix, valuable on every arch. (We can't build csrc locally to contribute this ourselves.)
  2. SM12x routing: until (1) lands, route SM12x prefill top-k through a torch.topk implementation of the kernel's documented contract — the decode path already excludes SM12x from cooperative_topk. PR incoming; verified on the affected hardware that the previously 9×-reproducible crash is eliminated.
  3. The NaN logits themselves (the primary SM12x defect): DeepGEMM fp8_fp4_mqa_logits on SM120. [New Model][Nvidia] Add SM12x support for DeepSeek V4 Flash with essential fixes #41834 appears to carry the relevant fixes (e8m0→fp32 upcast, SM120 DeepGEMM fallbacks) but is unmerged; even with (1)+(2), DSv4 output quality on SM120 remains broken for >2048-token prompts until this is addressed — with NaN scores, any correct top-k selects an arbitrary candidate set.

Happy to run further diagnostics on this hardware — SM120 workstations seem rare in CI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions