Skip to content

feat(kernels): Triton FlashAttention LSE export + varlen packing#233

Open
Billy1900 wants to merge 3 commits into
RL-Align:mainfrom
Billy1900:feat/triton-attention-lse-varlen
Open

feat(kernels): Triton FlashAttention LSE export + varlen packing#233
Billy1900 wants to merge 3 commits into
RL-Align:mainfrom
Billy1900:feat/triton-attention-lse-varlen

Conversation

@Billy1900

@Billy1900 Billy1900 commented Jul 19, 2026

Copy link
Copy Markdown

Summary

First step of a stack toward "Fused FlashAttention with causal mask, varlen
packing, and exported attention LSE" (see discussion in the repo). Rather than
attempting SM90 WGMMA+TMA + SM80 + ROCm MFMA + Triton all in one PR, this
lands the Triton piece first: it's the only backend where a real kernel
already existed to extend (rl_engine/kernels/ops/triton/triton_attn.py), and
it becomes the cross-platform semantic baseline the SM90/SM80/ROCm kernels
get checked against later, rather than against each other.

What this adds

  • Attention-domain LSE export: return_lse=True on the existing dense
    triton_flash_attention. No new kernel math — M (running max) and L
    (running sum-exp) were already computed online for the backward pass, just
    never returned. LSE is marked non-differentiable (ctx.mark_non_differentiable),
    matching flash_attn's external softmax_lse contract. Default calls
    (return_lse=False) are unchanged — no breaking change to the (currently
    uncalled) public API.
  • triton_flash_attention_varlen: a new packed variable-length path over
    cu_seqlens-style [total_tokens, H, D] tensors (same convention as
    flash_attn_varlen_func and this repo's pack op, feat(kernels): add fused masking + variable-length pack-and-pad op  #182), with forward +
    backward kernels. Causal masking uses the Skv - Sq anchor from
    NativeAttentionOp (docs/operators/attention.md) so prefill (Sq == Skv)
    and decode (Sq < Skv) share one formula.

Explicitly out of scope for this PR: SM90 WGMMA+TMA, SM80 mma.sync, and
ROCm MFMA kernels, and GQA support (the pre-existing dense Triton kernel
didn't support GQA either — not a regression). Not wired into
KernelRegistry yet.

Correctness

Both paths are checked against an independent fp32 masked-softmax +
logsumexp reference (not against each other, so a shared bug in the
online-softmax loop can't hide). Run end-to-end on real H100 SXM5 hardware:

  • Dense: out max-abs-diff ≈ 1.4e-3, lse1e-6 vs. reference (fp16
    inputs, fp32 accumulation).
  • Varlen: forward + backward (dq/dk/dv) checked across causal/non-causal,
    decode-style (Sq < Skv, varying independently per sequence in the batch),
    sequence lengths deliberately not aligned to the kernel's 64-token block
    size, head_dim 64 and 128, and a zero-length sequence in the batch (a real
    case for fully-masked/empty RL responses — packed tensors have the next
    sequence's data immediately after a short one, so this is a genuine
    correctness boundary, not just an edge case to tolerate).

8/8 tests in tests/test_triton_attention_varlen.py pass; the pre-existing
26/26 in tests/test_attention.py are untouched and still pass.

Docs

Added docs/operators/attention-varlen.md (template-following operator page,
wired into docs/.nav.yml and docs/operators/README.md), including an
explicit note that this LSE is attention-domain, not the vocab-domain LSE the
fused_logp/linear_logp kernels already use internally — same name,
different tensor, worth flagging to avoid confusion.

Test plan

  • pytest tests/test_triton_attention_varlen.py -v — 8 passed (H100 SXM5)
  • pytest tests/test_attention.py -v — 26 passed, no regressions
  • ruff check, black --check, isort --check-only all clean

Summary by CodeRabbit

  • New Features

    • Added optional per-query attention log-sum-exp (LSE) export for dense Triton FlashAttention.
    • Introduced packed variable-length FlashAttention using cumulative sequence lengths, supporting causal/non-causal and decode-style regimes (including zero-length sequences).
  • Documentation

    • Added a new operator documentation page and linked it from the Operators index/navigation.
  • Tests

    • Added Triton-gated tests covering dense LSE accuracy, correct backward behavior, packed/varlen correctness, and non-contiguous inputs.
  • Benchmarks

    • Added benchmark script comparing dense and packed varlen attention (including LSE export overhead) against PyTorch baselines.

Extends the Triton FlashAttention fallback with an attention-domain LSE
output (M + log(L), already computed online for the backward pass but
previously discarded) and a packed variable-length path operating on
cu_seqlens-style [total_tokens, H, D] tensors instead of a padded dense
batch. This is the cross-platform semantic baseline the planned SM90
WGMMA+TMA, SM80 mma.sync, and ROCm MFMA fused-attention kernels will be
checked against.

Both paths are validated against an independent per-sequence fp32
masked-softmax + logsumexp reference (not against the dense kernel
itself), covering causal/non-causal, decode-style (Sq < Skv), sequence
lengths not aligned to the kernel's block size, and empty sequences in
a packed batch -- run end-to-end on H100 hardware.
@Billy1900
Billy1900 requested a review from Flink-ddd as a code owner July 19, 2026 09:53
Copilot AI review requested due to automatic review settings July 19, 2026 09:53
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds optional attention-domain LSE export to dense Triton FlashAttention, introduces packed variable-length attention, adds CUDA/Triton correctness tests and benchmarks, and documents the APIs, tensor contracts, backend scope, validation, and limitations.

Changes

Triton attention enhancements

Layer / File(s) Summary
Dense attention LSE export
rl_engine/kernels/ops/triton/triton_attn.py, tests/test_triton_attention_varlen.py
Dense FlashAttention optionally returns non-differentiable float32 LSE while preserving the default tensor-only return and backward behavior.
Packed variable-length attention
rl_engine/kernels/ops/triton/triton_attn.py, tests/test_triton_attention_varlen.py
Adds packed cu_seqlens attention with optional LSE output and validates causal, non-causal, uneven-length, decode-style, head-dimension, zero-length, non-contiguous, output, LSE, and gradient cases.
Attention benchmark workflow
benchmarks/benchmark_triton_attention_varlen.py
Adds dense and packed benchmarks comparing Triton attention with PyTorch SDPA, including timing, backward, peak-memory, and LSE-export measurements.
Operator documentation and navigation
docs/operators/attention-varlen.md, docs/operators/README.md, docs/.nav.yml
Documents the dense and packed APIs, tensor contracts, reference behavior, test coverage, backend scope, performance notes, and known limitations, and adds the page to navigation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TestVarlenAttention
  participant triton_flash_attention_varlen
  participant TritonAttentionVarlenKernel
  participant ref_attn
  TestVarlenAttention->>triton_flash_attention_varlen: submit packed q, k, v, and cu_seqlens
  triton_flash_attention_varlen->>TritonAttentionVarlenKernel: execute attention with return_lse
  TritonAttentionVarlenKernel-->>triton_flash_attention_varlen: return packed output and LSE
  TestVarlenAttention->>ref_attn: compute fp32 per-sequence reference
  ref_attn-->>TestVarlenAttention: return reference output and LSE
  TestVarlenAttention->>TestVarlenAttention: compare outputs, LSE, and gradients
Loading

Suggested reviewers: kjldefeated, inaniloquentee, flink-ddd, ethanzero2hero

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: Triton FlashAttention now exports LSE and adds varlen packing support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds functionality to the Triton FlashAttention fallback kernels to (1) optionally export attention-domain LSE and (2) support packed variable-length (cu_seqlens) attention, positioning Triton as the cross-platform semantic baseline for upcoming SM90/SM80/ROCm fused implementations.

Changes:

  • Extend dense triton_flash_attention with return_lse=True to return attention-domain log-sum-exp (non-differentiable).
  • Add triton_flash_attention_varlen with forward/backward kernels for packed [total_tokens, H, D] + cu_seqlens inputs, including causal decode-style masking.
  • Add dedicated tests and operator documentation for the new LSE export and varlen path.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
rl_engine/kernels/ops/triton/triton_attn.py Adds dense LSE export and introduces packed varlen FlashAttention (fwd+bwd) kernels/APIs.
tests/test_triton_attention_varlen.py New CUDA+Triton-gated tests validating dense LSE + varlen fwd/bwd against an independent fp32 reference.
docs/operators/attention-varlen.md New operator page documenting LSE export + packed varlen attention contract and limitations.
docs/operators/README.md Adds the new operator doc page to the operators index.
docs/.nav.yml Wires the new operator doc page into site navigation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rl_engine/kernels/ops/triton/triton_attn.py
Comment thread rl_engine/kernels/ops/triton/triton_attn.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_triton_attention_varlen.py (1)

54-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefix unused unpacked vars. H and D from H, Sq, D = q.shape are unused (Ruff RUF059). Rename to _H/_D or use _, Sq, _.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_triton_attention_varlen.py` at line 54, Update the shape unpacking
near the attention test to mark the unused H and D dimensions as intentionally
unused, using underscore-prefixed names or anonymous underscores while retaining
Sq for subsequent logic.

Source: Linters/SAST tools

rl_engine/kernels/ops/triton/triton_attn.py (1)

792-792: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider fp32 accumulation for dq atomics.

dq is accumulated via tl.atomic_add in the input dtype (fp16), summing contributions from every K-block. fp16 atomic accumulation compounds rounding error; a fp32 dq scratch buffer (cast to q.dtype at the end) would be more robust. Current tests pass within atol=0.05, so this is not blocking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl_engine/kernels/ops/triton/triton_attn.py` at line 792, Update the dq
accumulation flow around the tl.atomic_add call to use an fp32 scratch buffer
for atomic accumulation, then cast the completed result back to q.dtype when
writing the final dq output. Ensure all K-block contributions use the fp32
buffer and preserve the existing masking behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rl_engine/kernels/ops/triton/triton_attn.py`:
- Around line 903-948: Fix varlen backward stride handling in the wrapper
invoking _bwd_preprocess_varlen and _bwd_kernel_varlen: do is contiguous, but
these kernels currently reuse out/q strides while q, k, and v may be
non-contiguous. Either make q, k, and v contiguous before launching both
kernels, or add and pass dedicated do strides through both launches, preserving
correct delta and gradient results for packed non-contiguous inputs.

---

Nitpick comments:
In `@rl_engine/kernels/ops/triton/triton_attn.py`:
- Line 792: Update the dq accumulation flow around the tl.atomic_add call to use
an fp32 scratch buffer for atomic accumulation, then cast the completed result
back to q.dtype when writing the final dq output. Ensure all K-block
contributions use the fp32 buffer and preserve the existing masking behavior.

In `@tests/test_triton_attention_varlen.py`:
- Line 54: Update the shape unpacking near the attention test to mark the unused
H and D dimensions as intentionally unused, using underscore-prefixed names or
anonymous underscores while retaining Sq for subsequent logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4bd91fa-2f38-473f-ac41-d98a69fcfb0b

📥 Commits

Reviewing files that changed from the base of the PR and between 6df029a and 3d5364b.

📒 Files selected for processing (5)
  • docs/.nav.yml
  • docs/operators/README.md
  • docs/operators/attention-varlen.md
  • rl_engine/kernels/ops/triton/triton_attn.py
  • tests/test_triton_attention_varlen.py

Comment thread rl_engine/kernels/ops/triton/triton_attn.py
_bwd_preprocess_varlen indexed DO with Out's strides, and _bwd_kernel_varlen
indexed DO with Q's strides. do.contiguous() only guarantees DO's own
contiguous layout -- it does not make DO's strides equal to Out's or Q's,
which may be non-contiguous (e.g. a transposed view). The mismatch produced
wrong gradients (or out-of-bounds reads) whenever Q/Out were non-contiguous.

Fixes by plumbing do.stride(*) through both kernels, matching how the
pre-existing dense _bwd_preprocess/_bwd_kernel already keep DO's strides
separate from Out's.

Added a regression test building q/k/v as transposed (non-contiguous) views;
confirmed it fails against the prior code (85% of dq elements wrong, max
abs diff 2.3) and passes with this fix.
@Flink-ddd

Copy link
Copy Markdown
Collaborator

Thank you for your contribution. Could you add some benchmark tests for the PR content? This will provide some convenience for the reviewers. Thanks!

…varlen

Compares triton_flash_attention (return_lse=True/False) against torch SDPA
on dense tensors, and triton_flash_attention_varlen against a pad-mask-unpad
SDPA baseline on packed cu_seqlens tensors -- the realistic naive
alternative for RL rollout batches with skewed per-sequence lengths.
Reports forward and forward+backward latency plus peak VRAM.

Requested by a reviewer on RL-Align#233 for easier review of the perf tradeoffs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Billy1900

Copy link
Copy Markdown
Author

Benchmark results

Added benchmarks/benchmark_triton_attention_varlen.py (466be55) covering both additions in this PR:

  • triton_flash_attention(..., return_lse=True/False) vs. torch.nn.functional.scaled_dot_product_attention on dense tensors
  • triton_flash_attention_varlen vs. a pad-mask-unpad SDPA baseline on packed cu_seqlens tensors — the realistic naive alternative for RL rollout batches with skewed per-sequence lengths

Ran on H100 SXM5 (--iters 10 --warmup 3, default configs):

Dense (causal):

shape (B×H×S×D) sdpa fwd ms triton fwd ms triton fwd+lse ms fwd speedup sdpa f+b ms triton f+b ms f+b speedup
2x8x512x64 0.018 0.060 0.071 0.30x 0.655 1.816 0.36x
2x8x2048x64 0.067 0.283 0.322 0.24x 1.172 2.276 0.52x
1x16x4096x128 0.330 0.325 0.332 1.02x 1.161 2.123 0.55x
1x16x8192x128 1.009 1.113 1.107 0.91x 3.683 5.586 0.66x

Native SDPA (cuDNN/flash backend) is faster at small/medium shapes and roughly at parity at head_dim=128, as expected — this kernel is the cross-platform semantic baseline, not a vendor-tuned kernel. return_lse=True adds only ~5-15% over return_lse=False, confirming the LSE export is nearly free since M/L were already computed online.

Packed varlen (causal, H=8, D=64):

config batch tokens (pad waste) pad+sdpa fwd ms triton varlen fwd ms fwd speedup pad+sdpa f+b ms triton varlen f+b ms f+b speedup
uniform_512 8 4096 (0%) 1.175 0.139 8.45x 10.054 2.496 4.03x
skewed_short_long 8 10976 (66%) 3.864 0.301 12.85x 13.853 2.647 5.23x
many_short_few_long 8 4464 (86%) 4.216 0.210 20.10x 13.460 2.631 5.12x
with_empty_seq 8 10468 (63%) 3.300 0.256 12.90x 10.879 2.685 4.05x

This is where packing pays off — up to 20x forward speedup and several-x memory savings, growing with how skewed the batch's length distribution is. The zero-length-sequence case (with_empty_seq) ran cleanly, no NaN/crash.

Reproduce with:

python benchmarks/benchmark_triton_attention_varlen.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
benchmarks/benchmark_triton_attention_varlen.py (1)

156-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind loop-scoped variables as closure defaults (Ruff B023). The benchmark closures already bind q/k/v via default args to freeze them per iteration, but leave sm_scale (dense) and seqlens/cu_seqlens/max_seqlen (varlen) captured by reference. This is currently harmless because each closure is invoked within the same loop iteration through _time_ms, but it's inconsistent with the existing binding style, defends against future refactors that store/defer these closures, and clears the Ruff B023 warnings.

  • benchmarks/benchmark_triton_attention_varlen.py#L156-L176: add sm_scale=sm_scale to triton_fwd, triton_fwd_lse, and triton_fwd_bwd signatures.
  • benchmarks/benchmark_triton_attention_varlen.py#L235-L259: add seqlens=seqlens to pad_fwd/pad_fwd_bwd and cu_seqlens=cu_seqlens, max_seqlen=max_seqlen to triton_fwd/triton_fwd_bwd.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/benchmark_triton_attention_varlen.py` around lines 156 - 176, Add
closure-default bindings for all loop-scoped variables: in
benchmarks/benchmark_triton_attention_varlen.py lines 156-176, bind
sm_scale=sm_scale in triton_fwd, triton_fwd_lse, and triton_fwd_bwd; in lines
235-259, bind seqlens=seqlens in pad_fwd and pad_fwd_bwd, and
cu_seqlens=cu_seqlens plus max_seqlen=max_seqlen in triton_fwd and
triton_fwd_bwd, preserving the existing q/k/v bindings.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@benchmarks/benchmark_triton_attention_varlen.py`:
- Around line 156-176: Add closure-default bindings for all loop-scoped
variables: in benchmarks/benchmark_triton_attention_varlen.py lines 156-176,
bind sm_scale=sm_scale in triton_fwd, triton_fwd_lse, and triton_fwd_bwd; in
lines 235-259, bind seqlens=seqlens in pad_fwd and pad_fwd_bwd, and
cu_seqlens=cu_seqlens plus max_seqlen=max_seqlen in triton_fwd and
triton_fwd_bwd, preserving the existing q/k/v bindings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 365bd06d-2e62-4c8c-a84b-f3b9722b3ef3

📥 Commits

Reviewing files that changed from the base of the PR and between 8276596 and 466be55.

📒 Files selected for processing (1)
  • benchmarks/benchmark_triton_attention_varlen.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants