Skip to content

q8kv4 decode: opt-in PDL for the decode indexer and decode attention - #28

Open
Orien-Xu wants to merge 1 commit into
MiniMax-AI:nv_devfrom
Orien-Xu:pdl-q8kv4-decode
Open

Orien-Xu wants to merge 1 commit into
MiniMax-AI:nv_devfrom
Orien-Xu:pdl-q8kv4-decode

Conversation

@Orien-Xu

@Orien-Xu Orien-Xu commented Oct 24, 2026 •

Copy link
Copy Markdown

Hi, I'm Orien from Parasail. This fixes an issue we ran into while serving MiniMax-M3.1-Flash (#25).

Closes #25.

Change

Indexer (indexer/decode/q8kv4)

  • run(..., pdl: bool = False), plumbed through the binding, IndexerGemmArguments.pdl and
    IndexerGemmRunner::run. With pdl=True the kernel launches with
    cudaLaunchAttributeProgrammaticStreamSerialization.
  • In sm100_indexer_gemm_umma.hpp the transport warp issues the first-page TMA for history pages
    (logical < (kv_length - query_length) / kPageTokens) before griddepcontrol.wait; the other
    pages and Q load after it. All warps then wait, and the CTA calls
    griddepcontrol.launch_dependents.

Decode attention (attention/decode/q8kv4)

  • MSA_Q8KV4_DECODE_ATTN_PDL=1 (read once) adds the PDL launch attribute and sets
    params.pdl_early_trigger.
  • run_fwd_mainloop_device calls griddepcontrol.wait after barrier init, TMEM allocation and
    tensor-map prefetch, and with the early trigger calls cudaTriggerProgrammaticLaunchCompletion()
    right after setup.

Without a PDL launch griddepcontrol.wait returns immediately, so the default path differs only in
the order of the indexer's first TMA issues. Outputs are bit-identical.

Results

B200, 4-way TP serving, 8-token speculative verify, engine A/B (same GPUs, ABAB, 4 runs per arm):
chain wall per sparse layer -2.8 us at 40 verify rows (5 x 120K) and -3.6 us at 96 rows
(12 x 60K), about 1.3-1.4% of the decode step. Golden outputs identical on/off.

Tests

  • New tests/inference/msa_v1/indexer/decode/q8kv4/test_pdl.py: pdl=True vs pdl=False,
    eager and CUDA Graph, mixed lengths, bit-equal.
  • TODO before submission: rebuild and run tests/inference/msa_v1/indexer/decode/q8kv4 and
    tests/inference/msa_v1/attention/decode/q8kv4 on SM100/SM103 on this base (the patch was
    rebased from bc5f83a onto 35dc1be; the only conflict was context in the launch code), plus the
    attention suite with MSA_Q8KV4_DECODE_ATTN_PDL=1, and re-measure on current nv_dev.

GPU validation (NVIDIA B200, driver 595.71.05, CUDA 13.3, torch 2.14.1+cu130, cutlass-dsl 4.8.1, head 175038d, base nv_dev 35dc1be)

Correctness:

  • indexer/decode/q8kv4/test_pdl.py (pdl on vs off, eager + CUDA Graph, B=1/32/129): 6 passed, bit-identical.
  • MINIMAX_INFERENCE_TEST_SUITE=smoke indexer decode q8kv4: 78 passed. The 1 failure was test_api_contract pinning run()'s signature; 175038d updates it, and the rerun gives 17/17 passed for test_api_contract + test_pdl.
  • Attention decode q8kv4 smoke: 203 passed / 18 skipped with MSA_Q8KV4_DECODE_ATTN_PDL=0 and with =1.

Speed (pdl_bench.py). Per sparse layer: Hq=16, Hkv=1, index heads=1, 8 query tokens per request, top-16, page 128. Every layer has its own cold KV pool, 8 layers per CUDA Graph, median of 11 x 20 replays. 4 runs per arm in ABBA order. Wall us/layer, off -> on, with output digests (TopK + attention + projection) identical on/off for every case:

batch x ctx chain A: producer copy -> indexer -> TopK -> attention -> cuBLAS proj chain B: indexer -> TopK -> attention -> next layer
1 x 120K 29.05 -> 29.11 (+0.2%) 19.50 -> 17.98 (-7.8%)
5 x 120K 47.26 -> 46.88 (-0.8%) 36.56 -> 34.73 (-5.0%)
7 x 120K 54.72 -> 54.47 (-0.5%) 44.31 -> 42.80 (-3.4%)
12 x 120K 73.90 -> 74.04 (+0.2%) 62.02 -> 61.23 (-1.3%)
12 x 60K 56.80 -> 56.92 (+0.2%) 44.88 -> 43.90 (-2.2%)
32 x 120K 147.62 -> 146.93 (-0.5%) 135.11 -> 133.64 (-1.1%)
32 x 60K 101.30 -> 100.54 (-0.8%) 88.45 -> 86.95 (-1.7%)
64 x 60K 167.46 -> 167.11 (-0.2%) 154.14 -> 153.02 (-0.7%)

Chain B, where the neighbours are the PDL-launched MSA kernels, saves 0.8-1.8 us/layer at every batch up to 64, with no regression at large batch. In chain A the neighbours (a torch copy and a cuBLAS GEMM) trigger no early launch, so the effect is within +-0.8% (max +0.14 us).

torch-profiler per-kernel device time, us/layer (gpu/t3p/prof.jsonl, traces in gpu/t3p/traces/). With PDL, a kernel's interval starts at its early launch and includes waiting at griddepcontrol.wait, so per-kernel times grow while the wall shrinks:

chain B, batch indexer off/on TopK off/on attention off/on wall off/on
1 7.91 / 13.06 2.39 / 2.34 9.53 / 10.17 19.50 / 17.99
7 27.26 / 36.49 2.54 / 2.50 14.87 / 15.44 44.26 / 42.81
12 41.75 / 54.68 2.61 / 2.58 18.10 / 18.78 61.97 / 61.07
32 98.94 / 108.23 3.21 / 3.21 32.77 / 33.26 134.39 / 132.94

Indexer: run(..., pdl=True) launches the score kernel with programmatic
stream serialization. Before griddepcontrol.wait the kernel reads only the
plan, the page table and history K pages (pages strictly before the page that
holds the request's first query token) and issues their TMA. Q and the pages
that may hold this step's tokens load after the wait. Callers must not set
pdl when the immediately preceding kernel wrote the plan or the page table.

Decode attention: MSA_Q8KV4_DECODE_ATTN_PDL=1 adds the PDL launch attribute.
The kernel waits after its setup (barrier init, TMEM allocation, tensor-map
prefetch) and triggers dependents right after setup, so a PDL-launched
consumer of the output can launch and prefetch its weights early.

Default behavior is unchanged: without a PDL launch griddepcontrol.wait
returns immediately. Outputs are bit-identical with the switches on and off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Orien-Xu
Orien-Xu marked this pull request as ready for review October 24, 2026 00:47
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.

1 participant