perf(sparse_moe): keep two Q4 group quads in flight in the routed gate/up dot product - #199
Open
MichaelDementii wants to merge 1 commit into
Open
Conversation
…e/up dot product dot_two_rows walks the routed gate/up K dimension in quads of four Q4 groups: one quad is one 128-byte warp transaction per row, and each lane owns eight consecutive K values so a single mantissa decode feeds eight FP32 FMAs. The loop issues one quad's code words and scales and then decodes them immediately, so for the whole of that decode and its sixteen FMAs the load unit has nothing outstanding. This commit issues both quads of a pair before either is decoded. The groups are still visited in ascending order, so the order of additions into acc0 and acc1 is unchanged. 50 insertions, 23 deletions, one file. The static_assert next to the constant pins the divisibility of the full K, not of the span the loop actually walks ([k_begin/kGroupK, k_end/kGroupK)); the two coincide only because every call site passes the full range, and a comment above the assertion says so rather than leaving it to be found. The change rests on a measurement of what limits this path. Holding the kernel, the token count, the grid, the warp count and the instruction count fixed and varying only the number of distinct experts the routing touches, the mandatory weight traffic moves 3.30x while the kernel moves 1.62x: on this artifact the routed gate/up path demands 30.8% of the measured 1689.4 GB/s DRAM read ceiling on a concentrated route, 55.1% on a trace-like one and 62.9% on an independent one, and extrapolated to zero experts it still costs 16.8 us. It is not a tensor-core path either - cuobjdump -sass over sparse_moe_decode_kernels.cu.o reports zero HMMA, IMMA or OMMA. What is left is memory latency, and one quad in flight does not cover it. Claim level: operator. The change is confined to one function body; it adds no template parameter, no instantiation and no launch-shape change, and the same body is also reached by the T=1 decode path (sparse_moe_d3_nine_warp_kernel), which is covered by the same gate. Measured on one RTX 5090 (sm_120a, CUDA 13.1.115, driver 580.159.03, 525 W cap, Release, -DCMAKE_CUDA_COMPILER=/usr/local/cuda-13.1/bin/nvcc), artifact Qwen3.6-35B-A3B (groupwise-int, codecs q4-q5), base ad0f3d3. Baseline and candidate are two binaries built from the same tree and differing only by this diff; no environment variable selects behaviour. Every measurement is a separate GPU-exclusive capture with its own card-witness window, and any window with a foreign process in it was discarded and re-taken: 13 attempts on 3 cells were, twelve of them in the research series that is not submitted, and none at all in the submitted series. The timestamp that opens the window is written before the run is handed to the lock wrapper, so the window also covers the wait for the card lock; the bias is one-sided and can only reject a good cell. Kernel medians come from nsys -t cuda --cuda-graph-trace=node, not from wall clock: the bench's wall clock for this operation lands on a 2.048 us lattice and the operation's own wall median is 55.296 us at trace-like, so one division is 3.7% of it - larger than the effect being measured. Operator, ninfer_sparse_moe_bench --codec q4-q5 --tokens 4 --distribution <D> --execution graph --cache cold --warmup 3 --repeat 20 under nsys, two passes per arm with the arm order rotated between passes, both passes shown: distribution same (8 unique experts): 21472/21376 -> 20448/20352 ns -4.78% distribution trace-like (22 unique experts): 28768/28736 -> 26464/26784 ns -7.40% distribution independent(31 unique experts): 34624/34592 -> 32576/32448 ns -6.06% The depth itself was chosen by a dose rather than assumed. On a separate research build in which the depth is an environment-selected template argument, trace-like gives depth 1 (master) 28928/28832 ns, depth 2 26464/26559 ns, depth 4 27103/27039 ns: the optimum is interior. Two was then rebuilt as a constant and re-measured as the two binaries above. Two knobs that did not work are reported because they bound the claim. Shrinking the grid and running a grid-stride loop is not available here: the launch already issues exactly one CTA per unit of work (6144 = 4 tokens x 3 path blocks x 512 columns), and every narrower grid measured is worse - the same work split rewritten as a strided loop costs +2.22%, half the columns +19.72%, a quarter +65.59%. The two knobs together are worse than either alone (+53.63%). These four figures come from the same research build as the depth dose above, in which both knobs are environment-selected template arguments; none of that scaffolding is part of this commit. The same feed-depth change on the routed down kernel (sparse_moe_d4_token_kernel) is -0.15%, i.e. nothing, because its k loop has two iterations, not eight; it is not included here. Round level, same binary pair, nsys over ninfer_bench --weights qwen3_6_35b_a3b.ninfer -pg 1000,128 --prefill-chunk 8192 --spec mtp --draft-tokens 3 -r 1 --warmup 1, two arms x two passes: the routed gate/up kernel median 24416/24416 -> 23744/23744 ns (-2.75%) over 5240 launches, kernel total 130512610/130622538 -> 126452934/126528751 ns (-3.11/-3.13%). In the same traces the routed down kernel and the W8 draft instantiation of the same kernel are the controls and do not move. The operator fixture overstates this change by 2.69x: its best case is -7.40% on trace-like, the same kernel inside the real round moves -2.75%. The number that belongs in a decision is the round one. One regression, measured and reported rather than left out. dot_two_rows is shared with sparse_moe_d3_nine_warp_kernel<Q4Codec>, the T=1 decode path, and in the same two traces it gets slower: median 11136 -> 11872 ns (+6.61%) and 11200 -> 11888 ns (+6.14%), minimum 10656 -> 11392 (+6.91%) and 10560 -> 11456 (+8.48%), total over 40 launches 452610 -> 480832 (+6.24%) and 451263 -> 482238 (+6.86%). The minima move with the medians in both passes, so it is real. It costs 28222 and 30975 ns, 0.004% of the trace's kernel time and 0.7% of what the change saves on the routed path. Separating the two callers of the helper would remove it at no cost to the routed path. End to end, same binaries without a profiler, ninfer_bench -pg 1000,128 --prefill-chunk 8192 --spec mtp --draft-tokens 3 -r 4 --warmup 1, four passes with the arm order rotated: decode 414.25/413.36/413.73/413.11 -> 416.40/415.80/415.93/415.82 tok/s, mean 413.61 -> 415.99, +0.57%, with every candidate cell above every baseline cell; the spread inside an arm is 0.28% on the baseline and 0.14% on the candidate, so the baseline's own spread is half the size of the effect and it is the separation of the cells, not the spread, that is claimed. The prefill column of the same runs, which this change cannot reach because prefill dispatches different kernels, moves -0.17%, i.e. the stand's systematic works against the claim rather than for it. Speculation acceptance (0.3567567568) and round counts (248/0) are identical across all eight cells, so the two arms did the same work. An end-to-end number does not isolate an operator change; it is reported as corroboration, not as the claim. Numerics: the output is bit-identical. ninfer_sparse_moe_test with NINFER_OP_REPORT_STATS=1 over all sixteen registered cells (q4+q5 T=1,2,46,47,768,4097; q4+q6 T=1,2,46,47,768; w8+w8 T=1,2,19,20,768) produces byte-identical statistics on both binaries, and both report OK. That the oracle can move was shown with a sabotage build that rounds each decoded Q4 weight to BF16 before the FMA: it moves rel_l2 by 5.04-5.34x on exactly the four cells this path serves (q4+q5 and q4+q6 at T=2 and T=46) and leaves the other twelve byte-identical. That sabotage moves the statistic without crossing the 1.2e-2 limit, so what is demonstrated is the sensitivity of the number, not a failing verdict; this is sufficient here only because the claim is bit-identity, which is strictly stronger than passing the tolerance. Rebuilding the same honest source into a different binary reproduces all sixteen cells byte for byte, so the comparison is not a false alarm. Where that strength control does not reach: the four cells the sabotage moves are T=2 and T=46; both T=1 cells are unmoved by it. T=1 is exactly the sparse_moe_d3_nine_warp_kernel path this commit also alters. Bit-identity there is asserted by the same instrument, whose sensitivity has been demonstrated on the small-T routed path and not on the T=1 path. ctest was run in full on a clean ad0f3d3 and on this commit from the same build tree: 104 of 104 pass on both, with the same six opt-in real-artifact tests skipped on both. clang-format reports zero violations on the base file, ten on the first version of this change (all on lines it adds), and zero after reformatting; both 19.1.7 and 23.1.0 agree. The timings and the ctest run above were taken before the reformatting and carry over because the reformatted source compiles to an object whose SASS and -res-usage are identical once the translation-unit hash is normalised away. Resources, cuobjdump --dump-resource-usage over sparse_moe_decode_kernels.cu.o with the translation-unit hash normalised away and the bodies matched by demangled name with template arguments: 5 of 33 bodies change register count, all five Q4 instantiations of the paths this commit touches, and shared memory, LOCAL and STACK change on none of them. The instantiation the artifact selects at mtp3, sparse_moe_d3_path_tiled_kernel<Q4Codec, 3, false>, is 40 registers before and after. The other four go 39 -> 40, 39 -> 40, 39 -> 40 and 37 -> 39, and sparse_moe_d3_nine_warp_kernel<Q4Codec> goes 37 -> 40. At 96 threads per block, 40 registers allow 17 resident blocks per SM against the 16 the thread limit allows, so occupancy stays limited by threads and shared memory, not by registers. Every W8 instantiation and every sparse_moe_d4 instantiation is unchanged. Workspace is unchanged. Checks not run, and the limitation each leaves: the gate's sensitivity on the T=1 path is not demonstrated, only its byte-for-byte agreement; the +6% regression on sparse_moe_d3_nine_warp_kernel is reported and not fixed; no accuracy evaluation was run, which is acceptable only because the output is bit-identical; the change was measured on one artifact and one codec pair (q4-q5, with q4-q6 covered by the gate but not by the operator bench), so the effect on q4-q6 and on w8-w8 is asserted from the code path taken rather than measured; and no profiler counters were available on this machine, so every attribution above rests on ablations and on byte budgets computed from geometry, not on measured DRAM traffic - the percentages of the DRAM ceiling are demand, not a counter reading. Base and provenance. This change now targets upstream 487f897; it was written and measured on ad0f3d3, and nothing in this message has been re-measured on the new base. Two instrument changes came with the rebase. The bench flag --mtp-draft-tokens no longer exists: the spelling is --spec mtp --draft-tokens N, and --draft-tokens 0 is rejected for MTP (src/product/speculative_options.h:41), so the zero-draft arm is spelled by passing neither flag. And ninfer_bench now disables the engine context cache itself (bench/targets/qwen3_6_27b/ninfer_bench.cpp:157), where on ad0f3d3 that line did not exist and the cache stayed at its enabled default. Per-request prefix reuse was already off on both bases (same file, line 65), so no request here could take a prefix hit; how far a constructed-but-unused cache reaches into prefill and TTFT has not been measured, and these figures describe a configuration the current bench no longer builds. tests/CMakeLists.txt registers 114 targets on 487f897 rather than 104, with 20 named targets explicitly artifact-gated rather than 19; no ctest run quoted here was repeated on the new base, and no round "114 of 114" is claimed for it: ninfer_attn_input_proj_test is red on the bare base (upstream defect, issue Neroued#196), and ctest and a direct run of the same binary have been seen to disagree on this host.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
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.
Linked Issue: #NNN (
ISSUE_BODY.mdin this package), scope confirmed there.Base:
ad0f3d38. One mechanism, one function body, one file.What changes
dot_two_rowswalks the routed gate/up K dimension in quads of four Q4 groups. One quad is one128-byte warp transaction per row, and each lane owns eight consecutive K values, so a single
mantissa decode feeds eight FP32 FMAs. Today the loop issues one quad's code words and scales and
then decodes them immediately, so for the whole of that decode and its sixteen FMAs the load unit
has nothing outstanding.
This change issues both quads of a pair before either is decoded (
kQuadsInFlight = 2), sothe decode and FMA chain of the first quad cover the memory latency of the second.
The groups are still visited in ascending order, so the order of additions into
acc0andacc1is unchanged and the output is bit-identical.Diff: 50 insertions, 23 deletions, one file. No new template parameter, no new instantiation, no
launch-shape change, no workspace change.
About the
static_assert. It pinskGroups % (4 * kQuadsInFlight) == 0, i.e. thedivisibility of the full K. That is not the quantity the loop depends on: the loop walks
[k_begin / kGroupK, k_end / kGroupK), so what the pairing needs is that the span is amultiple of
4 * kQuadsInFlight. That is a property of the arguments and cannot be asserted atcompile time. The two coincide today because all four call sites pass
k_begin = 0, k_end = K(
kGroups = 32, so 32 % 8 == 0); the assertion does not enforce that, and a future caller passinga partial K range would walk past
last_groupwithout tripping it. The patch says exactly this ina comment above the assertion rather than leaving it to be discovered.
Why this and not something else
Three measurements bound the claim. All three are in the Issue; the short form:
The path is not bandwidth-bound. Holding the kernel, the token count, the grid, the warp count
and the instruction count fixed and changing only how many distinct experts the routing touches
(
--distribution same | trace-like | independentat--tokens 4= 8, 22, 31 unique experts), themandatory weight traffic moves 3.30x while the kernel moves 1.62x. At the fixture's
trace-likepoint the kernel demands 930 GB/s = 55.1 % of the measured 1689.4 GB/s DRAM readceiling. Extrapolated to zero experts it still costs 16.8 us, i.e. 59 % of its own
trace-liketime does not depend on the weights it reads at all.
The byte numerators there are computed from the routing geometry, not read from a counter;
ncuis unavailable on this machine. The comment inside the patch quotes the same 55 % and names the
same point.
It is not a tensor-core path.
cuobjdump -sassoversparse_moe_decode_kernels.cu.oreportszero
HMMA,IMMAorOMMA. The kernel is SIMTfmafthroughout, so the MMA roofline doesnot apply to it.
The grid is not the knob. The launch already issues exactly one CTA per unit of work
(6144 = 4 tokens x 3 path blocks x 512 columns), so a grid-stride form can only narrow it, and
every narrower grid measured is worse: the same work split rewritten as a strided loop costs
+2.22 %, half the columns +19.72 %, a quarter +65.59 %. Feed depth and grid together are worse
than feed depth alone (+53.63 %). These four figures, and the depth dose below, come from a
separate research build in which both knobs are environment-selected template arguments; they
are reported to bound the claim, and none of them is part of this change.
The same feed-depth change on the routed down kernel is -0.15 %, i.e. nothing, because its
k loop has two iterations rather than eight. It is not included.
Verification
Hardware and toolchain: one RTX 5090 (sm_120a), driver 580.159.03, 525 W cap, CUDA 13.1.115,
Release,
-DCMAKE_CUDA_COMPILER=/usr/local/cuda-13.1/bin/nvcc. Artifact Qwen3.6-35B-A3B(
groupwise-int), codecsq4-q5,--spec mtp --draft-tokens 3(T = 4).Method: master and change are two separate binaries built from the same tree, differing only
by this diff; no environment variable selects behaviour. Every cell is its own GPU-exclusive
capture with its own card-witness window. Kernel medians come from
nsys, not wall clock: thebench's wall clock for this operation lands on a 2.048 us lattice, and the operation's own wall
median is 55.296 us at
trace-like(47.104 atsame, 63.488 atindependent), so one latticedivision is 3.7 % of it — larger than the effect being measured.
How the witness window is taken, precisely. The timestamp that opens the window is written
before the run is handed to the lock wrapper, so the window includes any time spent waiting for
the card lock. A neighbour legitimately holding the card during that wait therefore dirties a
window that covers a clean run. The bias is one-sided — it can only reject a good cell, never
accept a bad one — and it did reject cells (below), but the method is not "the window covers the
run", and this body no longer says that it is. A later harness of mine stamps the window inside
the lock instead; that is the form to copy on any re-measurement.
Cells discarded. Thirteen attempts were discarded, on three cells:
p1_sametook fiveattempts,
p1_indepnine,k1_basetwo (raw/journal.txt, lines beginningDIRTY). Twelve ofthose thirteen are in the
p/kresearch series, whose numbers are not submitted. In thesubmitted series — the two operator passes
q1/q2and the four end-to-end passes — every cellwas accepted on the first attempt and nothing was discarded (
raw/journal.txt,raw/prodround.witness.txt).Operator
Two passes per arm, arm order rotated between passes; medians of the two passes:
sametrace-likeindependentThe depth itself was chosen by a dose rather than assumed. On a separate research build in which
the depth is an environment-selected template argument,
trace-likegives depth 1 (master)28 928 / 28 832 ns, depth 2 26 464 / 26 559 ns, depth 4 27 103 / 27 039 ns - the optimum is
interior. Two was then rebuilt as a constant and re-measured as the two binaries above.
Pass-to-pass spread inside a cell is 0.1-1.2 %.
Round
Two arms x two passes. Routed gate/up kernel median 24 416 / 24 416 -> 23 744 / 23 744
(-2.75 %) over 5240 launches; kernel total 130 512 610 / 130 622 538 -> 126 452 934 /
126 528 751 ns (-3.11 / -3.13 %). The routed down kernel and the W8 draft instantiation of the
same kernel are the in-trace controls and do not move.
The operator fixture overstates this change by 2.7x, and that is the honest headline ratio.
The fixture's best case is -7.40 % on
trace-like; the same kernel inside the real round moves-2.75 %. The fixture is a cold-cache, fixed-distribution, T = 4 stand; the round is not. Anyone
reading -7.40 % as what the model gets is reading it wrong, and the number that belongs in a
decision is -2.75 % on the kernel and the end-to-end figure below.
An unreported regression on the second consumer of the same helper.
dot_two_rowsis sharedwith
sparse_moe_d3_nine_warp_kernel<Q4Codec>, the T = 1 decode path, which this change thereforealso alters. In the same two traces it gets slower:
The minima move together with the medians in both passes, so this is real and not a tail artefact.
In the round it costs 28 222 and 30 975 ns — 0.004 % of the trace's kernel time, and 0.7 % of
what the change saves on the routed path. It does not threaten the claim. It is reported because
it is a measured regression that the body would otherwise be silent about, and because it is a
direct answer to question 3 of the Issue: the two callers of
dot_two_rowsdo want differentthings, and separating them would remove this 6 % without costing anything on the routed path.
End to end
Four passes, arm order rotated, no profiler:
+0.57 %, every change cell above every master cell. Spread inside an arm is 0.28 % on
master and 0.14 % on the change — note that master's own spread is half the size of the effect,
which is why the separation of the cells, and not the spread, is what is claimed here. The prefill
column of the same runs, which this change cannot reach because prefill dispatches different
kernels, moves -0.17 %: the stand's systematic works against this claim, not for it.
Speculation acceptance (0.3567567568) and round counts (248/0) are identical across all eight
cells, so the arms did the same work.
An end-to-end number alone would not isolate an operator change; it is reported here as
corroboration of the operator measurement above, not as the claim.
Numerics
The claim is bit-identity, not a tolerance.
ninfer_sparse_moe_testwithNINFER_OP_REPORT_STATS=1over all sixteen registered cells (q4+q5T = 1, 2, 46, 47, 768,4097;
q4+q6T = 1, 2, 46, 47, 768;w8+w8T = 1, 2, 19, 20, 768) produces byte-identicalstatistics on both arms; both report
OK sparse_moe correctness.That the oracle can move was shown with a sabotage build (not part of this change) that rounds
each decoded Q4 weight to BF16 before the FMA:
rel_l2q4+q5 T=2q4+q5 T=46q4+q6 T=2q4+q6 T=46The other twelve cells are byte-identical under the sabotage. Stated plainly: that sabotage moves
the statistic without crossing the 1.2e-2 limit, so what is demonstrated is the sensitivity of the
number, not a failing verdict. That is sufficient here only because the claim is bit-identity,
which is strictly stronger than passing the tolerance. Rebuilding the same honest source into a
different binary reproduces all sixteen cells byte for byte, so the comparison is not a false
alarm.
Where that strength control does not reach, and it matters. The four cells the sabotage
moves are
T = 2andT = 46on the two Q4 codec pairs. BothT = 1cells are unmoved by it.T = 1is exactly thesparse_moe_d3_nine_warp_kernelpath — the second consumer ofdot_two_rows, the one this change also alters and the one that regresses 6 % above. So the gatecovers that path only in the weak sense that it compares its output byte for byte; it has not been
shown able to detect damage there. The honest statement is: bit-identity on that path is asserted
by the same instrument, and the instrument's sensitivity has been demonstrated on the small-T
routed path and not on the T = 1 path.
Test suite.
ctestwas run in full on a cleanad0f3d38and on this change from the samebuild tree: 104 of 104 pass on both, with the same six opt-in real-artifact tests skipped on
both (
raw/ctest/).ninfer_sparse_moe_testis #75 in that run.Resources
cuobjdump --dump-resource-usageoversparse_moe_decode_kernels.cu.o, translation-unit hashnormalised away and bodies matched by demangled name with template arguments: 5 of 33 bodies
change register count, and shared memory,
LOCALandSTACKchange on none. Independentlyre-taken for this submission in
raw/fmt/d3_res_compare.txt.sparse_moe_d3_path_tiled_kernel<Q4Codec, 3, false>- the one selected at mtp3sparse_moe_d3_path_tiled_kernel<Q4Codec, 3, true>sparse_moe_d3_path_tiled_kernel<Q4Codec, 1, true>sparse_moe_d3_path_tiled_kernel<Q4Codec, 9, false>sparse_moe_d3_path_tiled_kernel<Q4Codec, 9, true>sparse_moe_d3_nine_warp_kernel<Q4Codec>W8Codecand everysparse_moe_d4_*instantiationLOCAL,STACK, operation workspaceAt 96 threads per block (
PathsPerBlock * 32withPathsPerBlock = 3), 40 registers allow 17resident blocks per SM against the 16 the thread limit allows, so occupancy stays limited by threads and shared memory, not by registers.
Formatting, and what was re-checked after it
clang-formaton the base file reports zero violations; on the first version of this change itreported ten, all of them on lines this change adds (195-200 and 210-213). Both
clang-format19.1.7 and 23.1.0 give the same ten. The file has been reformatted; both versionsnow report zero (
raw/fmt/cf_base_19.txt,cf_arm_19.txt,cf_final_19.txt,cf_all_23.txt,and the reformat itself in
cf_reformat.diff). Both sides of every comparison are real files intwo worktrees, per the folder's rule about reading
.clang-formatfrom disk rather than a pipe.Every timing number and the
ctestrun above were taken on the unformatted arm. They carryover because the reformatting, and the two comment edits made with it, compile to the same
machine code: recompiling the translation unit before and after gives SASS and
-res-usagethatare identical once the translation-unit hash is normalised away (
raw/fmt/object_identity_d3.txt,raw/fmt/d3_res_reformat.txt). That is a measurement on the object, not an assumption about whatwhitespace can do.
raw/fmt/hashes.txtcarries the provenance chain: basead0f3d38/ treeaf48897f; the arm thatwas measured, blob
9bef59fc/ treee23a981f— the blob hash being theindex 12a68e0c..9bef59fcvalue printed inside the earlier patch itself, and the tree matching the commit the
ctestrun wastaken on; and the submitted state after formatting, blob
831e626f/ treead958599.Affected behaviour and ownership
No public contract changes. No ownership or lifetime boundary is touched: the change is inside one
__device__helper, reads the same buffers in the same order and writes nothing new. The helper isshared with
sparse_moe_d3_nine_warp_kernel(the T = 1 decode path, 0.057 % of the trace'skernel time —
nsys's ownTime (%)column reads 0.1), which therefore also gets the change; that path is covered by the same bit-identity gate (theT = 1cells), with the sensitivity caveat stated under Numerics, and it is the path thatregresses 6 % under Round.
Checks not run, and the limitation each leaves
T = 1. Bit-identity there is asserted, not demonstrated to be detectable.sparse_moe_d3_nine_warp_kernel<Q4Codec>is reported, not fixed.Splitting the two callers would remove it; that is question 3 of the Issue.
bit-identity claim is doubted, that claim is the thing to re-check, not the accuracy.
q4-q6is covered by the gate but not by theoperator bench; the statement about it rests on it taking the same code path.
ncuis unavailable on this machine, so every attribution above restson ablations and on byte budgets computed from geometry, not on measured DRAM traffic. The
"% of the DRAM ceiling" figures are therefore demand, computed from mandatory weight bytes
and time, not a counter reading.
nsysplus an end-to-end run without aprofiler; not two independent operator instruments.
the accepted cells stand, but a re-measurement should use the stamp-inside-the-lock form.
byte-identical, which is shown rather than assumed.
DISPROVED
Kept verbatim, as the folder's rules require, with the correct statement beside each.
Correct: 40.5 % appears in no raw file of this package. It comes from the predecessor
campaign, which divides a geometry estimate of 16.72 MB (at an assumed E ~ 13) by the round
median 24 416 ns. The figure this package can support, and the one the patch's own comment
quotes, is the fixture's
trace-likepoint: 26.74 MB / 28 752 ns = 930 GB/s = 55.1 %. Thecomment in the submitted source and the body now name the same point.
Correct: the two-pass means of the raw medians are 28 752 (28 768 / 28 736) and 34 608
(34 624 / 34 592), giving 930 GB/s / 55.1 % and 1062 GB/s / 62.9 %. The
samerow, 21 424 ns, wasalready right. The conclusions (3.30x bytes against 1.62x time, 16.8 us extrapolated to zero
experts) are unchanged by the correction.
Correct: in the two attached round traces the kernel is 130 512 610 ns of 798 162 921 ns of
kernel time, 16.35 % — which is what
nsys's ownTime (%)column says (16.4). Excluding theprefill kernels present in the same trace gives 17.71 %. 18.63 % is reproducible from neither,
and the denominator behind it was never stated.
Correct: 13 attempts on 3 cells, twelve of them in the research series that is not
submitted. In the submitted series nothing was discarded at all.
Correct: 0.28 % on master and 0.14 % on the change. Master's spread is half the size of the
+0.57 % effect, which is why cell separation and not spread is what the claim rests on.
Correct: the assertion pins the divisibility of the full K, not of the span the loop walks.
They coincide only because every call site passes the full range. See "About the
static_assert"above; the patch now carries the same statement in a comment.
Correct: the ratio is 2.69 and it belongs in the body. The fixture overstates this change
by that factor.
Correct: it regresses +6.1…+6.6 % on the median and +6.2…+6.9 % on the total, in both passes,
with the minima moving the same way. It costs 0.004 % of the trace's kernel time, and it is
reported above.
Correct: 3.7 %. 2.048 / 55.296 us at
trace-like; 4.3 % atsameand 3.2 % atindependent.The wall medians are in
raw/journal.txtunder theq1/q2arms. The conclusion — that wallclock cannot resolve this effect and the verdict must come from
nsyskernel medians — isunchanged.
Correct: 0.057 % — 452 610 ns and 451 263 ns of 798 162 921 ns and 798 871 366 ns of kernel
time in the two attached traces;
nsys's ownTime (%)column reads 0.1. Excluding the prefillkernels of the same trace it is 0.061 %. The point the figure was making — that this path is
small — survives; the number did not.
Correct: 50 insertions, 23 deletions, one file — 13 of the added lines are comments: a
nine-line block explaining the pairing and a four-line one above the
static_assert, leaving 37lines of code. The compiled object is unchanged (
raw/fmt/object_identity_d3.txt).Speculation counters: how much of this number is round count, and how much is round speed
Added 2026-09-06 after a dedicated audit of every decode headline I have prepared for this
project. That audit was run with my own harness outside this repository and is not a file
of this package; it is named only to say where the section came from, and nothing below
leans on it — its own logs can be supplied on request. The section answers, from this
package's own raw, the one question a throughput figure in tokens per second cannot answer
by itself.
ninfer_benchreports decode as generated tokens over elapsed time. A change that alters thenumbers the model produces can alter draft acceptance, and with it the number of speculative
rounds spent on the same fixed output length. Such a change raises tokens per second without any
kernel running faster. The two effects separate exactly, because the bench prints the round count
itself:
where
t = decode_seconds_mean / spec_roundsis the time of one round.spec_roundsis an exactinteger printed by the engine and
decode_seconds_meanis the measured time, so the split is anidentity rather than a model.
This package: the round count is identical across both arms, so the whole figure is round speed.
Raw:
raw/q1_base.txt,q1_prod.txt,q2_base.txt,q2_prod.txt,q3_base.txt,q3_prod.txt,q4_base.txt,q4_prod.txt- all eight atmtp_k=3 proposal_head=full, and all eight printingthe same
spec accandspec round/fb: 0.3567567568 and 248/0.The end-to-end figure is taken under speculation, which is where a round-count substitution
could hide. It does not hide there: with the same round count in both arms the whole of the
end-to-end delta is the time of one round. That is what a bit-identical output requires, and it is
stated here because it was read out of the raw rather than assumed from bit-identity.
Which proposal head this was taken at. Every cell cited above ran at the product default
ProposalHead::Full(include/ninfer/types.h:82). The audit put the same question to--lm-head-draft, the configurationdocs/performance.mdpublishes in, on Qwen3.6-35B-A3B at-pg 2048,384 --prefill-chunk 8192 --spec mtp --draft-tokens 3: a bit-exact arm and the base agree thereas well - 690 rounds and acceptance 0.7819767442 in both - so the split is the same under
either head. A non-bit-exact arm measured beside them in the same ladder does move the count
(714 rounds, acceptance 0.7422969188), which is what shows the instrument would have caught a
substitution had there been one.
🤖 Generated with Claude Code