Repository navigation
Conversation
added 2 commits
October 22, 2026 14:31
Problem: gdn_replayssm_compact_commit_kernel ran one CTA per (64-column V tile, request, value head x layer), 590k CTAs per step at batch 128 on a 48-layer, 48-value-head GDN model. Every CTA reloaded the request scalars, ring positions and key tile, and folded the history with four chained tensor-core products (key and key residual times the high and low update parts). With a bf16 checkpoint the commit folds every step, and it took 5.40 ms per step at batch 128. Fix: one program per (request, layer, key head) covers the HV / H value heads that read that key head, in 32-column V tiles walked by one flat loop. The history is folded 16 steps at a time: the key and key-residual rows and the high and low update parts of a chunk are stacked along the reduction axis, so each chunk is one tensor-core product with fp32 accumulation, and the tile size no longer depends on the ring length. A ring of one chunk (the default 16) loads its key tile once per program; longer rings visit only the chunks that hold history. fp32 rings carry no low part. Grouping alone and stacking alone do not help (0% and 44% slower); the gain needs both. Fold and track snapshots are computed for every row and masked at the store. Slot and layer strides, null rows, the early exit and the cursor finish are unchanged. The result differs from the previous kernel by fp32 summation order only: at most one bf16 ulp, or 2.8e-6 for fp32 checkpoints with bf16 rings (up to 1.6e-4 with fp32 rings, TF32 operands). Effect: GB300, 48 layers, K = V = 128, ring 16, bf16 checkpoint, 4 history steps, every row folding: - HV 48 / H 16 (TP1): bs128 5.40 -> 3.85 ms, bs8 341 -> 253 us, bs1 39.8 -> 42.0 us. - HV 12 / H 4 (TP4): bs128 1.35 -> 0.97 ms, bs8 90 -> 72 us, bs1 11.5 -> 17.9 us. With half of the rows taking a track snapshot, bs128 TP1 goes from 10.07 to 7.37 ms. Shared memory no longer grows with the ring: at most 37 KB and no spills, where the previous kernel needed 98 KB at ring 128 and spilled with track rows. On a raised --linear-replayssm-cache-len, a 4-step fold at bs128 TP1 goes 5.8 -> 7.5 ms at ring 32, 8.5 -> 8.1 ms at ring 64 and 13.8 -> 8.2 ms at ring 128. Folding a long history there is slower (ring 64, 56 steps: 9.3 -> 33 ms); only fp32 checkpoints fold that much at once.
Problem: the head-group commit kernel replaced the per-head kernel for every ring length. It is faster on the default 16-step ring, but on a raised --linear-replayssm-cache-len it folds the history 16 steps at a time and its time grows with the ring length and the history. At batch 128 with TP1 heads it is 1.2-2.7x slower than the per-head kernel at ring 32, and 3.1-5x slower when a full ring of 64 or 128 steps folds (for example, bf16 checkpoint, head group vs per head: 34.9 vs 9.6 ms at ring 64, 65.4 vs 17.3 ms at ring 128). A nearly full ring (all but one draft window) is what a fold at capacity commits, as with fp32 checkpoints on EAGLE and MTP. Fix: keep the per-head kernel and pick the kernel on the host from the ring length. A ring of one 16-step fold chunk commits per head group, a longer ring per value head. The ring length is a static shape, so the choice needs no sync, is the same on every TP rank, and does not depend on the batch: the two kernels differ in fp32 summation order, so a batch-size rule would make a request's checkpoint depend on the batch it runs in. The circular commit tests run every case through both kernels, and a new test checks which kernel each ring length launches. Effect: GB300 kernel time, 48 layers, K = V = 128, bf16 rings with residuals, every row folding, batch 128, TP1 heads, bf16 checkpoint, before -> after: - ring 16: unchanged, 3.87 ms (per-head kernel: 5.45 ms). - ring 32: 4 history steps 7.58 -> 5.79 ms, full ring 15.8 -> 5.84 ms. - ring 64: 4 steps 8.17 -> 8.47 ms, full ring 34.9 -> 9.62 ms. - ring 128: 4 steps 8.32 -> 13.8 ms, full ring 65.4 -> 17.3 ms. fp32 checkpoints, track rows, batch 1 and 8 and TP4 heads follow the same pattern: full rings 2.4-7x faster, every history on ring 32 1.2-3.9x faster, and 4- to 16-step histories on rings 64 and 128 up to 1.7x slower. Rings longer than 16 fold short histories only when every commit folds, where the ring only has to fit one draft window, or in a track snapshot taken soon after a lazy fold. On the 16-step ring the head-group kernel is still up to 12 us slower at batch 1 (TP4 heads, fp32 checkpoint, track rows: 45.1 vs 33.5 us for the per-head kernel).
jianc99
force-pushed
the
av/commit-cta
branch
from
October 22, 2026 21:52
22f0600 to
d71f0ca
Compare
jianc99
force-pushed
the
av/replayssm-dflash
branch
from
October 23, 2026 00:16
8eeda5a to
bf3c6b7
Compare
jianc99
force-pushed
the
av/commit-cta
branch
from
October 23, 2026 00:16
d71f0ca to
b02187e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #2 (ReplaySSM for DFLASH and DSPARK); this PR's own changes are the commits after it.
Motivation
With a bf16 checkpoint, the GDN ReplaySSM speculative path folds the accepted history into the checkpoint after every verify step (
commit_gdn_replayssm_circular).The commit kernel ran one CTA per (64-column V tile, request, value head x layer). On Qwen3.8-27B that is about 590k CTAs per step at batch 128 (48 GDN layers, 48 value heads per rank at TP1). Every CTA reloaded the request scalars, the ring positions and the key tile. It then folded the history with four chained tensor-core products: key and key residual, each times the high and low update parts.
On GB300 the commit took 5.40 ms per step at batch 128.
Modifications
New launch grid. The grid is (request, layer, key head). A program handles the HV / H value heads that read its key head, in 32-column V tiles. The group size comes from the per-rank head counts in the tensor shapes (3 for Qwen3.8 at any TP), and the wrapper asserts HV % H == 0.
One product per 16-step chunk. The history is folded 16 steps at a time. The key and key-residual rows and the high and low update parts of a chunk are stacked along the reduction axis, so each chunk is one tensor-core product with fp32 accumulation.
Default ring. A ring of one chunk (the default 16 steps) loads its key tile once per program and folds each tile in one product, with at most 37 KB of shared memory and no spills.
Ring-length fallback. The host picks the kernel from the ring length (
--linear-replayssm-cache-len):On a longer ring the head-group kernel folds chunk by chunk, so its time grows with the history; the per-head kernel folds the whole ring in one product and barely depends on the history. The ring length is a static shape fixed at startup, so the choice needs no sync, is the same on every TP rank and does not depend on the batch. The tests run every case through both kernels.
fp32 rings carry no low part.
Both changes are needed. Grouping heads alone gives no speedup, and stacking alone on the old grid is 44% slower.
Fold and track products are computed for every row and masked at the store. Carrying the track accumulator through a branch on the track flag produced wrong values in the last loop iteration with software pipelining, so the kernel avoids it.
Unchanged: slot and layer strides, null rows, the early exit for rows that neither fold nor track, the history-load masking and the cursor finish kernel.
Numerics: the result differs from the previous kernel by fp32 summation order only: at most one bf16 ulp, or 2.8e-6 for fp32 checkpoints with bf16 rings (up to 1.6e-4 with fp32 rings, TF32 operands).
Grouping value heads per CTA follows SubSir's WY ring commit kernel (one CTA per request, layer and head group). Thanks to SubSir for the idea.
Accuracy Tests
test_gdn_replayssm_circular_commit.py. It checks folded and tracked checkpoints against a float64 reference of the closed form, through both commit kernels. Coverage:test_gdn_replayssm_direct_commit.pythe chain tests use ring 16 (head-group kernel) and the lazy-fold test uses ring 32 (per-head kernel).Speed Tests and Profiling
Commit kernel on GB300 (SM clock 2070 MHz): 48 layers, K = V = 128, ring 16, bf16 checkpoint, 4 history steps, every row folding.
Batch 1 is slower: +2.2 us at TP1 and +6.4 us at TP4, because there are fewer, longer programs (with an fp32 checkpoint at TP4, 16.8 -> 26.5 us). The kernel choice deliberately ignores the batch: the two kernels differ in fp32 summation order, so a batch rule would make a request's checkpoint depend on the batch it runs in, to save at most about 12 us per step (TP4 heads, fp32 checkpoint, track rows: 45.1 vs 33.5 us for the per-head kernel).
Track snapshots: with half of the rows taking a snapshot, batch 128 at TP1 goes from 10.07 to 7.37 ms.
Ring-length fallback. Both kernels on every ring, batch 128, TP1 heads, bf16 checkpoint, bf16 ring with residuals, every row folding (CUDA graphs, best of 3). The kernel that runs is in bold. Without the fallback the head-group kernel ran on every ring; with it, rings above 16 run the per-head kernel, so their commit time is the same as before this PR.
fp32 checkpoints, track rows, batches 1 and 8 and TP4 heads follow the same pattern (264 cases): on ring 32 the per-head kernel is 1.2-3.9x faster for every history, and on full rings 2.4-7x faster. A nearly full ring is what a fold at capacity commits; only fp32 checkpoints with the lazy fold (generic EAGLE/MTP) fold that much at once.
Known cost: on rings 64 and 128 with short histories (4-16 steps), the per-head kernel is up to 1.7x slower than the head-group kernel alone would be (in the table above, ring 128 with 4 steps: 13.81 ms instead of 8.32 ms; with an fp32 checkpoint up to 16.87 instead of 10.77 ms). A raised ring folds only short histories when every commit folds, where the ring only has to fit one draft window, or in a track snapshot soon after a lazy fold. The history length lives on the device, so the host cannot pick the kernel by it.
End to end, on top of the DFLASH ReplaySSM commit branch: Qwen3.8-27B, TP1, DFLASH with an 8-token draft block, bf16 checkpoint, default 16-step ring (head-group kernel), radix cache off, 128 max running requests, T=1, three runs each. These were measured with an earlier revision of this kernel that has the same ring-16 arithmetic and the same kernel time.
Decode tok/s = per-request decode rate (output tokens after the first / time after the first token), times concurrency.
Accept length is unchanged. At c128 the running cap equals the concurrency, so about one request waits in the queue; #8 shows that this setup reads 7-15% higher at that level than a larger cap, on both sides, so compare c128 only within this table.
Related upstream work