Add forward-only fuse_q_l2norm to chunk_kda - #345
Conversation
Inference callers currently normalize q with a standalone l2norm pass before chunk_kda, which reads and writes the full [T, H, 128] tensor just to apply a per-row scalar. Every q-dependent term in the forward is linear in q's row and lands in the same output row, so the norm can be deferred: the grams stay in the raw-q basis (intra and K3b unchanged), the output kernel accumulates each row's sum of squares inside its existing k-tile loop, and one 1/||q|| row scale on the accumulated output completes the normalization. An earlier variant that emitted an rstd tensor from the intra kernel cost +154us from register pressure in that 1-warp kernel and was rejected. The fused ragged output launch needs a slightly larger register cap (152 vs 136) for the extra accumulator. The flag is opt-in and forward-only: it raises when any input requires grad (the saved Aqk stays in the raw-q basis, so the existing backward would be wrong) and when compiling (the fused route bypasses the compiler-opaque autograd op). This mirrors the precedent elsewhere: FLA's use_qk_l2norm_in_kernel supports backward only because it launches the standalone l2norm kernels inside the op rather than fusing them, while every true fusion (Helion varlen, FlashInfer decode) is forward-only. k keeps its explicit l2norm; this complements the decode-side preprocessing fusion in the recurrent path (#317, #334) on the chunked prefill side. Raw-q grams run about sqrt(K) hotter, so the flag assumes the bounded-gate contract; the eps floor matches l2norm's default and the tests cover zero rows and 1e-3..1e3 row scales against the explicit path. GB300, six frozen contract shapes, output-only forward, warm CUDA events: removes the 38-60us standalone q pass per call (l2norm kernel eliminated; +5us sumsq cost inside the o kernel). This flipped the h96/h64 mixed-shape comparisons against Helion's pretuned linear-attention kernels; final six-shape geomean time ratio ours/helion = 0.952. ```bash pytest test/test_kda_fuse_q_l2norm.py test/test_kda*.py python agent_space/bench_three_way.py ``` stack-info: PR: #345, branch: drisspg/stack/84
a120ff9 to
3f3f755
Compare
81db694 to
3e0d7a3
Compare
💡 Codex Reviewattention-gym/attn_gym/linear/kda/fwd/triton/chunk_delta_h.py Lines 422 to 423 in 81db694 For ordinary public AGENTS.md reference: AGENTS.md:L78-L84 ℹ️ 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". |
Inference callers currently normalize q with a standalone l2norm pass before chunk_kda, which reads and writes the full [T, H, 128] tensor just to apply a per-row scalar. Every q-dependent term in the forward is linear in q's row and lands in the same output row, so the norm can be deferred: the grams stay in the raw-q basis (intra and K3b unchanged), the output kernel accumulates each row's sum of squares inside its existing k-tile loop, and one 1/||q|| row scale on the accumulated output completes the normalization. An earlier variant that emitted an rstd tensor from the intra kernel cost +154us from register pressure in that 1-warp kernel and was rejected. The fused ragged output launch needs a slightly larger register cap (152 vs 136) for the extra accumulator. The flag is opt-in and forward-only: it raises when any input requires grad (the saved Aqk stays in the raw-q basis, so the existing backward would be wrong) and when compiling (the fused route bypasses the compiler-opaque autograd op). This mirrors the precedent elsewhere: FLA's use_qk_l2norm_in_kernel supports backward only because it launches the standalone l2norm kernels inside the op rather than fusing them, while every true fusion (Helion varlen, FlashInfer decode) is forward-only. k keeps its explicit l2norm; this complements the decode-side preprocessing fusion in the recurrent path (#317, #334) on the chunked prefill side. Raw-q grams run about sqrt(K) hotter, so the flag assumes the bounded-gate contract; the eps floor matches l2norm's default and the tests cover zero rows and 1e-3..1e3 row scales against the explicit path. GB300, six frozen contract shapes, output-only forward, warm CUDA events: removes the 38-60us standalone q pass per call (l2norm kernel eliminated; +5us sumsq cost inside the o kernel). This flipped the h96/h64 mixed-shape comparisons against Helion's pretuned linear-attention kernels; final six-shape geomean time ratio ours/helion = 0.952. ```bash pytest test/test_kda_fuse_q_l2norm.py test/test_kda*.py python agent_space/bench_three_way.py ``` stack-info: PR: #345, branch: drisspg/stack/84
3e0d7a3 to
8aaba8e
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8aaba8e214
ℹ️ 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".
| output, state, _, _ = _chunk_kda_fwd( | ||
| q, |
There was a problem hiding this comment.
Restrict fused normalization to BF16 queries
When q is an accepted FP16/FP32 input, this branch receives the tensor only after line 192 has converted the raw values to BF16, whereas the advertised explicit path computes l2norm(q) in the original dtype and converts the normalized result afterward. Inputs whose small components are rounded or underflow during the early BF16 cast can therefore produce a different direction—or an all-zero output—rather than matching the skipped normalization pass. Either reject non-BF16 q when this flag is enabled or preserve the original-precision normalization semantics.
Useful? React with 👍 / 👎.
| cu_seqlens = torch.tensor( | ||
| [0, *torch.tensor(seq_lens).cumsum(0).tolist()], device=device, dtype=torch.int32 | ||
| ) |
There was a problem hiding this comment.
Reuse the shared packed-offset builder
Replace this local cu_seqlens construction with attn_gym.testing.kda.cumulative_sequence_offsets(seq_lens). The scoped test guidance explicitly requires checking and reusing that helper for packed offsets, so keeping a second builder here duplicates the canonical dtype/device and boundary conventions.
AGENTS.md reference: test/AGENTS.md:L5-L10
Useful? React with 👍 / 👎.
8aaba8e to
a10335b
Compare
Inference callers currently normalize q with a standalone l2norm pass before chunk_kda, which reads and writes the full [T, H, 128] tensor just to apply a per-row scalar. Every q-dependent term in the forward is linear in q's row and lands in the same output row, so the norm can be deferred: the grams stay in the raw-q basis (intra and K3b unchanged), the output kernel accumulates each row's sum of squares inside its existing k-tile loop, and one 1/||q|| row scale on the accumulated output completes the normalization. An earlier variant that emitted an rstd tensor from the intra kernel cost +154us from register pressure in that 1-warp kernel and was rejected. The fused ragged output launch needs a slightly larger register cap (152 vs 136) for the extra accumulator. The flag is opt-in and forward-only: it raises when any input requires grad (the saved Aqk stays in the raw-q basis, so the existing backward would be wrong) and when compiling (the fused route bypasses the compiler-opaque autograd op). This mirrors the precedent elsewhere: FLA's use_qk_l2norm_in_kernel supports backward only because it launches the standalone l2norm kernels inside the op rather than fusing them, while every true fusion (Helion varlen, FlashInfer decode) is forward-only. k keeps its explicit l2norm; this complements the decode-side preprocessing fusion in the recurrent path (#317, #334) on the chunked prefill side. Raw-q grams run about sqrt(K) hotter, so the flag assumes the bounded-gate contract; the eps floor matches l2norm's default and the tests cover zero rows and 1e-3..1e3 row scales against the explicit path. GB300, six frozen contract shapes, output-only forward, warm CUDA events: removes the 38-60us standalone q pass per call (l2norm kernel eliminated; +5us sumsq cost inside the o kernel). This flipped the h96/h64 mixed-shape comparisons against Helion's pretuned linear-attention kernels; final six-shape geomean time ratio ours/helion = 0.952. ```bash pytest test/test_kda_fuse_q_l2norm.py test/test_kda*.py python agent_space/bench_three_way.py ``` stack-info: PR: #345, branch: drisspg/stack/84
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a10335be2f
ℹ️ 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".
| ) | ||
| # The fused path defers the row scale past the bf16 gram rounding, so | ||
| # outputs agree to bf16 resolution; the state path never touches q's norm. | ||
| torch.testing.assert_close(actual, expected, atol=2e-3, rtol=2e-2) |
There was a problem hiding this comment.
Calibrate the fused-output tolerance against a reference
Replace this fixed atol/rtol assertion with assert_matches_low_precision_reference, using the reference implementation as the high-precision result and the explicit-normalization fused run as the low-precision result. With the current random fixture, the absolute 2e-3 allowance can hide errors in small expected values and is not tied to the BF16 rounding error that the comment intends to permit; the scoped test convention specifically requires data-derived error budgets rather than hand-tuned tolerances.
AGENTS.md reference: test/AGENTS.md:L5-L13
Useful? React with 👍 / 👎.
Inference callers currently normalize q with a standalone l2norm pass before chunk_kda, which reads and writes the full [T, H, 128] tensor just to apply a per-row scalar. Every q-dependent term in the forward is linear in q's row and lands in the same output row, so the norm can be deferred: the grams stay in the raw-q basis (intra and K3b unchanged), the output kernel accumulates each row's sum of squares inside its existing k-tile loop, and one 1/||q|| row scale on the accumulated output completes the normalization. An earlier variant that emitted an rstd tensor from the intra kernel cost +154us from register pressure in that 1-warp kernel and was rejected. The fused ragged output launch needs a slightly larger register cap (152 vs 136) for the extra accumulator. The flag is opt-in and forward-only: it raises when any input requires grad (the saved Aqk stays in the raw-q basis, so the existing backward would be wrong) and when compiling (the fused route bypasses the compiler-opaque autograd op). This mirrors the precedent elsewhere: FLA's use_qk_l2norm_in_kernel supports backward only because it launches the standalone l2norm kernels inside the op rather than fusing them, while every true fusion (Helion varlen, FlashInfer decode) is forward-only. k keeps its explicit l2norm; this complements the decode-side preprocessing fusion in the recurrent path (#317, #334) on the chunked prefill side. Raw-q grams run about sqrt(K) hotter, so the flag assumes the bounded-gate contract; the eps floor matches l2norm's default and the tests cover zero rows and 1e-3..1e3 row scales against the explicit path. GB300, six frozen contract shapes, output-only forward, warm CUDA events: removes the 38-60us standalone q pass per call (l2norm kernel eliminated; +5us sumsq cost inside the o kernel). This flipped the h96/h64 mixed-shape comparisons against Helion's pretuned linear-attention kernels; final six-shape geomean time ratio ours/helion = 0.952. ```bash pytest test/test_kda_fuse_q_l2norm.py test/test_kda*.py python agent_space/bench_three_way.py ``` stack-info: PR: #345, branch: drisspg/stack/84
a10335b to
2871c81
Compare
Inference callers currently normalize q with a standalone l2norm pass before chunk_kda, which reads and writes the full [T, H, 128] tensor just to apply a per-row scalar. Every q-dependent term in the forward is linear in q's row and lands in the same output row, so the norm can be deferred: the grams stay in the raw-q basis (intra and K3b unchanged), the output kernel accumulates each row's sum of squares inside its existing k-tile loop, and one 1/||q|| row scale on the accumulated output completes the normalization. An earlier variant that emitted an rstd tensor from the intra kernel cost +154us from register pressure in that 1-warp kernel and was rejected. The fused ragged output launch needs a slightly larger register cap (152 vs 136) for the extra accumulator. The flag is opt-in and forward-only: it raises when any input requires grad (the saved Aqk stays in the raw-q basis, so the existing backward would be wrong) and when compiling (the fused route bypasses the compiler-opaque autograd op). This mirrors the precedent elsewhere: FLA's use_qk_l2norm_in_kernel supports backward only because it launches the standalone l2norm kernels inside the op rather than fusing them, while every true fusion (Helion varlen, FlashInfer decode) is forward-only. k keeps its explicit l2norm; this complements the decode-side preprocessing fusion in the recurrent path (#317, #334) on the chunked prefill side. Raw-q grams run about sqrt(K) hotter, so the flag assumes the bounded-gate contract; the eps floor matches l2norm's default and the tests cover zero rows and 1e-3..1e3 row scales against the explicit path. GB300, six frozen contract shapes, output-only forward, warm CUDA events: removes the 38-60us standalone q pass per call (l2norm kernel eliminated; +5us sumsq cost inside the o kernel). This flipped the h96/h64 mixed-shape comparisons against Helion's pretuned linear-attention kernels; final six-shape geomean time ratio ours/helion = 0.952. ```bash pytest test/test_kda_fuse_q_l2norm.py test/test_kda*.py python agent_space/bench_three_way.py ``` stack-info: PR: #345, branch: drisspg/stack/84
2871c81 to
38f5614
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38f56140c1
ℹ️ 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".
| if FUSE_Q_L2NORM: | ||
| # Both output terms are linear in the raw q row (Aqk carries the raw-q | ||
| # gram), so one row scale by 1/||q|| completes the deferred L2 norm. | ||
| b_o *= (1 / tl.sqrt(b_ssq + _L2NORM_EPS))[:, None] |
There was a problem hiding this comment.
Prevent raw-query overflow before applying the fused norm
For BF16 rows with large but finite values, the raw-q dot products can overflow before this final scale is applied, producing inf * 0 = NaN. For example, an all-3e38 query row and positive normalized keys make Aqk overflow, while b_ssq is also infinite; the explicit l2norm(q) path instead reduces that row to zero and returns a finite result. The bounded-gate requirement does not prevent this query-magnitude overflow, so the fused path needs overflow-safe scaling or an explicit magnitude restriction.
Useful? React with 👍 / 👎.
Inference callers currently normalize q with a standalone l2norm pass before chunk_kda, which reads and writes the full [T, H, 128] tensor just to apply a per-row scalar. Every q-dependent term in the forward is linear in q's row and lands in the same output row, so the norm can be deferred: the grams stay in the raw-q basis (intra and K3b unchanged), the output kernel accumulates each row's sum of squares inside its existing k-tile loop, and one 1/||q|| row scale on the accumulated output completes the normalization. An earlier variant that emitted an rstd tensor from the intra kernel cost +154us from register pressure in that 1-warp kernel and was rejected. The fused ragged output launch needs a slightly larger register cap (152 vs 136) for the extra accumulator. The flag is opt-in and forward-only: it raises when any input requires grad (the saved Aqk stays in the raw-q basis, so the existing backward would be wrong) and when compiling (the fused route bypasses the compiler-opaque autograd op). This mirrors the precedent elsewhere: FLA's use_qk_l2norm_in_kernel supports backward only because it launches the standalone l2norm kernels inside the op rather than fusing them, while every true fusion (Helion varlen, FlashInfer decode) is forward-only. k keeps its explicit l2norm; this complements the decode-side preprocessing fusion in the recurrent path (#317, #334) on the chunked prefill side. Raw-q grams run about sqrt(K) hotter, so the flag assumes the bounded-gate contract; the eps floor matches l2norm's default and the tests cover zero rows and 1e-3..1e3 row scales against the explicit path. GB300, six frozen contract shapes, output-only forward, warm CUDA events: removes the 38-60us standalone q pass per call (l2norm kernel eliminated; +5us sumsq cost inside the o kernel). This flipped the h96/h64 mixed-shape comparisons against Helion's pretuned linear-attention kernels; final six-shape geomean time ratio ours/helion = 0.952. ```bash pytest test/test_kda_fuse_q_l2norm.py test/test_kda*.py python agent_space/bench_three_way.py ``` stack-info: PR: #345, branch: drisspg/stack/84
38f5614 to
afcbcee
Compare
Inference callers currently normalize q with a standalone l2norm pass before chunk_kda, which reads and writes the full [T, H, 128] tensor just to apply a per-row scalar. Every q-dependent term in the forward is linear in q's row and lands in the same output row, so the norm can be deferred: the grams stay in the raw-q basis (intra and K3b unchanged), the output kernel accumulates each row's sum of squares inside its existing k-tile loop, and one 1/||q|| row scale on the accumulated output completes the normalization. An earlier variant that emitted an rstd tensor from the intra kernel cost +154us from register pressure in that 1-warp kernel and was rejected. The fused ragged output launch needs a slightly larger register cap (152 vs 136) for the extra accumulator. The flag is opt-in and forward-only: it raises when any input requires grad (the saved Aqk stays in the raw-q basis, so the existing backward would be wrong) and when compiling (the fused route bypasses the compiler-opaque autograd op). This mirrors the precedent elsewhere: FLA's use_qk_l2norm_in_kernel supports backward only because it launches the standalone l2norm kernels inside the op rather than fusing them, while every true fusion (Helion varlen, FlashInfer decode) is forward-only. k keeps its explicit l2norm; this complements the decode-side preprocessing fusion in the recurrent path (#317, #334) on the chunked prefill side. Raw-q grams run about sqrt(K) hotter, so the flag assumes the bounded-gate contract; the eps floor matches l2norm's default and the tests cover zero rows and 1e-3..1e3 row scales against the explicit path. GB300, six frozen contract shapes, output-only forward, warm CUDA events: removes the 38-60us standalone q pass per call (l2norm kernel eliminated; +5us sumsq cost inside the o kernel). This flipped the h96/h64 mixed-shape comparisons against Helion's pretuned linear-attention kernels; final six-shape geomean time ratio ours/helion = 0.952. ```bash pytest test/test_kda_fuse_q_l2norm.py test/test_kda*.py python agent_space/bench_three_way.py ``` stack-info: PR: #345, branch: drisspg/stack/84
afcbcee to
cd6d6ff
Compare
Inference callers currently normalize q with a standalone l2norm pass before chunk_kda, which reads and writes the full [T, H, 128] tensor just to apply a per-row scalar. Every q-dependent term in the forward is linear in q's row and lands in the same output row, so the norm can be deferred: the grams stay in the raw-q basis (intra and K3b unchanged), the output kernel accumulates each row's sum of squares inside its existing k-tile loop, and one 1/||q|| row scale on the accumulated output completes the normalization. An earlier variant that emitted an rstd tensor from the intra kernel cost +154us from register pressure in that 1-warp kernel and was rejected. The fused ragged output launch needs a slightly larger register cap (152 vs 136) for the extra accumulator. The flag is opt-in and forward-only: it raises when any input requires grad (the saved Aqk stays in the raw-q basis, so the existing backward would be wrong) and when compiling (the fused route bypasses the compiler-opaque autograd op). This mirrors the precedent elsewhere: FLA's use_qk_l2norm_in_kernel supports backward only because it launches the standalone l2norm kernels inside the op rather than fusing them, while every true fusion (Helion varlen, FlashInfer decode) is forward-only. k keeps its explicit l2norm; this complements the decode-side preprocessing fusion in the recurrent path (#317, #334) on the chunked prefill side. Raw-q grams run about sqrt(K) hotter, so the flag assumes the bounded-gate contract; the eps floor matches l2norm's default and the tests cover zero rows and 1e-3..1e3 row scales against the explicit path. GB300, six frozen contract shapes, output-only forward, warm CUDA events: removes the 38-60us standalone q pass per call (l2norm kernel eliminated; +5us sumsq cost inside the o kernel). This flipped the h96/h64 mixed-shape comparisons against Helion's pretuned linear-attention kernels; final six-shape geomean time ratio ours/helion = 0.952. ```bash pytest test/test_kda_fuse_q_l2norm.py test/test_kda*.py python agent_space/bench_three_way.py ``` stack-info: PR: #345, branch: drisspg/stack/84
cd6d6ff to
ede717d
Compare
|
Dropping this: a dedicated forward-only prefill path is planned and the flag belongs there rather than on the training API. Work preserved on branch archive/kda-fuse-q-l2norm. |
Add forward-only fuse_q_l2norm to chunk_kda
Inference callers currently normalize q with a standalone l2norm pass before chunk_kda, which reads
and writes the full [T, H, 128] tensor just to apply a per-row scalar. Every q-dependent term in the
forward is linear in q's row and lands in the same output row, so the norm can be deferred: the
grams stay in the raw-q basis (intra and K3b unchanged), the output kernel accumulates each row's
sum of squares inside its existing k-tile loop, and one 1/||q|| row scale on the accumulated output
completes the normalization. An earlier variant that emitted an rstd tensor from the intra kernel
cost +154us from register pressure in that 1-warp kernel and was rejected. The fused ragged output
launch needs a slightly larger register cap (152 vs 136) for the extra accumulator.
The flag is opt-in and forward-only: it raises when any input requires grad (the saved Aqk stays in
the raw-q basis, so the existing backward would be wrong) and when compiling (the fused route
bypasses the compiler-opaque autograd op). This mirrors the precedent elsewhere: FLA's
use_qk_l2norm_in_kernel supports backward only because it launches the standalone l2norm kernels
inside the op rather than fusing them, while every true fusion (Helion varlen, FlashInfer decode)
is forward-only. k keeps its explicit l2norm; this complements the decode-side preprocessing fusion
in the recurrent path (#317, #334) on the chunked prefill side. Raw-q grams run about sqrt(K) hotter,
so the flag assumes the bounded-gate contract; the eps floor matches l2norm's default and the tests
cover zero rows and 1e-3..1e3 row scales against the explicit path.
GB300, six frozen contract shapes, output-only forward, warm CUDA events: removes the 38-60us
standalone q pass per call (l2norm kernel eliminated; +5us sumsq cost inside the o kernel). This
flipped the h96/h64 mixed-shape comparisons against Helion's pretuned linear-attention kernels;
final six-shape geomean time ratio ours/helion = 0.952.
pytest test/test_kda_fuse_q_l2norm.py test/test_kda*.py python agent_space/bench_three_way.py