perf(ops): per-schedule activation cache policy in w8_rowsplit_gemm_mma, with ca on the BM=16 tiles - #201
Conversation
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. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 66d2eee575
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else { | ||
| const int valid = (nn < n && kk < k) ? min(8, k - kk) * 2 : 0; | ||
| ninfer::ops::cp_async_zfill<16>( | ||
| ninfer::ops::cp_async_zfill<16, ninfer::ops::Cache::cg>( |
There was a problem hiding this comment.
Requalify the feature route before changing its cache profile
For the supported W8 [5120,25600] projection at T=57..128, w8_dispatch.cpp:61-66 selects w8_feature.cu, whose launcher deliberately instantiates Full=false even at the full T=64 tile because the Full specialization regressed there. This blanket Cache::cg change also alters that deliberately retained profile, but the reported sweep only covers the unrelated large-T routes around 8192; since this is the latency-sensitive DFlash2 feature projection, remeasure its 57..128 interval and retain a schedule-specific policy if the existing winner no longer holds.
AGENTS.md reference: AGENTS.md:L91-L94
Useful? React with 👍 / 👎.
…ma, with ca on the BM=16 tiles The activation feed of w8_rowsplit_gemm_mma_kernel now reads its cache policy from the schedule rather than from a constant baked into the kernel. W8RowSplitMmaGemmSchedule gains a trailing template parameter ActivationCache_, defaulted to Cache::cg and exposed as kActivationCache, and both branches of stage_x -- the Full copy at src/ops/linear/w8/w8_rowsplit_gemm_mma.cuh:132 and the predicated cp_async_zfill at :135 -- take Cfg::kActivationCache. The weight side, codes at :159 and scales at :176, moves to ninfer::ops::Cache::cg unconditionally: no schedule anywhere wants ca there. Two schedules then ask for Cache::ca on the activation: src/ops/linear/w8/w8_feature.cu:42, where the policy is an expression of the template argument Rows, and src/ops/attn_input_proj/w8/w8_dflash2_attn_input.cu:127, where the geometry is written out. Three files, +26 / -9, verified by applying the patch to a fresh worktree of master a16b644. The earlier revision of this branch put all three sites on cg unconditionally, and that was wrong. The review bot flagged it: the ragged body is also the body that src/ops/linear/w8/w8_feature.cu:45 enters on every call, because that route hard-wires Full = false even on full tiles, and the branch swept only T around 8192. The bot was right, and it understated the cost. Measured on stand B against master, five arms per point, six passes with pass 0 dropped, median of five, each cell its own process: on the feature route, geometry [5120, 25600], the previous revision cost +12.84 % at T = 57, +16.36 % at T = 60 and +17.86 to +18.90 % at T = 64 - the two campaigns' master arms land one 2.048 us step apart there, and both readings are printed rather than reconciled. A second hard-wired route the bot did not name costs the same: the DFlash2 attention input projection at w8_dflash2_attn_input.cu:127 reads +17.86 / +17.24 / +16.67 % at T = 49 / 56 / 63, and that one is a decode route, not a prefill corner. Zero-control band over those measurements was 0.947 %; the routes below T = 56 use a different kernel entirely (w8_feature.cu:19) and read exactly +0.000 %. Splitting the change into two arms located the whole regression on one line. An arm carrying only the activation policy reproduces the previous revision to the last digit at every regressing point (+12.844 / +16.364 / +17.857 % on the feature route, +17.857 / +17.241 / +16.667 % on the attention route) while an arm carrying only the weight-side policy is zero on three of the five and +0.90 / +0.91 % at T = 57 / 60 (-3.57 / -3.45 / -3.33 % on the attention route). The same line also carries most of the branch's gain: on the alignment tooth of two of the branch's own families, the full change returns 57.6 % and 59.1 %, the activation-only arm 50.1 % and 50.0 %, and the weight-only arm 12.5 % and 13.6 %. Simply dropping the activation site would have left about a fifth of the branch, so the policy had to become conditional rather than be reverted. The discriminator is the shape of the tile, not how Full is chosen. At BM = 16 there are half as many rows per CTA, so twice as many CTAs read the same 64-column activation tile: 5120 / 16 = 320 against 160 on the feature route, 6144 / 16 = 384 against 192 on the attention route. Cache::ca leaves that tile in L1 where CTAs co-resident on one SM hit it again; Cache::cg bypasses L1 and sends every one of those reads back to L2. The falsifier is in the same file: w8_feature.cu hard-wires Full = false for both of its instantiations, and the BM = 32 one (T = 72..128) does not suffer from cg at all -- it gains -0.70 to -0.80 %. A census of the schedules that reach this kernel gives 87 instantiations across eight translation units, with first template arguments 32 (29 of them), 64 (28), 48 (13), 128 (10), 96 (5), the Rows parameter of w8_feature.cu (1) and a literal 16 (1). Two schedules have BM = 16 and both are set to ca here; the other 85 keep cg. Everything the branch claimed is preserved. On the feature route the submitted form matches the previous revision sign for sign on T = 72..128, holds -13.64 % at T = 129 and -13.59 % at T = 192, and reads exactly +0.000 % at T = 256, the Full == true control. On the attention route the +16.7...+17.9 % regression becomes a -3.33...-3.57 % gain, reproduced with zero spread in four independent measurements. On the alignment tooth the submitted form returns 57.6 % and 57.1 % against 56.2 % and 57.1 % for the previous revision in the same run, identical to three decimals in percentage points (-2.631 and -2.454 pp). One residual is not zero and is not rounded away. At T = 60 on the feature route the submitted form reads +0.909 % against master, in four independent measurements out of four. That is exactly one tick of the bench's 2.048 us timer, and it is inside the zero-control band of this campaign (0.887 %) and of the previous one (0.947 %), so the formal criterion passes. But it reproduces systematically, and it is the same +0.91 % the weight-only arm read at that point in the earlier decomposition, so it is most likely the price of moving the weight side to cg at BM = 16 rather than noise. What it costs was not profiled; no ncu run was taken and no L2 hit rate was measured. At T = 57 the single point reads +0.903 % and all three sweep windows read +0.000 %; at T = 64 three of four measurements read +0.000 % and the fourth reads +0.901 % against a zero control of +0.887 % at the same cell. Output is bit-identical, with the instrument's sensitivity shown rather than assumed. Gate on Qwen3.6-35B-A3B, four prompts of 17907 / 21423 / 24905 / 29572 characters, two chunk widths, two repeats, --greedy --seed 1234 --raw-output --max-new 96 with --max-context and --kv-capacity pinned at 12288 on every invocation: MAIN, master against this commit, 16 of 16 IDENTICAL; REFLEX, master against a copy of its own binary run separately, 16 of 16 IDENTICAL; DISCRIM, master against a sabotage build that delivers every predicated activation copy two bytes short on exactly the line this commit parameterises, 16 of 16 DIFFERENT with answer lengths moving too (463->499, 452->457, 463->466, 463->399, 464->451, 465->399 bytes); INVALID 0 in all three blocks, where an empty or sub-200-byte answer or a non-zero return code can never count as a match. The machine code says which routes moved and which did not. cuobjdump -sass over all eight translation units that instantiate this kernel, normalised for the build path and for the extra template argument in the mangled names: six of the eight are byte-for-byte identical between this commit and the previous revision of the branch, and the two that differ are exactly the two that own a BM = 16 schedule. All eight differ from master, which is the discriminating control that the comparison can see a policy change at all. Inside the two BM = 16 kernels the copy census reads six ca ZFILL copies -- the activation feed -- plus four cg ZFILL and six cg for the weight side, against sixteen ca copies on master and sixteen cg copies on the previous revision. ctest with -DBUILD_TESTING=ON registers 114 tests: 106 Passed, 7 Skipped, 1 Failed. Both changed routes are covered and their numerical tests pass: tests/ops/linear/test_w8_a16.cpp:16 sweeps t = 1..128 on geometry {5120, 25600}, covering all of T = 57..64, and tests/ops/test_attn_input_proj.cpp:509 sweeps tokens = 1..128 on the DFlash2 shape, covering all of T = 49..63. The single failure, ninfer_attn_input_proj_test, is a pre-existing defect of master, not of this change: the same test built from an untouched master worktree and run three times produces 36, 30 and 33 failing comparisons, against 18, 33 and 33 for this commit, and every failure on both sides is in the graph-replay phase of the DFlash2 case (tests/ops/test_attn_input_proj.cpp:519) while the eager path passes on both. It is filed as issue Neroued#196. clang-format finds no new violations in any of the three files; the pre-existing violations in w8_dflash2_attn_input.cu are the same set before and after the change under both versions checked (19.1.7 on the measuring host, 21.1.8 locally), so that file is already non-conforming on master. Roofline, from the bench's own columns and this repository's own references -- 1792 GB/s DRAM and the measured 1674.5 GB/s pure-read ceiling at bench/README.md:255,258, and 209.5 TFLOP/s for bf16 with an f32 accumulator at bench/ops/linear_bench.cu:48. The feature route is read-bound: at T = 64 master reads 629.9 GB/s, 35.2 % of DRAM and 37.6 % of the read ceiling, at 73.8 TFLOP/s or 35.2 % of the MMA reference. The previous revision dropped that to 529.7 GB/s, 29.6 % / 31.6 %, at 62.1 TFLOP/s; this commit reads 624.3 GB/s, 34.8 % / 37.3 %, at 73.1 TFLOP/s. Where the branch pays, at T = 129, both forms move the route from 297.0 GB/s to 343.9 GB/s, 16.6 % to 19.2 % of DRAM, and from 68.2 to 79.0 TFLOP/s, 32.6 % to 37.7 % of the MMA reference. On the attention route at T = 49 master reads 602.1 GB/s (33.6 % / 36.0 %), the previous revision 510.9 (28.5 % / 30.5 %) and this commit 624.4 (34.8 % / 37.3 %). Two statements published with the earlier revision of this branch are withdrawn rather than edited away. Its title and lede said the change "restores the L1-bypass cache policy on the ragged feed path" and that "one ragged column therefore takes the whole feed of the kernel off the L1 bypass", described as one file, +5 / -4: that is what the code did, and on two schedules it was a regression, so the policy is now a property of the schedule and not of the ragged path. Its Tradeoffs section asked "Could ca ever be right here?" and answered "For this kernel on these seven families, no", adding that the sweep "does not cover a shape where the same weight tile is re-read by many blocks and L1 would serve it". The answer is now yes, on two schedules, and the uncovered shape was real -- though the tile that is re-read turned out to be the activation tile, not the weight tile. The roofline ceilings quoted there, 1689.4 GB/s and 253.4 TFLOP/s, are not documented anywhere in this tree; the numbers above use the tree's own 1674.5 GB/s and 209.5 TFLOP/s instead. What is not measured. The end-to-end effect of any of this in a 27B or 35B round: everything here is at the operator level, and the share of these two routes in a round was not taken. The five other benchmark families of this branch were not re-swept at small T; the argument that they are unchanged is that their translation units are byte-identical to the previous revision, which is a reading of the machine code and not a measurement. The rule "BM = 16 wants ca" rests on two schedules against 85; no third BM = 16 schedule exists in the tree to test it on, and a new one would silently get the cg default. Whether ca would also win on some BM = 32 schedule was not swept. The gate exercises the policy change everywhere it changes policy, but it does not prove it entered the two ca routes: --prefill-chunk must be a multiple of 128 (src/serve/serve_options.cpp:356), so T = 49..64 can only arise in a tail chunk, and no route counter was added to check. Warm cache was not measured; only --cache cold. One card, one driver, one host, clocks not pinned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66d2eee to
ed15090
Compare
|
The Codex finding is correct, it understated the cost, and the change is now a different change: What the measurement found. The bot said the blanket The regression could not be fixed by dropping the offending line. Decomposing the previous head into arms showed the whole regression lives in one of its three sites — the activation load — and that same site carries about 85% of what the PR buys: on the two families the PR measures with, the tooth recovery falls from 57.6/59.1% to 12.5/13.6% without it. The discriminator is tile shape, not how So the policy became a schedule property — Result. The regression is gone ( The residual is not zero and is not rounded away. Two errors of mine, corrected in the body rather than dropped. The headline cell at Not measured. The end-to-end effect in a 27B round; the cause of the 🤖 Generated with Claude Code |
|
The branch was replaced after the finding above; the review on this PR still points at the previous head. Current head is @codex review |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
w8_rowsplit_gemm_mma_kernelbranches on the template parameterFull, so a ragged token countdoes not take a slow branch - it enters a separate kernel body, and that body staged every copy
it issues with a bare
cp_async_zfill<16>, whose default policy isCache::ca, while the alignedbody staged the same bytes with
cp_async<16, Cache::cg>. This revision makes the activationpolicy a property of the schedule instead of a constant of the kernel:
Cache::cgby default,Cache::caon the two schedules with aBM = 16tile. The weight side - codes and scales - goes toCache::cgunconditionally. Three files, +26 / -9, bit-identical output.Claim level: operator, and end-to-end prefill on ragged prompt lengths. It returns 39 to 64%
of what a ragged token count costs on the seven W8 operator configurations that reach this kernel,
+0.95 to +1.09 pp of end-to-end prefill throughput on four ragged lengths against -0.03 pp on
the aligned control, and -3.33 to -3.57% on the DFlash2 attention input projection at
T = 49..63. It also retracts an explanation I published with the neighbouring split of the sameFullflag.What changed in this revision, and why
The review bot filed a P1 against the previous head of this branch, and it was right. Its
finding: putting the predicated body on
cgalso movesw8_feature, a route that hard-wiresFull = falseeven on full tiles (src/ops/linear/w8/w8_feature.cu:45), and the branch's sweep onlycovered
Tnear 8192. I built the arms and measured it. The bot was right and it understated thecost: the previous head did not change the profile there, it made the operation 18.9% slower.
And a second hard-wired route it did not name, the DFlash2 attention input projection
(
src/ops/attn_input_proj/w8/w8_dflash2_attn_input.cu:127), lost 17.9% - that one is a decoderoute, not a prefill corner.
Tfeature_r16_c64,[5120, 25600]r16_c64_k128The left column is the campaign that found the regression, the right one the campaign that measured
the fix; they are separate runs on the same host and day, each compared against a master arm taken
inside its own run.
The two campaigns disagree in four cells, not one, and one of the four is the headline number.
Each took its own master arm inside its own run, and where those two medians land one 2.048 us step
apart the derived percentage moves with them:
T = 57(+12.84% then +13.75%),T = 64(+18.90%then +17.86%, master 227.360 us against 229.376 us), and
T = 100andT = 128on thefeature_r32_c64rows. Nothing in the arms changed - the regression arm reads 270.336 us at T = 64in both - so the honest statement of the worst cell is +17.86% to +18.90%, and the tables below
print whichever number its own campaign measured rather than reconciling them. An earlier revision
of this paragraph called
T = 57the only disagreement.The regression could not be fixed by dropping the offending line, because it is the same line that
carries most of the gain: on the alignment tooth of two of this branch's own families the full
change returns 57.6% and 59.1%, an activation-only arm returns 50.1% and 50.0%, and a weight-only arm
returns 12.5% and 13.6%. Reverting the activation site would have left roughly a fifth of the
branch. So the policy became conditional instead.
Base
Base
origin/master@a16b6442, re-read 2026-09-07 withgit ls-remote https://github.com/Neroued/ninfer.git refs/heads/master.a16b6442is one commitpast
487f8977, the base of the previous edition of this report, and that commit isdocumentation only -
git diff --stat 487f8977 a16b6442 -- src include tests apps benchisempty. Every
file:linein this report was re-read ona16b6442.Provenance, said once. Three groups of numbers, on three bases, and they are labelled where they
appear.
a16b6442, stand B, 2026-09-07. Everything under The regression, Mechanism,Effect of this revision, Machine code, Bit-exactness gate,
ctest. 984 measured cells inthe second campaign, 1194 in the first, zero rejected in either.
ad0f3d38, host R. The seven-family operator table and theend-to-end tables under What the branch claims. No timing under that heading was re-taken,
and it is not the submitted code. What carries it is Machine code below: six of the eight
translation units that instantiate this kernel are byte-identical between this revision and
that one, and none of the seven benchmark configurations dispatches into either of the two that
differ (they hold the only
BM = 16schedules in the tree).a140e7ae, host C. The instruction and register counts under Change and the nulling probeunder Coverage. The suite had 104 tests there; it has 114 on
a16b6442.Scope
One mechanism, three files, +26 / -9, verified by applying the patch to a fresh worktree of
a16b6442with a plaingit apply(git diff --numstat: 5/1, 8/1, 13/7). One new templateparameter with a default, so no call site outside the two that opt in changes behaviour, and no new
instantiation appears for the other 85 schedules. No signature change on the kernel, no new branch,
no new launch, no workspace.
"Seven" throughout counts operator benchmark configurations (
attn_input_projin two formats,linear_addat twok), mapping onto the six registered tests named under Coverage; "family"means a configuration. The two routes this revision sets to
caare not among those seven.Environment
sm_120a, driver 580.159.03, CUDA 13.1, 525 W, Release,-DCMAKE_CUDA_ARCHITECTURES=120a. Clocks not pinned (pinning is not permitted on this host); underload 2542-2662 MHz, median 2557, falling to 195 MHz between cells. All numbers on
a16b6442.Four arms per point:
A= master,B= the previous head66d2eee5,N= this revision,Z= a second independent build of master.AandZare the same tree built twice and theirbinaries differ in md5, so
Zis another build of the same code and not a copy of a file.Protocol: ABAB with the arm order reversed on odd passes, 6 passes, pass 0 discarded, median of 5;
every cell its own process;
--warmup 5 --repeat 30, cold cache, 256 MiB L2 eviction buffer; oneexclusive card grab for the campaign, with the witness sampling the card's process list started
after the lock is taken and stopped before it is released. Zero foreign processes in any of the
984 cells (312 main, 270 sweep windows, 192 attention, 162 attention windows, 48 tooth); nothing
rejected. The first campaign, which found the regression, was 1194 cells under the same protocol
with five arms, also zero rejected.
ad0f3d38. Only the tables underWhat the branch claims; 60 clean cells, none rejected.
a140e7ae. Instruction and register counts, nullingprobe.
Resolution. The operator bench reports medians in steps of 2.048 us. On the feature route at
T = 57(223.2 us) one step is 0.92%; on the attention route atT = 49(57.3 us) it is3.57%. Nothing below one step is resolvable, and the report says so wherever an effect is one
step wide.
Zero-control band.
|Z - A|in this campaign: main table max 0.008%, sweep windows max0.887%, attention 0.000%, attention windows 0.000%, tooth +0.214 pp; median 0.000
everywhere. The single outlying cell is
T = 64in the widest sweep window and is exactly one timerstep. The first campaign gave 0.947%. The rule used throughout: anything under 0.95% is treated as
zero, and anything that survives it is named.
The regression the review found
Stand B,
a16b6442, five arms.Amaster,Bthe previous head,X= master plus the activationpolicy only,
W= master plus the weight policy only,Zthe zero control. The source ofXwiththe
Wpatch applied was checked to be byte-identical toBbefore building.Where it lost
bench/ops/linear_bench.cu --qtype w8 --n 5120 --k 25600, medians of five passes, us:Tw8_dispatch.cpp:61-66)feature_r16_c64feature_r16_c64feature_r16_c64feature_r32_c64mma_r64_c128,Full=falsemma_r64_c128,Full=trueRe-measured in three further sweep windows (
T +/- 4,+/- 8,+/- 12, each its own process): fourindependent measurements agree on sign and magnitude at
T = 57..64and atT = 129.T <= 56uses a different kernel entirely (w8_feature.cu:19->w8_small_t_mma.cuh) and readsexactly +0.000% at
T = 16 / 32 / 48 / 56.bench/ops/attn_input_proj_bench.cu --format w8-dflash2-qkv, medians of five passes, us:Tsmall_t, other kernelr16_c64_k128r16_c64_k128r16_c64_k128r32_c32_k128,Full=truer32_c64*The whole regression is one line, and it is also most of the gain
Tfeature_r16_c64feature_r16_c64feature_r16_c64mma_r64_c128r16_c64_k128r16_c64_k128Arm
Xalone reproducesBto the last digit at every regressing point. And the same line carriesmost of what the branch is worth: on the alignment tooth (
t(8191)/t(8192) - 1, both points insideone process) of two of this branch's own families,
attn_input_projw8-qgkvattn_input_projw8-qkvSo reverting the activation site would have left about a fifth of the branch. The fix had to make
the policy conditional.
Mechanism: the discriminator is the tile, not how
Fullis chosenAt
BM = 16each CTA owns half as many rows, so twice as many CTAs read the same 64-columnactivation tile:
5120 / 16 = 320against 160 on the feature route,6144 / 16 = 384against 192on the attention route.
Cache::caleaves that tile in L1, where CTAs co-resident on one SM hit itagain;
Cache::cgbypasses L1 and sends every one of those reads back to L2. AtBM >= 32thebalance is the other way and
cgis neutral or better.This is falsifiable and it was tested. The obvious alternative rule - "
cawhereFullishard-wired,
cgwhere it is chosen by alignment" - is refuted inside one file:w8_feature.cu:45hard-wiresFull = falsefor both of its instantiations,launch<16>at:59and
launch<32>at:63, and only theBM = 16one suffers. TheBM = 32one (T = 72..128)gains -0.70 to -0.80% from
cgand is left oncghere.How many schedules this rule touches.
git grep -h -o "W8RowSplitMmaGemmSchedule<[^>]*" -- src/on
a16b6442gives 87 instantiations in eight translation units. By first template argument(
BM):BMRowsThe last two are the two this commit touches:
w8_feature.cu:42withRows == 16, andw8_dflash2_attn_input.cu:127. The other 85 keep thecgdefault. Selecting bygit grep "W8RowSplitMmaGemmSchedule<16"would be wrong twice over - it matches16in theWM/WNpositions (e.g.
<32, 64, 16, 16, 3, 2, 128, 1>atw8_dflash2_attn_input.cu:141) and missesw8_feature.cu, whoseBMarrives as a template parameter.Change
Three files. The policy becomes a schedule parameter with a
cgdefault, which is the idiom fivesibling headers in this tree already use:
Both branches of
stage_xthen read it - theFullcopy at:132and the predicatedcp_async_zfillat:135- exactly asbf16_gemm_mma.cuh:208,212does. TheFullbranch wasalready on
cg, so for it this is a rename, not a policy change. Weight codes (:159) and weightscales (
:176) takeninfer::ops::Cache::cgoutright: no schedule wantscathere, and onr16_c64_k128the weight side is a gain.The member is spelled
kActivationCachewhile the rest of the struct is upper-case. That isdeliberate:
git grep kActivationCachethen finds this schedule alongside the others carrying the same knob -bf16_gemm_mma.cuh:49,fp8_a16_gemm_mma.cuh:34,fp8_a8_mma.cuh:49,q4_rowsplit_gemm_mma.cuh:50,q5_rowsplit_gemm_mma.cuh:65and this one.Then the two opt-ins. On the feature route
BMarrives as the template parameterRows, so the ruleis an expression of it, kept on one line at the point where
RowsbecomesBM:Named weakness of that form: the rule is written on
Rowswhile the thing that decides isBM.Here they are the same -
Rowsis the schedule's first argument - but the link is a convention, nota compiler check, and
Rows == 16would keep compiling and silently pick the wrong policy if thatever stopped holding. Checking
Schedule::BMis not possible at that point, the schedule not yetbeing built. A stricter shape would be a
constexprhelper next to the schedule itself, called fromboth routes; it is not done here to keep the change minimal, and I will take it if you prefer it.
On the attention route the geometry is written out, so
Cache::cagoes straight into the schedule atw8_dflash2_attn_input.cu:127. Both sites carry a comment giving the CTA counts above, so the nextreader sees the reason rather than the policy.
From the ISA.
cp.async.ca.shared.globalandcp.async.cg.shared.globalboth transfersrc-sizebytes and both zero-fill up tocp-size = 16; the difference is the cache level the copysettles in. A cache does not change values. All the sites are already
<16>, satisfying thestatic_assertatmemory.cuh:39and:56that thecgform requires a 16-byte copy.From the binary (Host C,
a140e7ae, the previous head).LDGSTStotals were unchanged at 2372in both arms across 158 instantiations of this kernel,
0 of 158differing in issue count, andcuobjdump -res-usagediffered in0of all 3135 bodies onREG/STACK/SHARED/LOCAL.Within one arm the 79 pairs of instantiations differing only in
Fullagree on issue count79 of 79, so raggedness never cost an extra feed instruction; it changed the form of every existing one.This revision adds a template parameter with a default, which changes mangled names but instantiates
no new kernel - see the per-unit
IDENTICALcolumn under Machine code.Effect of this revision
Feature route,
[5120, 25600]Medians of five passes, us. Stand B,
a16b6442.Tfeature_r16_c64feature_r16_c64feature_r16_c64feature_r32_c64feature_r32_c64feature_r32_c64feature_r32_c64feature_r32_c64feature_r32_c64feature_r32_c64mma_r64_c128,Full=falsemma_r64_c128,Full=falsemma_r64_c128,Full=trueT = 256is the real control here:256 % 128 == 0, soFull = trueand neither form is on theexecuted path. It reads +0.000%.
T = 129andT = 192are not controls -Fullisfalsethere too, and they are the points with the largest gain.
One difference between the two campaigns, both readings kept.
B - AatT = 57was+12.84% in the first campaign (
B = 251.904) and +13.75% here (B = 253.920); the per-passvalues in this run are 251.936, 251.936, 253.920, 253.952, 253.952, so the median crossed one timer
step. The same happened at
T = 128, where the first campaign readB - A = -0.70%and this onereads
+0.00%. Both are right for their own run, andNis compared against theBof the samerun throughout.
The residual at
T = 60is not zeroN - AatT = 60reads +0.909% in the single point and in all three sweep windows - fourindependent measurements out of four. In the main run all five per-pass medians of
Nread227.328 us against a master median of 225.280; across the three windows exactly one pass of fifteen
falls below that.
TZ-A = +0.887in the same cell)That value is exactly one 2.048 us step (225.280 -> 227.328), and it sits inside the zero-control
band of both campaigns (0.887% here, 0.947% before), so the formal criterion "inside the band"
passes. It is nevertheless not noise. It is the same +0.91% that the weight-only arm
Wread atthat point in the decomposition above, so the most likely reading is that it is the price of moving
the weight side to
cgatBM = 16. I did not establish that: noncuprofile was taken and noL2 traffic was measured. At
T = 57the single point shows one step and all three windows showzero; at
T = 64three of four measurements show zero and the fourth shows one step against a zerocontrol that moved by the same step. Below the timer's step this instrument cannot separate a real
+0.9% from zero, and another window will not help - a finer timer would.
DFlash2 attention input,
T = 49..63: a regression became a gainTsmall_t, other kernelr16_c64_k128r16_c64_k128r16_c64_k128r32_c32_k128,Full=truer32_c64_k128r32_c64_k128,Full=truer32_c64,Full=trueThree further windows (the attention bench takes a token list, not a
--sweep) give the same-3.571 / -3.448 / -3.333% in all nine cells, with the zero control at 0.000% in all nine. Each of
those is one timer step wide, but it is one step repeated in four independent measurements with zero
spread, and it reproduces the weight-only arm
Wof the first campaign to three decimals. Thisgain does not exist on the previous head; it appears only because the weight side goes to
cgwhilethe activation side goes back to
ca.The tooth this branch is measured on is unchanged
Same estimator as the branch's own tables,
t(8191)/t(8192) - 1inside one process:attn_input_projw8-qgkvattn_input_projw8-qkvIdentical to three decimals in percentage points, which is what Machine code predicts: both
families dispatch through
w8_attn_input_gemm_mma.cu, whose schedules are allBMin{32, 64, 128}, and that translation unit compiles byte-identically in the two forms. The share
column is a median of per-pass shares whose per-pass spread is 54.5-60.6% and 54.6-61.8%, so the
57.6 / 59.1% quoted from the first campaign and the 56.2 / 57.1% here are the same number seen twice;
Nis compared with theBof its own run.Roofline
Ceilings taken from this repository rather than from me: 1792 GB/s DRAM and the measured
1674.5 GB/s pure-read ceiling of
tools/hbm_bandwidth_probe.cu, both atbench/README.md:255and
:258, and 209.5 TFLOP/s for bf16 with an f32 accumulator,kRtx5090Bf16Fp32AccumulateTFLOPsatbench/ops/linear_bench.cu:48.us,GB/sandTFLOP/sarethe bench's own columns; the two percentage columns and the MMA column are those divided by those
constants.
T = 57T = 57T = 57T = 64T = 64T = 64T = 129T = 129T = 129The previous head took 5.6 points of read-ceiling share off this route at
T = 64; this revisiontakes 0.3 and adds 2.6 at
T = 129. On the attention route atT = 49, GB/s derived from thebench's
logical_bytes = 34527232and its median: A 602.1 GB/s = 33.6% DRAM / 36.0% of the readceiling; B 510.9 = 28.5% / 30.5%; N 624.4 = 34.8% / 37.3%.
Machine code: which routes moved
cuobjdump -sassover all eight translation units instantiating this kernel, normalised for thebuild path and for the extra template argument in the mangled names.
ops/linear/w8/w8_feature.cuops/attn_input_proj/w8/w8_dflash2_attn_input.cuops/linear/w8/w8_rowsplit_gemm_mma.cuops/attn_input_proj/w8/w8_attn_input_gemm_mma.cuops/gdn_input_proj/w8/w8_gdn_input_gemm_mma.cuops/linear_add/w8/w8_linear_add_gemm_mma.cuops/linear_pair/w8/w8_pair_gemm_concat.cuops/linear_swiglu/w8/w8_linear_swiglu_gemm_mma.cuSix of eight are byte-identical to the previous head, and the two that differ are exactly the two
holding a
BM = 16schedule. This is the direct answer to "are the branch's other numbers still thesame numbers": on six of eight units the instructions are literally the same bytes. The
A vs Bcolumn is the discriminating control - the comparison does see a cache-policy change when there is
one.
Copy census inside the kernels (
LDGSTS.E.BYPASSiscg, withoutBYPASSisca;ZFILLis thepredicated copy):
w8_feature,BM = 32caZFILL + 6cacgZFILL + 6cgcgZFILL + 6cgw8_feature,BM = 16caZFILL + 6cacgZFILL + 6cgcaZFILL + 4cgZFILL + 6cgBM = 32(three kernels)cacgcgBM = 16caZFILL + 6cacgZFILL + 6cgcaZFILL + 4cgZFILL + 6cgBM = 64cacgcgSix copies stay on
cain bothBM = 16kernels - the activation feed - and all ten weight-sidecopies go to
cg. That is the intent, read off the instructions rather than off the source.What the branch claims
Provenance: every number in this section was taken on host R, base
ad0f3d38, on the previous headof this branch. None of it was re-taken. It carries to the submitted code because the seven
families dispatch only into translation units that compile byte-identically in the two forms (table
above), and none of them reaches either
BM = 16schedule:w8_attn_input_gemm_mma.cuholdsBMin {32, 64, 128},gdn_input_projonly 64,linear_swiglu{32, 64, 128},linear_add{32, 48, 64, 128},
linear_pair{32, 48, 64, 96, 128}. That is an argument from the machine code,not a measurement; it is listed again under What is not settled.
Operator
Three passes per arm, arm order rotated, point order reversed in pass 1; 30 of 30 cells clean on the
first attempt. Nothing compared in absolute time across arms: inside one pass of one binary
tooth(T) = t(T)/t(8192) - 1, an arm's effect is the per-pass difference against stock, and thefigure is the median of three.
attn_input_projw8-qgkvattn_input_projw8-qkvgdn_input_projw8linear_swigluw8linear_addw8, k=4096linear_addw8, k=6144linear_pairw8Two zero controls: at
T = 8192the untouchedFull == truebody runs in every arm and absolutet(8192)spans 0.00 to 0.42% across all arms and families; a second independent build of theidentical base source reads at worst 0.56 pp, exactly one 2.048 us tick on the shortest family.
The share column is a median of per-pass shares, not the effect divided by the tooth; dividing across
the rows would give 59.4 / 63.5 / 53.6 / 40.7 / 47.0 / 58.1 / 60.0%.
The effect is a step, not a share. It is flat on 28 cells of 28 while the number of cancelled
issues varies 96x between
T = 8191andT = 8096, and within 0.10 pp of zero at the alignedlength. A mechanism proportional to the remainder cannot produce that. The
spancolumn refutes theproportionality hypothesis from the stock binary alone: 96x more cancelled issues moves the tooth by
0.19 to 0.57 pp, one or two timer ticks on the two widest families. The price is proportional to
the issues executed in the predicated form, all 1186 of them whenever the columns are ragged.
End to end
ninfer_bench, Qwen3.6-35B-A3B,-r 5 --warmup 2 --prefill-chunk 12288 --max-ctx 12288,proposal_head=full,mtp_draft_tokens=0. The aligned referenceT = 8192is re-read between everytwo ragged points inside a pass; three passes per arm, arms alternated, one exclusive card grab and
one witness verdict per cell.
No reference reading was discarded, out of 18 across six cells, by a rule fixed before any effect was
looked at. The effect is flat while the penalty it comes out of falls from 2.25% to 1.01% - the same
signature as the operator table - and the aligned control reads -0.03 pp with both arms running the
same code, so that is the instrument's floor rather than an effect.
Where it does nothing. Aligned token counts: both arms compile to the same instantiation. The
canonical fixture
-pg 2048,384is aligned (2048 mod 128 == 0), so this change is not on theexecuted path there; measured anyway it reads -0.087% prefill against -0.041% for a second
build of the identical base whose own passes span -0.224 to +0.155%. Speculation counters are
identical between arms to the last digit under both proposal heads -
spec_rounds678 and acceptance0.7994100295 with the product default
ProposalHead::Full(include/ninfer/types.h:82), 690 and0.7819767442 with
--lm-head-draft- which is what a bit-exact change must do.Observation: the omission this branch started from
Fullis a template parameter (w8_rowsplit_gemm_mma.cuh:65) consumed byif constexpr, so aragged call enters a different kernel body, compiled separately. Three consumers feed it:
stage_xactivations (131-138),stage_wweight codes (153-161) and weight scales (168-177)used
cp_async<16, Cache::cg>underFulland a barecp_async_zfill<16>otherwise. The policy isa property of the instantiated body, so every copy the kernel issues - including the 63 fully
valid tiles of 64 - changed cache level the moment the token count stopped being a multiple of
BN.That is a different cost from predication: predication is about which issues get cancelled, this is
about which form all of them execute in.
Counted on
a16b6442withgit grep -n "cp_async_zfill<" -- src include apps bench tests, excludingthe header that defines it: 39 sites in 22 files; eighteen files pass a policy at every site,
three at none, one in part.
sparse_moe/prefill/*,softmax_attention/*(3),gdn_gating_proj/bf16/*,dynamic_grouped_conv/bf16/*Cache::cglinear/bf16/*(2),linear/fp8/*(3),linear/w8/w8_small_t_mma.cuh(2),linear_topk/w8.cuSchedule::kActivationCachecontext_kv_materialize/*,linear_topk/*(the other four)Cache::ca, chosenlinear/nvfp4/nvfp4_w4a4_mma.cuh:96yes; activation scales:111and:130, the two branches of oneif constexpr, nolinear/q4/q4_small_t_mma.cuh:114)linear/w8/w8_rowsplit_gemm_mma.cuhlinear_pair/w8/w8_pair_gemm_mma.cuhcp_asyncwithout zfill is called withCache::cgin 50 further places (51 matches, one of themthe
gemm_cp_asyncwrapper atrowsplit_mma.cuh:52). This commit fixes the first of the threeno-policy files, and moves it into the second row of that table rather than the first: the
activation reaches the policy through the schedule, the weight side spells
Cache::cgout.The default itself must not be changed. Flipping it in
memory.cuh:35/:50does not compile:cgrequires a 16-byte copy (static_assertat:39,:56) andcp_asyncis instantiated at 4 and8 bytes in
rowsplit_grouped_mma.cuh:163,199. The correct shape is the one adopted here, where thepolicy is a schedule property and the width is checked where used.
Correctness evidence
Bit-exactness gate
Stand B,
a16b6442. Four prompts of 17907 / 21423 / 24905 / 29572 characters,--greedy --seed 1234 --raw-output --max-new 96, both capacities pinned on every invocation(
--max-context 12288 --kv-capacity 12288) so no arm can differ through a capacity derived from freeVRAM, two chunk widths (12288 and 4096), two repeats, ABAB order. A non-zero return code or an answer
under 200 bytes counts as
INVALID, never as a match. Card empty at the start(
foreign_apps=[none], 2 MiB occupied),NINFER_*variables in the environment: 0. Every cell is afresh single-shot CLI process, and the CLI disables the context cache unconditionally
(
apps/cli/main.cpp:281-283).REFLEX, master against a copy of its own binary in another directory - two separate runs, not a file compared with itselfMAIN, master against this commitDISCRIM, master against the saboteurINVALIDin all three blocksThe strength control is the point of the exercise, and it is a separate build with a deliberate
fault on exactly the line this commit parameterises, not a mode switch:
min(8, k - kk) * 2becomes
* 2 - 2in the activation feed, so every predicated copy arrives two bytes short. It isinert while
Full == trueand corrupts numbers as soon as a column is ragged - a literal model ofhow this change could have gone wrong if the
cgform treatedsrc-sizedifferently fromca. Itcomes out DIFFERENT on all sixteen cells and moves answer lengths as well: 463->499, 452->457,
463->466, 463->399, 464->451, 465->399 bytes. Without that block, "16 of 16 IDENTICAL" would say
nothing.
What the gate does not cover. The two routes this commit sets to
calive atT = 49..63andT = 57..64, while--prefill-chunkmust be a multiple of 128 (src/serve/serve_options.cpp:356),so those windows can only be entered by a tail chunk of a prompt, and only at the right length.
Whether they were entered in these 96 runs was not checked - no route counter was added. What the
gate does prove is bit-exactness everywhere the policy actually changes: the weight side on every
route, and the activation on every schedule with
BM >= 32. For the twoBM = 16routes theevidence is the copy census under Machine code (their activation feed carries the same
cacopiesas master) and
ctestbelow, which checks them against an oracle at everyTfrom 1 to 128.ctest-DBUILD_TESTING=ON(off by default atCMakeLists.txt:19, andcteston an empty set returnszero after printing "No tests were found", so everything below is a count and not a return code).
The seven skips are marked
***Skippedbyctestitself - the tests that need real weight artifacts(
*_real_test,*_load_plan_test).Both changed routes are covered and both numerical tests pass.
tests/ops/linear/test_w8_a16.cpp:16runsfor (int t = 1; t <= 128; ++t)on geometry{5120, 25600}(:18,:26), so the whole offeature_r16_c64'sT = 57..64is compared againstan oracle; that test passes.
tests/ops/test_attn_input_proj.cpp:509runsfor (int tokens = 1; tokens <= 128; ++tokens)on the DFlash2 shape (:514), covering the whole ofr16_c64_k128'sT = 49..63, and its eager path passes.The single failure is a defect of master, and here are the numbers.
ninfer_attn_input_proj_testfails on the graph-replay block. Built from an untouched master worktree and run three times
under the same card lock as this branch's binary, run three times:
a16b6442The count moves from run to run on both arms - the test is flaky - and every failure on both
sides is tagged
graph phase=1, i.e. thereplay = truecase attests/ops/test_attn_input_proj.cpp:519; the eager variant passes on both. The token counts on whichit fails are the same set on both arms in the retained logs (49, 53, 54, 63, 64, 65, 96, 97; the
harness keeps the first 20 failure lines of each run, and the replay case is registered at
:518forsixteen token counts). It is filed as issue #196. The statement to check on this base is
therefore "the arm matches the base, and the one failure reproduces on bare master" - not "the suite
is green".
clang-formatTwo versions, both on
a16b6442and ona16b6442plus this change:src/ops/linear/w8/w8_rowsplit_gemm_mma.cuhsrc/ops/linear/w8/w8_feature.cusrc/ops/attn_input_proj/w8/w8_dflash2_attn_input.cuw8_dflash2_attn_input.cuis already non-conforming on bare master under both versions checked,and the violating set is identical before and after this change. One violation the change did
introduce was fixed: adding
kActivationCacheshifted an alignment group andSCALE_CACHE_BYTESneeded one more space. Which version upstream formats with was not established; that seven or
seventeen lines are already flagged on untouched master says it is neither of these.
Coverage: the tests do reach the changed lines
On
a140e7ae, a nulling probe - the submitted tree with the payload of exactly those copies set tozero - failed exactly six of the registered tests and nothing else:
ninfer_linear_w8_a16_test,ninfer_attn_input_proj_test,ninfer_gdn_input_proj_test,ninfer_linear_add_w8_a16_test,ninfer_linear_pair_w8_a16_test,ninfer_linear_swiglu_w8_a16_testMatch on names: the ordinals from
a140e7aedo not carry to this base. The probe branch is notsubmitted, and the probe was not re-run against this revision.
Corrected and superseded
Nothing that has been published on this branch is deleted silently. The previous wordings stand here
next to the corrected ones.
1. The title and the mechanism. Previously:
"perf(ops): restore the L1-bypass cache policy on the ragged feed path of w8_rowsplit_gemm_mma",
and "One ragged column therefore takes the whole feed of the kernel off the L1 bypass. This commit
adds the missing template argument at those three sites: one file, +5 / -4, bit-identical output by
construction."
Now: the L1 bypass is right for the weight side everywhere and for the activation side on 85
schedules of 87, and wrong for the activation side on the two with
BM = 16. The policy is aproperty of the schedule, not of the ragged path. Three files, +26 / -9.
2. Whether
cacould ever be right here. Previously, under Tradeoffs:"It moves data off L1 for a body that previously used it. Could
caever be right here? For thiskernel on these seven families, no: 28 cells of 28 improve and the aligned control does not move. The
sweep does not cover a shape where the same weight tile is re-read by many blocks and L1 would serve
it; it is one artifact's production shapes at four ragged lengths."
Now: the answer is yes, on two schedules, at a cost of up to +18.9%. The named gap was real
and the review found it. The one detail the old paragraph had backwards: what many blocks re-read at
BM = 16is the activation tile, not the weight tile.3. That the branch does not move the routes below
T = 192. Previously, under Base:"the route is unmoved: every edit to
w8_dispatch.cppbetween the bases is guarded byt <= 192ortighter ... and at
T = 8096...8192every geometry still returnslaunch_w8_mma_r64_c128."That statement was about the dispatch table moving between bases, and it is still true. But it
was doing duty as an argument that nothing below
T = 192mattered, and that was wrong: the changereached
t <= 64andt <= 128onw8_dispatch.cpp:64-65and cost 12.8 to 18.9% there.4. Roofline ceilings. Previously: "Ceilings are this project's own measurements on this card:
1689.4 GB/s for reads, 253.4 TFLOP/s for
mma.syncbf16 with an f32 accumulator", givinggdn_input_proj w8at 83.6% of the MMA ceiling on 7.96% of the read ceiling atT = 8192.Neither denominator is documented in this tree, and I cannot point at where they were measured.
The numbers in this revision use the tree's own references instead - 1674.5 GB/s
(
bench/README.md:258) and 209.5 TFLOP/s (bench/ops/linear_bench.cu:48). Recomputed against thefirst of those, the
gdn_input_projaligned point reads 8.03% of the read ceiling; its211.9 TFLOP/s is above the 209.5 reference, so for that operator I quote the level and not a
share of a ceiling.
5. Host P, superseded earlier and repeated here so it is not lost. The first edition of the
operator table came from a host with a foreign process on the card and per-run inputs that had not
been kept. Everything was re-taken on host R, and the re-take moved the headline against this
change: operator share of the tooth 43-65% -> 39-64%; end to end +1.12 / +1.27 / +1.09 / +1.12 ->
+0.95 to +1.09 pp; the aligned control -0.18 pp, then reported as an unexplained systematic tail, ->
-0.03 pp, so that sign did not survive. Nothing from host P is quoted as a result.
What this retracts elsewhere
The neighbouring change - splitting
FullintoFullCols/FullRowsso a ragged token count stopspredicating the weight feed - explained its gain as the removal of a predication link on the weight
feed. That explanation is wrong, and this measurement is what shows it. An arm restoring the
cache policy on the weight feed and nothing else - no predication removed, no flag changed, no
instruction added - is worth +0.28 to +1.34 pp on the seven families at
T = 8191, against the+0.58 to +1.78 pp the split measures on the same families at the same point by the same
estimator, from the split's own report. They coincide. The split was winning because taking the
weight feed to the fast body incidentally gave the weights back
Cache::cg. The split's numbersstand; only its account of the mechanism is withdrawn.
Checkable inside this package: if what the split leaves is exactly the activation term, then
activation-onlymust equalbothminusweights-only. Five of seven families agree within0.38 pp;
linear_add k=6144(1.13) andlinear_pair(0.56) do not, so the two policy terms are notstrictly additive there and the whole exceeds the sum of its parts.
For merge order: this change and the
Fullsplit are expected not to be additive. The splitmoves both weight branches to
if constexpr (FullRows), so when only the columns are ragged - thecase this change was written for - two of the three sites already take the fast
cgpath and onlythe activation site still runs. If the split lands first, the weight-side term is likely already
taken while the activation-side term is not, so the expected total is nearer the maximum of the two
than their sum. That is read off the two diffs; the arm that would settle it does not exist.
Tradeoffs
do not opt in produce the same instantiations, and six of the eight translation units compile to
the same bytes as the previous head.
measured, and the falsifier is in the tree: at
BM = 32in the same file, under the samehard-wired
Full = false,cggains 0.70 to 0.80%.kActivationCachedefaults tocg; a futureBM = 16schedulewould get
cgand would have to be switched by hand. There is no thirdBM = 16schedule in thetree to check the rule against.
cgform treatedsrc-sizedifferently fromcathe change would be silently wrong on ragged tiles only - exactly where no default bench looks.
Hence a saboteur attacking the byte count on that path, and gate prompts long and ragged rather
than the repository's 70-token fixtures.
The second model.
w8_featureis aQwen3.6-27Broute -dflash2/feature_projection, bound atsrc/targets/qwen3_6_27b/impl/load/bindings.cpp:400and materialised at5120 x 25600at:654,which is exactly the dispatch case at
w8_dispatch.cpp:61-66. So the two routes this revisionprotects are 27B routes, and the branch's measured gain is a 35B prefill effect. No end-to-end 27B
measurement was taken, on either side of this: not for the regression the review found and not for
its removal.
Reproduction
Every cell file opens with a state block: md5 of every binary taking part,
HEADand dirty-filecount of its tree, count of
NINFER_*markers in the environment (0), the card's occupancy, clockand power cap, and foreign compute processes. Each cell is one exclusive grab of the card under
flock, and the witness that samples the card's process list starts after the lock is taken andstops before it is released, so a cell can only be rejected for a process present while it
measured. 984 cells in this campaign, 1194 in the one that found the regression, zero rejected in
either.
What is not settled
BM = 16routes is at the operator level; the share of those calls in a 27B or 35B round was nottaken. So "+18.9% on an operator" is not converted into a round-level number in either direction.
T = 60is unexplained. It is one timer step, it reproduces fourtimes out of four, it matches the weight-only arm at the same point, and it is inside the
zero-control band. Whether it is really the weight side paying at
BM = 16was not establishedncuprofile, no L2 traffic measurement.Ton this revision. The argument thatthey are unchanged is that their translation units are byte-identical to the previous head, which
is a reading of the machine code, not a measurement. Likewise the end-to-end tables.
BM = 16wantsca" rests on two schedules against 85. There is no thirdBM = 16schedule in the tree, so the rule has never been tested on a case it was not derived from, and a
new such schedule would silently take the
cgdefault.cawould also win somewhere atBM = 32was not swept. The change keepscgtherebecause
cgmeasures faster on everyBM >= 32route tested, but a full "policy x tile shape"sweep was not run.
caroutes - see the end of Bit-exactness gate.ctesthas one failure and it is master's. Shown by three runs on each side; the cause of thegraph phase=1failure was not investigated, and the retained logs keep only the first 20 failurelines per run.
--cache cold. One card, one driver, onehost, clocks not pinned (2542-2662 MHz under load).
linear_pair/w8/w8_pair_gemm_mma.cuh(four sites, shortT, selected atw8_pair_plan.cpp:46-66) andlinear/q4/q4_small_t_mma.cuh:114. Neither is measured here.linear/nvfp4/nvfp4_w4a4_mma.cuh:111and:130stage activation scales without a policy whilethe codes at
:96have one; also not measured.Fullsplit and with the W8 A8 series is read off the code, never off abinary carrying both.
🤖 Generated with Claude Code