Skip to content

[SM120] Support 32-state pages in FP8 paged-MQA logits - #14

Open
luoyuctl wants to merge 1 commit into
vllm-project:devfrom
luoyuctl:fix/sm120-fp8-paged-mqa-page32
Open

luoyuctl wants to merge 1 commit into
vllm-project:devfrom
luoyuctl:fix/sm120-fp8-paged-mqa-page32

Conversation

@luoyuctl

@luoyuctl luoyuctl commented Sep 17, 2026

Copy link
Copy Markdown

Summary

Enable FP8 paged-MQA logits with 32-state KV pages on SM120, without switching to an FP4 indexer cache or introducing a fallback kernel.

DeepSeek-V4.1 mixes compression ratios 1 and 2. With a 64-token kernel block, these yield indexer pages of 64 and 32 states respectively. The SM120 FP8 path currently rejects the latter in both the API and host launcher.

  • Permit block_kv in {32, 64} for SM120 at the API and FP8 launcher boundaries.
  • Reuse the existing split_kv / block_kv grouping: 128 / 32 = 4 groups, compared with 128 / 64 = 2.
  • Use two KV pipeline stages only for page32 + 64 heads + (varlen or next_n >= 2), to stay within the SM120 shared-memory budget. Other configurations retain three stages.
  • Assert supported page sizes and sufficient TMA warps at compile time.
  • Add 72 parameterized correctness cases to the existing SM120 MQA test suite.

Duplicate-work check / overlap

Checked open SM120 / paged-MQA PRs before submission. #10 migrates the SM120 device layer and changes page addressing, but its FP8 paged kernel still restricts PAGE_KV to 64/128/256; it does not enable FP8 page32. This narrow API/launcher/kernel change addresses that missing case. The device-header edits will need reconciliation if #10 lands first; I am happy to coordinate rather than maintain divergent kernels.

Related integration discussion: vllm-project/vllm#56461, vllm-project/vllm#56702, vllm-project/vllm#57292. This PR alone does not provide complete model support: vLLM cache geometry and FlashInfer dual-cache prefill support are separate dependencies.

Validation

Recorded earlier in this development session on NVIDIA RTX PRO 5000 72GB Blackwell (SM120), for this page32 implementation based on a6bbb8000161c0dc3a85a0300a905f76898a7913:

pytest tests/test_sm120_mqa.py -k fp8_paged_mqa_page_geometry
72 passed, 11 deselected in 212.78s

pytest tests/test_sm120_mqa.py -k 'not fp8_paged_mqa_page_geometry and not test_filter'
10 passed, 73 deselected in 9.92s

The imported test_filter helper is excluded from pytest collection in the second command. The added matrix covers page32/64, heads16/32/64, head dimensions32/64/128, uniform next_n=1/2/3 and varlen, shuffled physical pages, padded page strides, zero/partial contexts, a float64 reference, repeat execution and CUDA graph replay.

Recorded metadata+logits graph-replay medians at 32 heads, in microseconds (not end-to-end model throughput):

batch / context page32 page64
1 / 4096 3.82 3.82
8 / 4096 5.31 5.08
1 / 32768 4.97 4.62

Submission-time local checks: git diff --check HEAD^ HEAD and Python AST parsing of the test file passed. GPU tests were not rerun while preparing this draft. No repository pre-commit configuration is present.

Validation limits and remaining review items

  • Compute-sanitizer, large-load profiling and full-model evaluation of this exact dependency combination remain outstanding.
  • A separate backport onto an older DeepGEMM 2.6.1 serving image, with vLLM and FlashInfer patches, passed startup and a short chat on 8x SM120. That is NOT an end-to-end validation of this fork commit and is not presented as one.
  • Human contributor review and reproduction of the relevant tests remain outstanding before merge. Marking this PR ready invites maintainer review; it does not certify completed human review or kernel correctness.

Failure evidence and reproduction

The following is an excerpt from the archived old serving-image error (2026-09-17); it is not a traceback captured against this fork's current base. Paths/line numbers belong to that image. The final assertion was propagated through the worker RPC:

File "/usr/local/lib/python3.12/dist-packages/vllm/v1/executor/multiproc_executor.py", line 103, in _wait_for_response
    response = self.aggregate(self.get_response())
File "/usr/local/lib/python3.12/dist-packages/vllm/v1/executor/multiproc_executor.py", line 437, in get_response
    raise RuntimeError(
RuntimeError: Worker failed with error 'Assertion error (/workspace/.deps/deepgemm-src/csrc/apis/attention.hpp:320): (arch_major == 10 and (block_kv == 32 or block_kv == 64 or block_kv == 128)) or (arch_major == 9 and (block_kv == 32 or block_kv == 64)) or (arch_major == 12 and ((is_fp4 and (block_kv == 32 or block_kv == 64)) or (not is_fp4 and block_kv == 64)))', please check the stack trace above for the root cause

Process/timestamp prefixes were removed for readability. The recovered excerpt is the RPC propagation tail; it is not presented as the full original worker call stack. The independent fork code has the same SM120 FP8 page32 rejection; its line numbers differ.

Standalone regression recipe

On an SM120 CUDA machine with a matching PyTorch/CUDA development environment, check out this PR, initialize its submodules, and build the extension and its matching JIT headers together. Merely replacing the CUDA header leaves the API/launcher restrictions inside the old _C binary.

The saved build commands used a system-site-packages venv, libdw-dev, and NVIDIA wheel header/library paths:

git submodule update --init --recursive
uv venv --system-site-packages .venv
# Install pytest / required build dependencies in this venv if absent.
# libdw-dev is an OS build prerequisite for the DeepJIT-based fork.
export CPATH="$(find /usr/local/lib/python3.12/dist-packages/nvidia -type d -name include | paste -sd:)"
export LIBRARY_PATH="$(find /usr/local/lib/python3.12/dist-packages/nvidia -type d -name lib | paste -sd:)"
DG_USE_LOCAL_VERSION=0 DG_FORCE_BUILD=1 MAX_JOBS=4 \
  .venv/bin/python setup.py build_ext --inplace
.venv/bin/python -m pytest tests/test_sm120_mqa.py \
  -k fp8_paged_mqa_page_geometry -q
.venv/bin/python -m pytest tests/test_sm120_mqa.py \
  -k 'not fp8_paged_mqa_page_geometry and not test_filter' -q

Adjust the NVIDIA wheel search root for a different Python installation. The recorded results are 72 and 10 passes, respectively, as listed above. For a pre-fix comparison, retain the added test file but rebuild the parent revision's extension and matching headers in a separate checkout; page32 is expected to fail the original assertion. That exact parent-revision pytest rerun was not recorded, so it is a reproduction recipe, not an additional claimed test result.

Environment and evidence boundaries (2026-09-17)

These are two different validation tracks; they must not be combined into a claim that the current vLLM PR head was tested end-to-end.

Item Fork kernel test Serving backport smoke test
GPU 1 allocated NVIDIA RTX PRO 5000 72GB Blackwell, SM120 8x same GPU, TP8 + expert parallelism
DeepGEMM a6bbb8000161c0dc3a85a0300a905f76898a7913 + the page32 change now committed as 471d62c4ef85e259c33cf66b25f48cbbf8e758ef DeepGEMM 2.6.1 lineage, 8b1392b + a separate page32 backport
Environment provenance Saved test/build Pod manifests use the same base serving image; saved build log confirms CPython 3.12 and -std=c++20. The temporary Pod was deleted; a complete contemporaneous environment dump was not retained. Versions below were read from the still-running successful Pod, not inferred from the build host
Python 3.12 confirmed by saved build log; exact patch version not independently retained for this test Pod 3.12.3
PyTorch Shared base image; exact package version was not retained in the four archived test logs 2.13.0+cu130
CUDA toolkit / nvcc Shared base image; not independently retained in the archived test logs CUDA 13.0, nvcc V13.0.88
NVIDIA driver Historical test-time driver version was not separately archived 595.58.03, read from the running node during this documentation update
vLLM Not a full-model test 0.1.dev20904+g179dd0fa9 + geometry backports
FlashInfer Not used for the standalone DeepGEMM kernel tests flashinfer-python 0.6.18, flashinfer-jit-cache 0.6.18+cu130, with local decode/prefill patches

Successful serving image manifest digest, confirmed from the Pod's imageID:
sha256:406cb11244831d00bbe05e90834a94177e3befe505d61570f22e2e1eb6cc75f5.
This is the manifest digest; the previously quoted 17bff2ab... is an image-config digest and must not be used as the pull digest.

The successful deployment used --tensor-parallel-size 8, --enable-expert-parallel, --kv-cache-dtype fp8, --block-size 64, --max-model-len 262144, --max-num-seqs 64, --max-num-batched-tokens 8192, --gpu-memory-utilization 0.95, EPLB, and DSpark K=5 with adaptive verification disabled. These are experiment settings, not a claim that every setting is necessary or optimal.

AI assistance

AI assistance was used for implementation, review, test execution and preparation of this PR. The implementation is submitted for review; human review and validation remain required before merge. No completed human sign-off on kernel correctness is asserted.

Commit-message correction

The existing commit message mistakenly writes 64 / 32 when describing four KV groups. The implementation reuses split_kv / block_kv, which is 128 / 32 = 4. The source and summary above are correct; this note corrects the prose without rewriting the tested commit.

The SM12x FP8 paged-MQA path asserted block_kv == 64, which leaves the
DeepSeek-V4.1 sparse indexer without a usable page on SM120: vLLM
disables the MXFP4 indexer cache on SM12x (dsa_indexer_uses_fp4()), so
the FP8 cache is the only option, and V4.1 mixes compress_ratios 1 and 2,
so a 64-token storage block yields num_states = 32 for the ratio-2
layers.

Accept block_kv == 32 by splitting the KV range into kNumGroups = 4
groups (64 / 32), matching the existing SM90 block_kv=32 port, and drop
num_kv_stages to 2 for the (block_kv=32, num_heads=64) shape with varlen
or next_n >= 2 so the kernel stays within the 99 KiB SMEM budget.

Verified on RTX PRO 5000 (sm_120): 72/72 new parameterized cases pass
(page 32/64 x heads 16/32/64 x next_n 1/2/3 + varlen x head_dim
32/64/128, including random physical pages, padded strides, empty
history, partial pages, CUDA graph replay) and the 10 pre-existing SM120
cases still pass. Graph-replay latency for the page-32 path is within
4.5%-7.6% of page-64 at heads=32.

Signed-off-by: zack <51604064+luoyuctl@users.noreply.github.com>

Copy link
Copy Markdown
Author

@lucifer1004 @zyongye Could you take a look at this change when you have time?

DeepSeek-V4.1's ratio-2 layers produce 32-state indexer pages with a 64-token kernel block, but the SM120 FP8 paged-MQA path currently rejects them. This patch extends the existing kernel rather than adding a fallback. It keeps the page64 path unchanged and reduces KV pipeline stages only for page32 configurations that would exceed the shared-memory budget.

We recorded 72 new correctness cases and 10 existing tests passing on RTX PRO 5000 (SM120), including CUDA graph replay. The PR description includes the test environment, reproduction steps, and remaining validation gaps.

@lucifer1004, I'd especially appreciate your review of the TMA/barrier handling and advice on coordinating this with #10. @zyongye, your feedback on the scope and merge order for this fork would also be helpful. Thanks!

@lucifer1004

Copy link
Copy Markdown

Hi @luoyuctl — I'm the author of #10 (the SM120 device-layer vendoring). Happy to coordinate; here's how the pieces fit.

Where the conflict is. #14's device-header edit lands on the fork's pre-vendoring sm120_fp8_paged_mqa_logits.cuh. #10 replaces that header with a byte-identical vendored copy from DeepGEMM-sm120 v0.1.3, which renamed BLOCK_KVPAGE_KV and restructured page addressing: the vendored kernel fixes the compute tile at BLOCK_KV=64 and supports physical PAGE_KV ∈ {64, 128, 256} via within-page TMA offsets. Your two static_asserts reference the old constants and would not survive the rebase textually or semantically.

Why page32 still needs kernel work. With 32-row pages, a 64-row compute tile would straddle two non-contiguous physical pages, which the current TMA addressing (tile_idx * BLOCK_KV / PAGE_KV, % PAGE_KV) cannot express — so PAGE_KV=32 genuinely requires a BLOCK_KV=32 / 4-group kernel configuration, not just relaxed asserts. That makes it a device-layer feature, and under the vendoring scheme the device layer's single source of truth is DeepGEMM-sm120 (a scheduled watcher flags fork trees that match no release tag, so landing kernel edits directly here would create exactly the divergence we just removed).

Proposed sequencing:

  1. feat(sm120): vendor DeepGEMM-sm120 v0.1.0 device layer #10 lands first (it's review-complete and mergeable).
  2. The page32 kernel support (BLOCK_KV=32 template path with the group/TMA-warp accounting from your change) goes into DeepGEMM-sm120, where CI cross-compiles it against both fork lineages; I can do the port myself with credit to you, or you're welcome to open the PR there directly — whichever you prefer. Released as v0.1.4.
  3. [SM120] Support 32-state pages in FP8 paged-MQA logits #14 then rebases onto dev + vendored v0.1.4 and shrinks to the parts that genuinely belong to this fork: the API/launcher relaxation (block_kv ∈ {32, 64}, the num_kv_stages SMEM heuristic) and your test matrix — no device-header edit needed.

Same feature is relevant to the nv_dev-lineage fork (deepseek-ai#447, DSv4.1 indexer pages), which picks it up by vendoring the same tag — another reason to route the kernel change through the canonical repo rather than per-fork. Happy to review either way.

lucifer1004 added a commit to lucifer1004/DeepGEMM-sm120 that referenced this pull request Sep 17, 2026
Mirror the FP4 sibling's derivation in the FP8 kernel: BLOCK_KV =
min(PAGE_KV, 64), so a 32-row physical page gets a 32-row compute tile
(a 64-row tile would straddle two non-contiguous pages, which the
within-page TMA offset addressing cannot express). SPLIT_KV stays 128 =
BLOCK_KV * groups, so PAGE_KV=32 yields 4 KV groups (4 TMA warps each
own one group). Motivated by DSv4.1 indexer caches, which mix 64- and
32-state pages (vllm-project/DeepGEMM#14).

Gate coverage: instantiate FP8 paged MQA at PAGE_KV 32/64/256.
Compile gate passes against both deployment lineages (deepseek main,
vllm dev). Runtime enablement stays with the forks' host glue.

Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>

Copy link
Copy Markdown
Author

Thanks @lucifer1004, that sequencing makes sense. Please go ahead with the page32 kernel port in DeepGEMM-sm120, and thanks for offering to credit our contribution.

Once #10 and the updated device layer are integrated into this fork, we’ll rebase #14, drop the device-header changes, and keep the API/launcher updates and test matrix. We’ll rerun the SM120 tests against that version and ask for another review.

Thanks for helping coordinate this!

lucifer1004 added a commit to lucifer1004/DeepGEMM that referenced this pull request Sep 17, 2026
FP8 paged MQA logits gains PAGE_KV=32 (BLOCK_KV derived as
min(PAGE_KV, 64), mirroring the FP4 sibling). Device-only update: this
fork's host launcher still restricts paged FP8 to page 64, so page32
stays inert until host glue opts in (vllm-project#14).

Validated on sm_120a: test_sm120_mqa.py + test_sm120_fp8_fp4.py 23/23
passed from a fresh JIT cache (only the pre-existing test_filter
collection quirk remains, deepseek-ai#446).

Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
lucifer1004 added a commit to lucifer1004/DeepGEMM that referenced this pull request Sep 17, 2026
Vendor DeepGEMM-sm120 v0.1.4, which derives BLOCK_KV = min(PAGE_KV, 64)
in the FP8 paged kernel (mirroring the FP4 sibling): a 32-row page gets
a 32-row compute tile and 4 KV groups at SPLIT_KV=128, instead of a
64-row tile straddling two non-contiguous physical pages. DSv4.1 indexer
caches mix 64- and 32-state pages (vllm-project#14).

Host glue: relax the FP8 launcher and fused-cache API gates to admit
block_kv=32 for arch 12, derive tile_kv = min(block_kv, 64) like the FP4
launcher, and drop to two KV stages for page32 + 64 heads + paired
queries, where three stages exceed the 99 KiB SMEM budget by 4 bytes.

Tests: the paged-MQA contract matrix gains (fp8, page32), and a focused
case covers the two-stage fallback (page32, 64 heads, paired/varlen).
Validated on sm_120a from a fresh JIT cache: paged MQA suite 20/20
passed, incl. graph replay and legacy-API cross-checks.

Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
@lucifer1004

Copy link
Copy Markdown

Update: the device-side page32 support is now done — DeepGEMM-sm120 v0.1.4 derives BLOCK_KV = min(PAGE_KV, 64) in the FP8 paged MQA logits kernel (mirroring the FP4 sibling), so a 32-row page gets a 32-row compute tile with 4 KV groups at SPLIT_KV=128. #10 has re-vendored at v0.1.4 (ea3b2f8), device-only — the fork's host launcher still asserts page 64, deliberately leaving the host/API enablement and the test matrix to this PR.

Once #10 merges, your rebase shrinks to: API + launcher relaxation (block_kv ∈ {32, 64}), the num_kv_stages SMEM heuristic, and your tests — no device-header edits. Two notes for the rebase: (1) the vendored kernel's constants are renamed (PAGE_KV for the physical page; BLOCK_KV is the derived compute tile), so your two static_asserts are already covered by the vendored compile-time checks; (2) on the nv_dev lineage we needed the same num_kv_stages=2 fallback for page32 + 64 heads + paired queries — the three-stage layout misses the 99 KiB budget by exactly 4 bytes there too, so your heuristic transfers as-is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants