The fp8 A8 GEMM stages its operands through TMA: prefill +2.4% at chunk 1024 and +4.9% at 4096 - #167
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. |
…pair Neroued#167/Neroued#160, dylan wave 2 (7dd98fd), gzenz safety net (08636ed), md fp8-KV campaign, T22-T27 plan
Neroued
left a comment
There was a problem hiding this comment.
This review is posted by an agent on behalf of @Neroued. The comments below have been reviewed by the maintainer.
Reviewed commit: 3d6f7f2e71e729c9235ef35c8e1878224a59145b. This is a static review of the implementation and the submitted evidence; no local compilation, tests, or profiling were performed for this review.
The optimization direction is sound, and the TMA kernel is worth keeping. I would like the following addressed before merging.
-
Make the shared-memory alignment requirement of the swizzle indexing explicit.
In
fp8_a8_tma.cuh,fp8_tma_shared_byte()computes the 64-byte swizzle from relative row coordinates, implicitly assuming a zero swizzle base offset. The shared storage and dynamic shared backing only declare 128-byte alignment. The 64-byte swizzle pattern repeats at a 512-byte boundary; an otherwise valid 128-byte-aligned base can require a nonzero offset. Please either guarantee 512-byte alignment for the relevant backing, keeping the stage/subtile offsets consistent with it, or incorporate the actual shared base offset into the indexing.This is a missing source-level correctness precondition, not a claim that the submitted binary has already produced incorrect output. A particular compiler layout can satisfy the stronger alignment incidentally. See NVIDIA's swizzle offset description.
-
Keep the routing policy compact and distinguish calibration from a performance guarantee.
The wave-count comparison is a reasonable heuristic, and a non-monotonic admission frontier is not inherently a problem. However, the
0.936coefficient is fitted at one shape/width and shared across different K dimensions and epilogues. The model does not represent pipeline fill or epilogue cost, and the floor plus the5120x6144ceiling compensate for measured cases it cannot distinguish.Empirical tuning for the RTX 5090 fits this project's scope; this is not a request for a general model covering other GPUs. Please simplify the large calibration/assertion section in
fp8_a8_schedule.cuh: retain geometry/resource invariants and necessary coverage checks, and move the detailed measurement history to a maintained performance reference. Assertions that a predicate keeps returning a particular answer do not establish that the selected route remains faster. Express the applicable calibration domain and known bounds clearly. -
Reconsider making the old kernel's bitwise behavior a permanent routing constraint.
The new kernel handles a partial trailing token tile, but
fp8_a8_tma_applies()additionally requirestokens % MmaSchedule::kBlockTokens == 0to match the oldFullTokensbranch's observed floating-point contraction behavior. That couples the new route's performance coverage to a private property of the old implementation.Please distinguish a measured performance reason for excluding these widths from a restriction imposed only to simplify
memcmp. The long-term qualification criterion is the production Op against its independent mathematical oracle at the defined semantic boundaries. Bitwise comparison with the previous kernel is useful supporting evidence, but should not become an additional public numerical contract. I am not asking to remove the gate without evidence; I am asking for its long-term justification to follow the Op contract and performance requirements. The comments also currently describe bit identity more strongly than the PR body's toolchain-dependent caveat warrants. -
Reuse the existing mbarrier helpers.
The PR adds
fp8_mbarrier_init,fp8_mbarrier_wait,fp8_mbarrier_arrive, andfp8_mbarrier_arrive_expect_tx, while its base already provides the corresponding functionality insrc/ops/common/mbarrier.cuh, used by the NVFP4 TMA implementation. These operations have no FP8-specific semantics. Please reuse the existing helpers so synchronization fixes and maintenance have one owner. This does not require introducing a generic GEMM framework.
The performance explanation should also describe the actual combined change: TMA delivery, a larger output tile, and a different warp decomposition. It is more than an operand-copy substitution:
- The output tile changes from
64x128to256x128. - The consumer warp tile changes from
32x32to64x64, increasing accumulator storage and operand reuse. - Two stages of K=128 become four stages of K=64: the nominal buffered K extent remains 256.
- Dedicated producer/consumer synchronization replaces the old K-loop's repeated CTA-wide synchronization pattern.
These changes provide credible mechanisms for the reported speedup, while also explaining the occupancy and small-shape tradeoffs. They can change repeated operand requests and cache behavior, so unchanged logical inputs do not prove unchanged physical memory traffic. Likewise, bandwidth derived from assumed bytes divided by time is not a DRAM counter measurement; the unreestablished peak denominator and the cold-operator versus inference discrepancy limit the roofline/bottleneck conclusions. Please narrow those claims accordingly. This does not by itself invalidate the reported elapsed-time improvements.
The reported default-chunk prefill gain is worth pursuing. The shared kernel body with Op-specific epilogue/output policies, the explicit paired SwiGLU handling, guarded tail accesses, and reuse of the existing workspace are good aspects of the implementation. Keeping the old route for widths where it is needed is justified. Per-launch descriptor construction and repeated device queries can remain follow-up optimization opportunities; this review has no evidence that they should block this change.
3d6f7f2 to
0159162
Compare
|
Thank you. All four are addressed, and the branch is rebased onto The first item turned out to be a defect rather than a documentation gap, so I will lead with that. 1. Shared-memory alignment of the swizzle indexingYou were right, and it is stronger than "the source does not state the precondition": the swizzle And
2. Routing policy: calibration separated from the codeThe measurement history is out of the header and into You are right that the assertions could not carry what they looked like they carried. The ones that The constant's description now says what the model represents — wave quantisation, and nothing else: 3. The multiple-of-tile condition is goneMeasured, and the answer is not the one I expected. Swept over 1024..1520 in steps of 16 on 32 widths, 8 of them whole cp.async tiles. The condition declined the other 24:
The 8 whole tiles were never its business: the model takes 1024, 1408 and 1472 and declines 1088, It never rescued a width the model would have wrongly taken, and it cannot: the model's only Confirmed on the shipped predicate rather than inferred. Re-swept after the removal, the same 32 That admits between 1134 and 5292 further widths per geometry over 1024..8192. They are not The consequence for the description: this change is no longer bitwise identical to the route it 4. mbarrier helpersRemoved. The route uses Worth recording why this was more than redundancy: the copies used plain labels ( The description of what changesYou are right that "the operand copy is staged differently" is not the change. Four things move
The wider warp tile is the arithmetic change: an The same table is the cost. Resident warps per SM fall from 16 to 9, so a shape with too few token Claims withdrawn
None of that touches the elapsed times. RebaseFour files conflicted on One thing that is not mine to fix
The number of failing cases is not stable across runs — 34 in one, 36 in another — while their form |
Add a second implementation of the FP8 row-scaled A8 GEMM and route to it where a cost model finds it cheaper. The arithmetic is unchanged: same accumulator, same m16n8k32, same scale application, same epilogue and output policies. Five call sites reach it - linear, linear_add, linear_swiglu, attn_input_proj, gdn_input_proj - all at prefill widths. What differs is the CTA's shape and how it is fed. The output tile grows from 64 to 256 tokens and the consumer warp tile from 32x32 to 64x64, so the MMA issued per ldmatrix goes from 8 to 32. Feeding that tile from one CTA needs a deeper, narrower pipeline - four stages of K=64 in place of two of K=128, the buffered K extent unchanged at 256 - and a dedicated producer warp issuing cp.async.bulk.tensor under an mbarrier pair, in place of the CTA-wide barrier the old K loop takes on every stage. Occupancy falls from two CTAs of 256 threads to one of 288, which is why narrow shapes are declined. The 64-byte TMA swizzle is a function of the shared-memory address, not of an offset relative to the destination, so a tile base must sit on a 512-byte swizzle atom. Measured: the same tile loaded 128 bytes past an atom boundary de-swizzles wrongly in all 1024 of its bytes. That alignment is declared on the storage, the union, the outer storage and the allocation, with static asserts that both stage strides and a paired block's second branch are whole atoms. CTAs are rasterised token-fastest, so the blocks that share a weight tile run at the same time. Each CTA computes the same tile either way, so the output does not change; the operator gains 1.5 to 3.3% on 34816x5120, whose weights do not fit in L2, and loses 0.2 to 0.5% on 5120x17408 through linear_add. Routing is a comparison of two quantised costs - waves needed times the work one SM carries through a wave - with one empirical constant for this part, plus a width floor and one per-geometry ceiling the model cannot express. The sweeps behind all three, the alignment measurement, the rasterisation choice, and what each bound costs are in docs/maintainer/fp8-a8-tma-route.md. Measured on an RTX 5090 (sm_120a, 170 SMs, 525 W) against the route it replaces, the arms alternating inside each pass and the base repeated under a second label to carry the instrument's zero: operator, linear: 0.877 to 0.989 where the route is taken operator, linear_add: 0.895 to 0.979 where the route is taken end to end, qwen3.8-27b prefill: +2.40% at chunk 1024, +4.95% at 4096, +4.38% at 8192 decode: 0.9993 to 1.0002 over four repetitions, and the route launches zero times in a decode-only kernel census taken with CUDA graphs expanded Output is not bitwise identical to the route it replaces. Where the width is a whole number of the old kernel's token tiles the two match byte for byte; at other widths they round differently by construction, and the numerical tests check those against a host reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0159162 to
f0c5ed8
Compare
|
Rebased onto All four items are closed: 512-byte alignment on every tile base the producer writes — it was a Prefill on qwen3.8-27b: +2.40% at chunk 1024, +4.95% at 4096, +4.38% at 8192, with a fourth arm — Your Ready for another look whenever suits you. |
|
@codex review |
|
Codex Review: Didn't find any major issues. You're on a roll. 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". |
A second implementation of the FP8 row-scaled A8 GEMM, and a cost model that routes to it where it
is cheaper. The arithmetic is untouched: same accumulator, same
m16n8k32, same scale application,same epilogue and output policies. Five call sites —
linear,linear_add,linear_swiglu,attn_input_proj,gdn_input_proj— all at prefill widths.Rebased onto
b88c0f6fand re-measured there, because the base moved:ee9d5192speeds up theNVFP4 part of qwen3.8-27b, and that mixed artifact is the one this route runs on.
The four items
1. Swizzle alignment — a defect, not a missing sentence. The swizzle is a function of the shared
address, not of an offset from the destination. Same tile, same descriptor: an atom-aligned base is
correct, base+128 is 1024 of 1024 bytes wrong.
__align__(128)happens to land on0x400underCUDA 13.1, which is why the submitted code computed correct results. 512 now sits on the tensor storage, the union, the outer storage and the allocation, with
static asserts that all three tile bases the producer writes — both stage strides and a paired
block's second branch — are whole atoms. It costs 384 B of shared,
sizeof98432 → 98816 against a101376 cap, and no time.
2. Calibration out of the header. The measurement history moved to
docs/maintainer/fp8-a8-tma-route.md, indexed fromdocs/README.md; the header section went 387 →284 lines. What is left asserts coverage and says so — that the predicate still sends the test
widths down this path, so the numerical tests exercise the kernel they were extended for. Nothing
there claims the route is faster.
3. The multiple-of-tile condition is gone. Swept 1024..1520 in steps of 16 on
14336x5120: itdeclined 24 of 32 widths — repeating the model's own decision on 15, blocking a measured gain on 9
(0.975 down to 0.915). It could not have rescued a width the model wrongly takes: the model's only
width-dependent inputs are
ceil(T/256)andceil(T/64), both constant across each 64-token band.So it was a
memcmpconvenience, and this route is no longer bitwise identical to the one itreplaces. Widths that are a whole cp.async token tile still match byte for byte; the rest are
checked against the host reference — 1345, 1153 and 4001 added across the five test files.
4. mbarrier helpers. Removed; the route uses
cta_mbarrier_*fromops/common/mbarrier.cuh.New since your review: token-fastest rasterisation, matching
ee9d5192and the cp.asyncschedules this route falls back to, which are already registered
Fp8MmaRaster::TokenFast. Each CTAcomputes the same tile, so the output is unchanged by construction. It is not uniformly free: at the
operator it is worth 1.5–3.3% on
34816x5120and costs 0.2–0.5% on5120x17408throughlinear_add; end to end it is −0.12% at chunk 1024, +0.71% at 4096, +0.97% at 8192. The directionthe numbers point in is the concurrent working set against L2 —
34816x5120is the only geometrywhose weights, at 178 MB, do not fit. Choosing the order per geometry from that working set would
recover the 0.2–0.5%; it is written up in the document rather than taken here, because it is a
decision for three routes and not only this one.
What actually changes
Not one operand-copy mechanism swapped for another. Four things move together and only the last is
about TMA:
The wider warp tile is the arithmetic change: an
ldmatrixof A feeds eight N fragments instead offour, so MMA issued per operand load goes from 8 to 32. The deeper, narrower pipeline and the
dedicated producer are what let one CTA keep that tile fed without the per-stage CTA-wide barrier.
The same table is the cost. Resident warps per SM fall from 16 to 9, so a shape with too few token
tiles to fill the machine pays that and gets none of the reuse back — which is why narrow widths are
declined, and what the cost model has no term for. Per SM the route uses less of both budgeted
resources than the one it replaces: 98816 B of shared against 100352, and 166 x 288 registers
against 94 x 512.
Measured
One RTX 5090, sm_120a, 170 SMs, 525 W, CUDA 13.1. Clock locking is not available and the card idles
at 180 MHz, so the arms alternate inside each pass and a fourth arm — the base under a second label
— carries the instrument's zero.
Operator, ratio of this route to the route it replaces, median of three passes. The null arm
spans 0.989 to 1.003; † marks a width the predicate declines, where both arms run the same kernel:
5120x17408linear_add5120x6144linear_add5120x17408linear5120x6144linear14336x5120linear16384x5120linear34816x5120linearEnd to end. Only qwen3.8-27b carries
FP8_E4M3FN_ROW_BF16Sweights — qwen3.6-27b is nvfp4 andqwen3.6-35b-a3b groupwise-int end to end — so it is the only artifact this route runs on.
ninfer_bench -p 16384, five passes in mirrored order:Each change is the median of the five paired ratios; past the first pass they spread under 0.1%.
Decode is not on this path, and that is a census.
-n 512with--cuda-graph-trace=node: 29distinct kernels execute and
fp8_a8_tma_kernelis not among them, 0 launches. Decode throughputover four alternating repetitions ranges 0.9993 to 1.0002.
Suite and tools on the tree this pushes:
ctest114 of 114, and the same on cleanb88c0f6f,both built and run on one machine with one toolchain.
compute-sanitizer:memcheck0 errors at 1024, 1345,4096 and 4288 on
14336x5120and at 4001 on5120x17408— the last three reach this route onlysince item 3;
racecheck,synccheckandinitcheck0 at 1024.Your
b88c0f6falso closed theninfer_attn_input_proj_testfailure the previous revision reportedas not mine to fix. It passes on both arms now.
What this does not claim
physical traffic, and I did not measure traffic.
ninfer_linear_benchpublishes, which is a model.and the cold-operator bench does not reproduce the inference-time picture.
The routing constant is fitted for this part and is not portable; what travels is the shape of the
comparison. The sweeps behind it, behind both width bounds, and behind the rasterisation choice are
in
docs/maintainer/fp8-a8-tma-route.md.🤖 Generated with Claude Code