Skip to content

[perf]: register a real backward for FA2 default + masked/varlen custom ops (training-under-compile) - #1388

Merged
mergify[bot] merged 6 commits into
hao-ai-lab:mainfrom
Mister-Raggs:perf/flash-attn-fa2-real-backward
Jul 12, 2026
Merged

[perf]: register a real backward for FA2 default + masked/varlen custom ops (training-under-compile)#1388
mergify[bot] merged 6 commits into
hao-ai-lab:mainfrom
Mister-Raggs:perf/flash-attn-fa2-real-backward

Conversation

@Mister-Raggs

@Mister-Raggs Mister-Raggs commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds register_autograd parity to the FA2 default-path and masked/varlen-path custom ops, so dynamo sees a traceable node on both inference and training paths through attention. Mirrors the FP4 cute template's 4-piece pattern (forward + register_fake + setup_context + register_autograd) and follows up on the direction confirmed in Slack ("training-under-compile is definitely wanted").

Prerequisites

Stacks on #1373 (the FA2/FA3 default custom-op wrapper + the CI-unblocking autograd carve-out). The diff against main currently includes #1373's commits; will rebase onto main once #1373 merges.

Scope (FA2 only — FA3 is a separate follow-up)

Path Status before Status after
FA2 default flash_attn_func custom op, no backward → carve-out routes grads to original autograd.Function (graph break on training) full register_autograd; training also traceable
FA2 masked flash_attn_no_pad unwrapped (graph break on every call) custom op with full register_autograd
FA2 varlen flash_attn_varlen_qk_no_pad (cross-attn) unwrapped (graph break) custom op with full register_autograd
FA3 default / masked / varlen same as before #1373 / carve-out unchanged — separate PR gated on Hopper validation
FA4 (cute) already custom op unchanged

The carve-out (flash_attn_func_compilable checking is_grad_enabled and requires_grad) is removed for FA2 — autograd flows through the op like inference does.

Mechanism (the non-trivial bit)

FA2's varlen kernels return softmax_lse in unpadded form [nheads, total_q]. To keep the custom op's outputs statically-shaped (so register_fake matches dynamo's expectation), we pad lse to [batch, nheads, seqlen] on the way out and re-unpad it in backward using the saved mask. Backward then re-unpads qkv / out / dout, allocates dq / dk / dv on the unpadded form, calls flash_attn.flash_attn_interface._flash_attn_varlen_backward, and re-pads gradients back. softmax_scale=None is resolved to head_dim ** -0.5 in setup_context (FA2's backward demands a concrete float).

Validation matrix

Gate Result
FA2 default-path unit tests (16 cases) ✅ A100, fp16+bf16, causal T/F, inference-parity atol=0 rtol=0, grad through op matches original autograd.Function, opcheck × {with grad, without grad}
FA2 masked/varlen unit tests (12 cases) ✅ A100, both ops × inference-parity + grad-through-op + opcheck × grad+no-grad
Wan2.1-T2V-1.3B end-to-end SSIM (default path) MIN=1.000000 49/49 frames
HunyuanVideo-1.5 end-to-end SSIM (masked path) ⚠️ Deferred — blocked by upstream bug #1387 (text_encoding.py:221 crashes on HV15's dual-encoder preprocess for both t2v and i2v variants); HV15 is the only model in FastVideo that exercises the masked branch. Unit-test bit-exactness (atol=0 rtol=0) is the proxy until that issue is fixed.

Test command:

pytest fastvideo/tests/attention/test_flash_attn_default_custom_op.py \
       fastvideo/tests/attention/test_flash_attn_no_pad_custom_op.py -v
# 28/28 expected on FA2 + CUDA boxes; skips on FA3/FA4

Inference perf A/B (per maintainer request)

Wan2.1-T2V-1.3B, RTX A5000, bf16, sp=1, 480×832 / 49 frames / 20 steps. Per condition: 3 eager + 4 compile runs, 1 warmup discarded, median of measured.

Compile (enable_torch_compile=True):

Warmup Median wall
Pre-#1373 (3a67319c) 60.4 s 50.8 s
Post-#1373 main (2f3ca8aa) 53.8 s 49.8 s
This PR (3626db1b) 52.3 s 49.3 s

Cumulative (pre-#1373 → this PR): median −3.0%, warmup −13.5%.

The per-iteration delta is small — Wan's e2e is already compile-squeezed (Kuan's profile bounds compile-payoff at ~few-%). The clearer signal is the −13.5% warmup: removing the graph break at the FA call site shrinks the compiled region, so dynamo has less to trace.

Eager (no compile): 59.2 / 59.0 / 59.1 s — flat. The wrapping adds zero measurable overhead on the no-compile path.

SSIM (this PR vs pre-#1373): MEAN = MIN = MAX = 1.000000 in both eager and compile modes. Bit-identical output across conditions.

This PR's marginal benefit over #1373 on Wan is sub-noise (~1%) — expected. Wan doesn't take the masked/varlen branch this PR wraps; that benefit fires only on HunyuanVideo-1.5, which is blocked by upstream #1387. The full register_autograd for the default path is invisible to inference (autograd doesn't run under inference_mode) — its payoff is on the training-under-compile path, validated structurally by the 28-unit-test suite (grad-through-op parity) but not measured here.

What this PR does NOT measure end-to-end

  • Training-under-compile inference wall — the FA2 register_autograd is the centerpiece of this PR for the training path, but a training-loop wall measurement is out of scope here. The 28-unit-test suite validates grad-through-op parity + opcheck with grad inputs, which is the correctness floor.
  • HunyuanVideo-1.5 e2e perf — the masked/varlen wrapping only fires on HV15, currently blocked by upstream issue [bug]: HunyuanVideo-1.5 generation crashes at text_encoding.py:221 — 'list' object has no attribute 'strip' #1387. The unit-test bit-exactness (atol=0 rtol=0) is the proxy.

Files

  • fastvideo/attention/backends/flash_attn.py — FA2 default custom op gets backward + setup_context + register_autograd; FA3 path keeps carve-out; FA4 path unchanged; masked-branch call site routes through *_compilable dispatchers.
  • fastvideo/attention/utils/flash_attn_no_pad.py — wraps flash_attn_no_pad and flash_attn_varlen_qk_no_pad as custom ops with full FA2 register_autograd (lse padding + re-unpad on backward); FA3/FA4 keep carve-out.
  • fastvideo/tests/attention/test_flash_attn_default_custom_op.py — extended.
  • fastvideo/tests/attention/test_flash_attn_no_pad_custom_op.py — new.

Copilot AI review requested due to automatic review settings May 23, 2026 04:49
@mergify mergify Bot added type: perf Performance improvement scope: attention Attention backends (VSA, STA, Flash, etc.) scope: infra CI, tests, Docker, build labels May 23, 2026
@mergify

mergify Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 PR merge requirements

  • #approved-reviews-by>=1
  • check-success=fastcheck-passed
  • check-success=full-suite-passed
  • check-success~=pre-commit
  • title~=(?i)^\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\]

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces torch.library.custom_op wrappers for FlashAttention (FA2/FA3) and masked/varlen attention paths to improve torch.compile traceability and prevent graph breaks. For FA2, full autograd support is registered, while FA3/FA4 utilize a carve-out pattern for training. New regression tests are added to ensure numerical consistency and autograd correctness. Feedback focuses on optimizing the backward passes by reusing computed indices to avoid redundant unpad_input calls.

Comment on lines +287 to +296
out_unpad = rearrange(
unpad_input(rearrange(out_padded, "b s h d -> b s (h d)"), key_padding_mask)[0],
"nnz (h d) -> nnz h d", h=h).contiguous()
dout_unpad = rearrange(
unpad_input(rearrange(grad_out, "b s h d -> b s (h d)"), key_padding_mask)[0],
"nnz (h d) -> nnz h d", h=h).contiguous()

# Re-unpad lse: [b, h, s] -> [b, s, h] -> [nnz, h] -> [h, nnz].
lse_unpad = unpad_input(lse_padded.permute(0, 2, 1).contiguous(),
key_padding_mask)[0].t().contiguous()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The code calls unpad_input multiple times for out_padded, grad_out, and lse_padded. Since indices is already computed in line 282 using the same key_padding_mask, you can avoid the overhead of unpad_input (which involves finding non-zeros and computing prefix sums) by directly indexing the flattened tensors. Additionally, the .contiguous() calls on the results of unpad_input are redundant as indexing already returns a contiguous copy.

        out_unpad = out_padded.flatten(0, 1)[indices].view(-1, h, d)
        dout_unpad = grad_out.flatten(0, 1)[indices].view(-1, h, d)

        # Re-unpad lse: [b, h, s] -> [b, s, h] -> [nnz, h] -> [h, nnz].
        lse_unpad = lse_padded.permute(0, 2, 1).contiguous().flatten(0, 1)[indices].t().contiguous()

Comment on lines +352 to +355
k_unpad, _, cu_seqlens_k, max_seqlen_k, _ = unpad_input(
rearrange(key, "b s h d -> b s (h d)"), key_padding_mask)
v_unpad, _, _, _, _ = unpad_input(
rearrange(value, "b s h d -> b s (h d)"), key_padding_mask)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

key_unpad and v_unpad use the same key_padding_mask. You can compute k_indices once and reuse it to unpad value, avoiding a redundant unpad_input call.

        k_unpad, k_indices, cu_seqlens_k, max_seqlen_k, _ = unpad_input(
            rearrange(key, "b s h d -> b s (h d)"), key_padding_mask)
        v_unpad = rearrange(value, "b s h d -> (b s) (h d)")[k_indices]

Comment on lines +417 to +431
v_unpad, _, _, _, _ = unpad_input(
rearrange(value, "b s h d -> b s (h d)"), key_padding_mask)
q_unpad = rearrange(q_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
k_unpad = rearrange(k_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
v_unpad = rearrange(v_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()

# out and lse follow q's shape, so re-unpad with q_mask.
out_unpad = rearrange(
unpad_input(rearrange(out_padded, "b s h d -> b s (h d)"), query_padding_mask)[0],
"nnz (h d) -> nnz h d", h=h).contiguous()
dout_unpad = rearrange(
unpad_input(rearrange(grad_out, "b s h d -> b s (h d)"), query_padding_mask)[0],
"nnz (h d) -> nnz h d", h=h).contiguous()
lse_unpad = unpad_input(lse_padded.permute(0, 2, 1).contiguous(),
query_padding_mask)[0].t().contiguous()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In the cross-attention backward, v_unpad can reuse k_indices, and out_unpad, dout_unpad, and lse_unpad can all reuse q_indices. This avoids multiple redundant calls to unpad_input and its internal non-zero finding logic.

        v_unpad = rearrange(value, "b s h d -> (b s) (h d)")[k_indices]
        q_unpad = rearrange(q_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
        k_unpad = rearrange(k_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
        v_unpad = rearrange(v_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()

        # out and lse follow q's shape, so re-unpad with q_mask.
        out_unpad = out_padded.flatten(0, 1)[q_indices].view(-1, h, d)
        dout_unpad = grad_out.flatten(0, 1)[q_indices].view(-1, h, d)
        lse_unpad = lse_padded.permute(0, 2, 1).contiguous().flatten(0, 1)[q_indices].t().contiguous()

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

Note

Copilot was unable to run its full agentic suite in this review.

Adds torch.compile-friendly custom-op wrappers around FlashAttention default and masked/varlen paths, with FA2-only register_autograd to keep training graphable, and introduces regression tests to lock in inference/training equivalence + op schema/fake consistency.

Changes:

  • Wrap FA2/FA3 default-path flash_attn_func behind a torch.library.custom_op (FA2: real backward; FA3: carve-out for grads).
  • Add masked/varlen custom ops (flash_attn_no_pad*) with FA2 register_autograd and FA3/FA4 carve-out behavior.
  • Add CUDA-only regression tests validating inference equality, backward correctness (FA2), and torch.library.opcheck.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.

File Description
fastvideo/attention/backends/flash_attn.py Introduces default-path custom op + FA2 autograd registration; routes backend calls through compilable wrappers.
fastvideo/attention/utils/flash_attn_no_pad.py Adds masked/varlen custom ops, FA2 backward wiring via private FA2 varlen backward, and compilable dispatchers.
fastvideo/tests/attention/test_flash_attn_default_custom_op.py New CUDA regression tests for default-path custom op behavior (inference, training, opcheck; FA2-only backward tests).
fastvideo/tests/attention/test_flash_attn_no_pad_custom_op.py New CUDA regression tests for masked/varlen custom ops (inference, FA2 backward, opcheck incl. autograd registration).

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

v: torch.Tensor,
softmax_scale: float | None,
causal: bool,
) -> tuple[torch.Tensor, torch.Tensor]:
v: torch.Tensor,
softmax_scale: float | None,
causal: bool,
) -> torch.Tensor:
dropout_p: float,
softmax_scale: float | None,
deterministic: bool,
) -> tuple[torch.Tensor, torch.Tensor]:
# third party's use or distribution of any of the Tencent Hunyuan works or outputs and your exercise
# of rights and permissions under this agreement.
# See the License for the specific language governing permissions and limitations under the License.

dropout_p: float,
softmax_scale: float | None,
deterministic: bool,
) -> torch.Tensor:
Comment on lines +405 to +406
def _flash_attn_varlen_qk_no_pad_backward(ctx, grad_out, grad_lse):
del grad_lse
Comment on lines +280 to +282
# Re-unpad qkv -> q, k, v unpadded ([nnz, h, d] each).
x = rearrange(qkv, "b s three h d -> b s (three h d)")
x_unpad, indices, cu_seqlens, max_s, _ = unpad_input(x, key_padding_mask)
Comment on lines +316 to +317
def _repad(dt_unpad):
padded = pad_input(rearrange(dt_unpad, "nnz h d -> nnz (h d)"), indices, b, s)
with torch.inference_mode():
out_ref = mod.flash_attn_no_pad(qkv, mask, causal=False, dropout_p=0.0)
out_test = mod.flash_attn_no_pad_compilable(qkv, mask, causal=False, dropout_p=0.0)
torch.testing.assert_close(out_test, out_ref, atol=0, rtol=0)
with torch.inference_mode():
out_ref = mod.flash_attn_varlen_qk_no_pad(q, k, v, qmask, kmask, causal=False, dropout_p=0.0)
out_test = mod.flash_attn_varlen_qk_no_pad_compilable(q, k, v, qmask, kmask, causal=False, dropout_p=0.0)
torch.testing.assert_close(out_test, out_ref, atol=0, rtol=0)
Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request May 23, 2026
…e indices in backward

Two improvements, both from the gemini-code-assist + Copilot reviews on hao-ai-lab#1388:

(a) Mark `softmax_lse` non-differentiable in all 3 custom-op setup_contexts
    (default + masked + varlen_qk). We return lse alongside out so it can be
    saved for backward; nobody should differentiate through it. `del grad_lse`
    in backward silently drops grads if a caller wires lse into a loss —
    `ctx.mark_non_differentiable(lse)` makes autograd error loudly instead.
    The public `*_compilable` dispatchers already drop lse (`out, _ = op(...)`),
    so this only matters for callers using `torch.ops.fastvideo._*_forward`
    directly, but it's free defensive hygiene.

(b) Stop re-running `unpad_input` for tensors that share a mask. The first
    `unpad_input(mask)` call returns `indices` + `cu_seqlens` + `max_s`;
    subsequent unpads for out/dout/lse on the same mask just re-derived
    the same indices (and the `.max().item()` call inside each unpad does
    a GPU→CPU sync). Replace those with `tensor.flatten(0,1)[indices].view(...)`,
    which is what `unpad_input` does internally anyway:

      - flash_attn_no_pad backward: 4 unpad_input calls -> 1
      - flash_attn_varlen_qk_no_pad backward: 6 unpad_input calls -> 2
        (one per distinct mask; v reuses k_indices, out/dout/lse reuse
        q_indices, the final repad of dk/dv reuses k_indices instead of
        recomputing).

Semantics are preserved — `unpad_input(x, mask)[0]` is equivalent to
`x.flatten(0,1)[indices]` for the same mask. The existing 28 unit tests
(inference parity atol=0 rtol=0, training-backward parity, opcheck with
and without grad inputs, all on FA2 + A100) cover the regressions.
@Mister-Raggs

Copy link
Copy Markdown
Contributor Author

Two threads from the automated review worth addressing in narrative, since per-line replies don't capture the full reasoning. (The other threads — mark_non_differentiable(lse) and reusing indices across unpad_input calls in backward — are addressed in commit c58b736.)

Version-dependent op schema

The same custom-op name registers different schemas across FA versions — FA2 returns (out, lse), FA3 returns out. Only one branch ever imports per process, so within any given process the op has a single stable schema and there's no within-process ambiguity. The brittleness is real only for exported graphs (torch.export / AOTInductor artifacts) that cross FA-version boundaries — an artifact cooked on FA2 with (out, lse) wouldn't load on an FA3 box where the op declares out.

Three options weighed:

  1. Versioned op namesfastvideo::_flash_attn_default_fa2_forward, …_fa3_forward. Cleanest separation, more code, more churn.
  2. Unified schema with FA3 returning a placeholder lse — keeps one schema, but forces FA3 to expose an lse it doesn't use (FA3's path is still the autograd carve-out from [perf]: register FA2/FA3 default flash_attn_func as a torch.library custom op #1373; nothing consumes lse there).
  3. Accept the version-dependent schema — what's checked in. Documented in the file-level comment; the only brittleness is for graph-export use cases that aren't current FastVideo workflows.

Picked (3). If the FA3 leg lands a real backward later (gated on a Hopper box for grad-check), it'll converge on (out, lse) and the schema becomes uniform across FA2/FA3 naturally. Happy to refactor to (1) up front if cleaner separation is preferred — let me know.

Inference-parity test asserts atol=0, rtol=0

This is bit-identical by construction, not aspirationally. The custom-op forward calls the same flash_attn_func as the reference, only toggling return_attn_probs=True to extract softmax_lse for backward. FA's forward kernel computes softmax_lse as a side-product of the online softmax regardless — return_attn_probs controls whether it's returned, not whether it's computed. Same kernel, same inputs, same dtype → identical out.

Validated on A100 + FA2 v2.8.1 across fp16/bf16 × causal {True, False} — 16 cases on the default path, 4 on each masked/varlen op, all pass with atol=0, rtol=0.

The training-backward parity tests do allow numerical tolerance (atol = rtol = 6e-3 for fp16, 2e-2 for bf16) — that's where independent dq/dk/dv allocations + ordering differences between the registered backward and the original autograd.Function legitimately introduce small drift. Forward bit-equality is the stronger regression alarm to keep tight; if it ever loses bits, the wrapping changed something it wasn't meant to and we want to know.

@mergify

mergify Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase:

git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts label May 23, 2026
@Mister-Raggs
Mister-Raggs force-pushed the perf/flash-attn-fa2-real-backward branch from c58b736 to 3626db1 Compare May 23, 2026 08:42
Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request May 23, 2026
…e indices in backward

Two improvements, both from the gemini-code-assist + Copilot reviews on hao-ai-lab#1388:

(a) Mark `softmax_lse` non-differentiable in all 3 custom-op setup_contexts
    (default + masked + varlen_qk). We return lse alongside out so it can be
    saved for backward; nobody should differentiate through it. `del grad_lse`
    in backward silently drops grads if a caller wires lse into a loss —
    `ctx.mark_non_differentiable(lse)` makes autograd error loudly instead.
    The public `*_compilable` dispatchers already drop lse (`out, _ = op(...)`),
    so this only matters for callers using `torch.ops.fastvideo._*_forward`
    directly, but it's free defensive hygiene.

(b) Stop re-running `unpad_input` for tensors that share a mask. The first
    `unpad_input(mask)` call returns `indices` + `cu_seqlens` + `max_s`;
    subsequent unpads for out/dout/lse on the same mask just re-derived
    the same indices (and the `.max().item()` call inside each unpad does
    a GPU→CPU sync). Replace those with `tensor.flatten(0,1)[indices].view(...)`,
    which is what `unpad_input` does internally anyway:

      - flash_attn_no_pad backward: 4 unpad_input calls -> 1
      - flash_attn_varlen_qk_no_pad backward: 6 unpad_input calls -> 2
        (one per distinct mask; v reuses k_indices, out/dout/lse reuse
        q_indices, the final repad of dk/dv reuses k_indices instead of
        recomputing).

Semantics are preserved — `unpad_input(x, mask)[0]` is equivalent to
`x.flatten(0,1)[indices]` for the same mask. The existing 28 unit tests
(inference parity atol=0 rtol=0, training-backward parity, opcheck with
and without grad inputs, all on FA2 + A100) cover the regressions.
Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request May 23, 2026
Addresses the S2 finding from the post-merge review on hao-ai-lab#1373: per FastVideo
precedent (fastvideo/attention/utils/flash_attn_cute.py — the FP4 cute
custom-op wrapper hao-ai-lab#1388 says it mirrors) and vLLM's similar pattern
(vllm/utils/flashinfer.py), per-kernel custom-op wrappers live in
attention/utils/, not inline in the backend dispatcher.

Move the entire `if fa_version == "2": ... elif "3": ... elif "4": ... else:`
block — including the FA version probe, custom_op + register_fake +
setup_context + register_autograd, and `flash_attn_func_compilable` —
from `attention/backends/flash_attn.py` to a new sibling at
`attention/utils/flash_attn_default.py`. The backend now imports
`fa_version` and `flash_attn_func_compilable` from there; the rest of the
backend (FlashAttentionBackend, FlashAttentionImpl, FP4 quantize helpers,
masked-branch routing) is unchanged.

Benefits:
  - Internal consistency with flash_attn_cute.py (the FP4 template the
    file-level comment claims to mirror) and flash_attn_no_pad.py (the
    masked/varlen wrappers, already in utils/).
  - A future backend that wants the same traceable FA2/FA3/FA4 op can
    import from utils/ without depending on the FA backend's dispatcher.
  - Cleaner separation between backend registry/dispatch logic and
    per-kernel custom-op plumbing.

No semantic change. Verified there is exactly one registration of
`fastvideo::_flash_attn_default_forward` after the move (grep clean).
Update `test_flash_attn_default_custom_op.py` to import from the utils
module (the test referenced `fa_backend._fa_default`, a name that lived
inside the moved block).
@Mister-Raggs

Copy link
Copy Markdown
Contributor Author

Force-pushed 3626db1b — rebased onto main now that #1373 has merged, and addressed the remaining S2 finding from Gob's post-merge review on #1373.

S2 (file placement): Moved the entire if fa_version == "2": ... elif "3": ... elif "4": block (probe + custom_op + register_fake + setup_context + register_autograd + dispatcher) from attention/backends/flash_attn.py to a new sibling at attention/utils/flash_attn_default.py. The backend now imports fa_version and flash_attn_func_compilable from there; everything from FlashAttentionBackend onwards is unchanged. Verified single registration of fastvideo::_flash_attn_default_forward after the move (grep clean — no duplicate-registration risk). Updated test_flash_attn_default_custom_op.py to import from where the symbols now live.

Rebase conflict: tiny — took main's version (long carve-out comment + yapf-clean single-line if) for the FA3 carve-out block; no semantic change.

Commits on top of clean main:

  • ce044a77 FA2 default real-backward
  • aad28dd0 FA2 masked/varlen real-backward
  • 6044df30 mark_non_differentiable(lse) + reuse indices in backward (the prior gemini/Copilot suggestions)
  • 3626db1b move FA default custom-op registration to attention/utils/

Re-validation on GPU is pending. The 28-unit-test suite was last green on A100 + FA2 v2.8.1 at the pre-rebase tip; 3626db1b is a pure file-move + import-path change with byte-identical kernel-level logic, so I'd expect a re-run to stay green.

@mergify mergify Bot removed the needs-rebase PR has merge conflicts label May 23, 2026
@Mister-Raggs

Copy link
Copy Markdown
Contributor Author

Updated the PR body with the inference perf A/B numbers (per the perf request). tl;dr:

Full numbers + interpretation in the body. The wrapping does what it claims structurally — graph break removed, compile region shrinks (the warmup drop is the cleanest signal of that), output bit-identical. The per-iter delta is small because Wan inference is already compile-squeezed (per Kuan's profile).

@mergify

mergify Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase:

git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts label May 23, 2026
Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request May 28, 2026
…e indices in backward

Two improvements, both from the gemini-code-assist + Copilot reviews on hao-ai-lab#1388:

(a) Mark `softmax_lse` non-differentiable in all 3 custom-op setup_contexts
    (default + masked + varlen_qk). We return lse alongside out so it can be
    saved for backward; nobody should differentiate through it. `del grad_lse`
    in backward silently drops grads if a caller wires lse into a loss —
    `ctx.mark_non_differentiable(lse)` makes autograd error loudly instead.
    The public `*_compilable` dispatchers already drop lse (`out, _ = op(...)`),
    so this only matters for callers using `torch.ops.fastvideo._*_forward`
    directly, but it's free defensive hygiene.

(b) Stop re-running `unpad_input` for tensors that share a mask. The first
    `unpad_input(mask)` call returns `indices` + `cu_seqlens` + `max_s`;
    subsequent unpads for out/dout/lse on the same mask just re-derived
    the same indices (and the `.max().item()` call inside each unpad does
    a GPU→CPU sync). Replace those with `tensor.flatten(0,1)[indices].view(...)`,
    which is what `unpad_input` does internally anyway:

      - flash_attn_no_pad backward: 4 unpad_input calls -> 1
      - flash_attn_varlen_qk_no_pad backward: 6 unpad_input calls -> 2
        (one per distinct mask; v reuses k_indices, out/dout/lse reuse
        q_indices, the final repad of dk/dv reuses k_indices instead of
        recomputing).

Semantics are preserved — `unpad_input(x, mask)[0]` is equivalent to
`x.flatten(0,1)[indices]` for the same mask. The existing 28 unit tests
(inference parity atol=0 rtol=0, training-backward parity, opcheck with
and without grad inputs, all on FA2 + A100) cover the regressions.
Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request May 28, 2026
Addresses the S2 finding from the post-merge review on hao-ai-lab#1373: per FastVideo
precedent (fastvideo/attention/utils/flash_attn_cute.py — the FP4 cute
custom-op wrapper hao-ai-lab#1388 says it mirrors) and vLLM's similar pattern
(vllm/utils/flashinfer.py), per-kernel custom-op wrappers live in
attention/utils/, not inline in the backend dispatcher.

Move the entire `if fa_version == "2": ... elif "3": ... elif "4": ... else:`
block — including the FA version probe, custom_op + register_fake +
setup_context + register_autograd, and `flash_attn_func_compilable` —
from `attention/backends/flash_attn.py` to a new sibling at
`attention/utils/flash_attn_default.py`. The backend now imports
`fa_version` and `flash_attn_func_compilable` from there; the rest of the
backend (FlashAttentionBackend, FlashAttentionImpl, FP4 quantize helpers,
masked-branch routing) is unchanged.

Benefits:
  - Internal consistency with flash_attn_cute.py (the FP4 template the
    file-level comment claims to mirror) and flash_attn_no_pad.py (the
    masked/varlen wrappers, already in utils/).
  - A future backend that wants the same traceable FA2/FA3/FA4 op can
    import from utils/ without depending on the FA backend's dispatcher.
  - Cleaner separation between backend registry/dispatch logic and
    per-kernel custom-op plumbing.

No semantic change. Verified there is exactly one registration of
`fastvideo::_flash_attn_default_forward` after the move (grep clean).
Update `test_flash_attn_default_custom_op.py` to import from the utils
module (the test referenced `fa_backend._fa_default`, a name that lived
inside the moved block).
@Mister-Raggs
Mister-Raggs force-pushed the perf/flash-attn-fa2-real-backward branch from 3626db1 to 90e35c9 Compare May 28, 2026 16:07
@mergify mergify Bot removed the needs-rebase PR has merge conflicts label May 28, 2026
@alexzms
alexzms self-requested a review June 10, 2026 22:58

@alexzms alexzms left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did an independent pass on this — ran the two new test files on an H200. Forcing FA2 (blocking the FA3/cute probes so fa_version == "2"), I get 28/28 passing, so the core register_autograd work checks out end-to-end. Two things I'd like addressed before approving 👇

1. register_fake for the varlen-qk op uses the query's head_dim instead of the value's

In fastvideo/attention/utils/flash_attn_no_pad.py, _flash_attn_varlen_qk_no_pad_forward_fake does:

out = query.new_empty(query.shape)

but the real forward's out takes its head_dim from value (the kernel returns out_unpad with d_v, and out_padded is [b, sq, h, d_v]). The default-path fake gets this right — q.new_empty(b, sq, hq, v.shape[-1]) — so the two are inconsistent. With d_q == d_v (all current models) it's harmless, but if d_v != d_q the fake reports the wrong output shape, which under torch.compile means dynamo traces a wrong shape for this op (downstream shape mismatch / guard failure) — exactly the case register_fake exists to cover. Suggested fix:

b, sq, h, _ = query.shape
out = query.new_empty(b, sq, h, value.shape[-1])   # don't `del value` before reading its shape

2. Inconsistent FA2 gating in the new tests — they fail on Hopper + recent flash_attn_3

The _FA_VARLEN_VERSION != "2" skip is only applied to the autograd tests; test_varlen_qk_inference_matches_original and test_varlen_qk_forward_opcheck aren't gated. On an H200 with flash_attn_3==3.0.0 installed (probe resolves to FA3), those two run instead of skipping and fail:

TypeError: flash_attn_varlen_func() got an unexpected keyword argument 'dropout_p'
  fastvideo/attention/utils/flash_attn_no_pad.py:166

To be clear: the root cause is pre-existing on main — the FA3 varlen path passes dropout_p, which recent flash_attn_3 dropped — not introduced here. But the new ungated tests surface it, so any Hopper box on a current FA3 will go red (CI stayed green presumably because it has no / an older FA3). Could you gate the inference + opcheck tests on FA2 the same way the autograd ones are, so they skip cleanly off-FA2? The underlying FA3 dropout_p incompatibility is worth a separate fix, but it's out of scope for this PR.

Everything else looks good — nice work on the lse pad/unpad round-trip and the non-diff handling.

@Mister-Raggs

Copy link
Copy Markdown
Contributor Author

Thanks for the careful pass! Both addressed in b1846a1:

  1. Fixed — the varlen-qk register_fake now builds out with value's head_dim (query.new_empty(b, sq, h, value.shape[-1])) in both the FA2 and FA3 fakes, and no longer dels value before reading its shape.
  2. Gated test_varlen_qk_inference_matches_original and test_varlen_qk_forward_opcheck on _fa2_only, matching the autograd tests, so they skip cleanly off FA2. Agreed the FA3 varlen dropout_p incompatibility is a separate, pre-existing fix — out of scope here.

Also typed the two new _repad backward closures (-> Tensor) while in here so pre-commit mypy is clean on the new code.

@alexzms alexzms left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Thanks for iterative contribution!

@alexzms

alexzms commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

/merge

@github-actions github-actions Bot added the ready PR is ready to merge label Jun 30, 2026

@alexzms alexzms left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Both fixes verified on H200 — the varlen-qk fake now uses value's head_dim (and the FA3 fake too), and the inference/opcheck tests gate on FA2 cleanly. Re-ran the suite: default probe (FA3) now 0 failed / 13 passed / 15 skipped, and forced-FA2 still 28/28. Thanks for the quick turnaround! LGTM.

Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request Jul 2, 2026
…e indices in backward

Two improvements, both from the gemini-code-assist + Copilot reviews on hao-ai-lab#1388:

(a) Mark `softmax_lse` non-differentiable in all 3 custom-op setup_contexts
    (default + masked + varlen_qk). We return lse alongside out so it can be
    saved for backward; nobody should differentiate through it. `del grad_lse`
    in backward silently drops grads if a caller wires lse into a loss —
    `ctx.mark_non_differentiable(lse)` makes autograd error loudly instead.
    The public `*_compilable` dispatchers already drop lse (`out, _ = op(...)`),
    so this only matters for callers using `torch.ops.fastvideo._*_forward`
    directly, but it's free defensive hygiene.

(b) Stop re-running `unpad_input` for tensors that share a mask. The first
    `unpad_input(mask)` call returns `indices` + `cu_seqlens` + `max_s`;
    subsequent unpads for out/dout/lse on the same mask just re-derived
    the same indices (and the `.max().item()` call inside each unpad does
    a GPU→CPU sync). Replace those with `tensor.flatten(0,1)[indices].view(...)`,
    which is what `unpad_input` does internally anyway:

      - flash_attn_no_pad backward: 4 unpad_input calls -> 1
      - flash_attn_varlen_qk_no_pad backward: 6 unpad_input calls -> 2
        (one per distinct mask; v reuses k_indices, out/dout/lse reuse
        q_indices, the final repad of dk/dv reuses k_indices instead of
        recomputing).

Semantics are preserved — `unpad_input(x, mask)[0]` is equivalent to
`x.flatten(0,1)[indices]` for the same mask. The existing 28 unit tests
(inference parity atol=0 rtol=0, training-backward parity, opcheck with
and without grad inputs, all on FA2 + A100) cover the regressions.
Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request Jul 2, 2026
Addresses the S2 finding from the post-merge review on hao-ai-lab#1373: per FastVideo
precedent (fastvideo/attention/utils/flash_attn_cute.py — the FP4 cute
custom-op wrapper hao-ai-lab#1388 says it mirrors) and vLLM's similar pattern
(vllm/utils/flashinfer.py), per-kernel custom-op wrappers live in
attention/utils/, not inline in the backend dispatcher.

Move the entire `if fa_version == "2": ... elif "3": ... elif "4": ... else:`
block — including the FA version probe, custom_op + register_fake +
setup_context + register_autograd, and `flash_attn_func_compilable` —
from `attention/backends/flash_attn.py` to a new sibling at
`attention/utils/flash_attn_default.py`. The backend now imports
`fa_version` and `flash_attn_func_compilable` from there; the rest of the
backend (FlashAttentionBackend, FlashAttentionImpl, FP4 quantize helpers,
masked-branch routing) is unchanged.

Benefits:
  - Internal consistency with flash_attn_cute.py (the FP4 template the
    file-level comment claims to mirror) and flash_attn_no_pad.py (the
    masked/varlen wrappers, already in utils/).
  - A future backend that wants the same traceable FA2/FA3/FA4 op can
    import from utils/ without depending on the FA backend's dispatcher.
  - Cleaner separation between backend registry/dispatch logic and
    per-kernel custom-op plumbing.

No semantic change. Verified there is exactly one registration of
`fastvideo::_flash_attn_default_forward` after the move (grep clean).
Update `test_flash_attn_default_custom_op.py` to import from the utils
module (the test referenced `fa_backend._fa_default`, a name that lived
inside the moved block).
@Mister-Raggs
Mister-Raggs force-pushed the perf/flash-attn-fa2-real-backward branch from b1846a1 to 4cc6023 Compare July 2, 2026 18:57
@mergify

mergify Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase:

git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts label Jul 4, 2026
Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request Jul 5, 2026
…e indices in backward

Two improvements, both from the gemini-code-assist + Copilot reviews on hao-ai-lab#1388:

(a) Mark `softmax_lse` non-differentiable in all 3 custom-op setup_contexts
    (default + masked + varlen_qk). We return lse alongside out so it can be
    saved for backward; nobody should differentiate through it. `del grad_lse`
    in backward silently drops grads if a caller wires lse into a loss —
    `ctx.mark_non_differentiable(lse)` makes autograd error loudly instead.
    The public `*_compilable` dispatchers already drop lse (`out, _ = op(...)`),
    so this only matters for callers using `torch.ops.fastvideo._*_forward`
    directly, but it's free defensive hygiene.

(b) Stop re-running `unpad_input` for tensors that share a mask. The first
    `unpad_input(mask)` call returns `indices` + `cu_seqlens` + `max_s`;
    subsequent unpads for out/dout/lse on the same mask just re-derived
    the same indices (and the `.max().item()` call inside each unpad does
    a GPU→CPU sync). Replace those with `tensor.flatten(0,1)[indices].view(...)`,
    which is what `unpad_input` does internally anyway:

      - flash_attn_no_pad backward: 4 unpad_input calls -> 1
      - flash_attn_varlen_qk_no_pad backward: 6 unpad_input calls -> 2
        (one per distinct mask; v reuses k_indices, out/dout/lse reuse
        q_indices, the final repad of dk/dv reuses k_indices instead of
        recomputing).

Semantics are preserved — `unpad_input(x, mask)[0]` is equivalent to
`x.flatten(0,1)[indices]` for the same mask. The existing 28 unit tests
(inference parity atol=0 rtol=0, training-backward parity, opcheck with
and without grad inputs, all on FA2 + A100) cover the regressions.
Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request Jul 5, 2026
Addresses the S2 finding from the post-merge review on hao-ai-lab#1373: per FastVideo
precedent (fastvideo/attention/utils/flash_attn_cute.py — the FP4 cute
custom-op wrapper hao-ai-lab#1388 says it mirrors) and vLLM's similar pattern
(vllm/utils/flashinfer.py), per-kernel custom-op wrappers live in
attention/utils/, not inline in the backend dispatcher.

Move the entire `if fa_version == "2": ... elif "3": ... elif "4": ... else:`
block — including the FA version probe, custom_op + register_fake +
setup_context + register_autograd, and `flash_attn_func_compilable` —
from `attention/backends/flash_attn.py` to a new sibling at
`attention/utils/flash_attn_default.py`. The backend now imports
`fa_version` and `flash_attn_func_compilable` from there; the rest of the
backend (FlashAttentionBackend, FlashAttentionImpl, FP4 quantize helpers,
masked-branch routing) is unchanged.

Benefits:
  - Internal consistency with flash_attn_cute.py (the FP4 template the
    file-level comment claims to mirror) and flash_attn_no_pad.py (the
    masked/varlen wrappers, already in utils/).
  - A future backend that wants the same traceable FA2/FA3/FA4 op can
    import from utils/ without depending on the FA backend's dispatcher.
  - Cleaner separation between backend registry/dispatch logic and
    per-kernel custom-op plumbing.

No semantic change. Verified there is exactly one registration of
`fastvideo::_flash_attn_default_forward` after the move (grep clean).
Update `test_flash_attn_default_custom_op.py` to import from the utils
module (the test referenced `fa_backend._fa_default`, a name that lived
inside the moved block).
@Mister-Raggs
Mister-Raggs force-pushed the perf/flash-attn-fa2-real-backward branch from 4cc6023 to d938ed5 Compare July 5, 2026 21:13
@mergify mergify Bot removed the needs-rebase PR has merge conflicts label Jul 5, 2026
@Mister-Raggs

Copy link
Copy Markdown
Contributor Author

Rebased onto current main to clear the merge conflicts. Most of the 6 commits replayed as-is; the one thing that's genuinely new since your approval is the FA4 backend selection.

Main's #1540 made FA4 (flash_attn.cute) an explicit opt-in via FASTVIDEO_FA4=1 and deleted the auto-detect fallback. This branch had moved that resolver into attention/utils/flash_attn_default.py and flash_attn_no_pad.py, so I folded #1540's opt-in gating into both — they now match main's behavior (FA4 only when FASTVIDEO_FA4=1, else FA3→FA2) instead of auto-selecting FA4 when installed. Main's _forward_impl addition from #1447 is preserved.

@alexzms since that resolver is new vs the version you approved, worth a quick re-look at flash_attn_default.py before re-merging. yapf/ruff/codespell are clean locally on the changed files.

Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request Jul 10, 2026
…e indices in backward

Two improvements, both from the gemini-code-assist + Copilot reviews on hao-ai-lab#1388:

(a) Mark `softmax_lse` non-differentiable in all 3 custom-op setup_contexts
    (default + masked + varlen_qk). We return lse alongside out so it can be
    saved for backward; nobody should differentiate through it. `del grad_lse`
    in backward silently drops grads if a caller wires lse into a loss —
    `ctx.mark_non_differentiable(lse)` makes autograd error loudly instead.
    The public `*_compilable` dispatchers already drop lse (`out, _ = op(...)`),
    so this only matters for callers using `torch.ops.fastvideo._*_forward`
    directly, but it's free defensive hygiene.

(b) Stop re-running `unpad_input` for tensors that share a mask. The first
    `unpad_input(mask)` call returns `indices` + `cu_seqlens` + `max_s`;
    subsequent unpads for out/dout/lse on the same mask just re-derived
    the same indices (and the `.max().item()` call inside each unpad does
    a GPU→CPU sync). Replace those with `tensor.flatten(0,1)[indices].view(...)`,
    which is what `unpad_input` does internally anyway:

      - flash_attn_no_pad backward: 4 unpad_input calls -> 1
      - flash_attn_varlen_qk_no_pad backward: 6 unpad_input calls -> 2
        (one per distinct mask; v reuses k_indices, out/dout/lse reuse
        q_indices, the final repad of dk/dv reuses k_indices instead of
        recomputing).

Semantics are preserved — `unpad_input(x, mask)[0]` is equivalent to
`x.flatten(0,1)[indices]` for the same mask. The existing 28 unit tests
(inference parity atol=0 rtol=0, training-backward parity, opcheck with
and without grad inputs, all on FA2 + A100) cover the regressions.
Mister-Raggs added a commit to Mister-Raggs/FastVideo that referenced this pull request Jul 10, 2026
Addresses the S2 finding from the post-merge review on hao-ai-lab#1373: per FastVideo
precedent (fastvideo/attention/utils/flash_attn_cute.py — the FP4 cute
custom-op wrapper hao-ai-lab#1388 says it mirrors) and vLLM's similar pattern
(vllm/utils/flashinfer.py), per-kernel custom-op wrappers live in
attention/utils/, not inline in the backend dispatcher.

Move the entire `if fa_version == "2": ... elif "3": ... elif "4": ... else:`
block — including the FA version probe, custom_op + register_fake +
setup_context + register_autograd, and `flash_attn_func_compilable` —
from `attention/backends/flash_attn.py` to a new sibling at
`attention/utils/flash_attn_default.py`. The backend now imports
`fa_version` and `flash_attn_func_compilable` from there; the rest of the
backend (FlashAttentionBackend, FlashAttentionImpl, FP4 quantize helpers,
masked-branch routing) is unchanged.

Benefits:
  - Internal consistency with flash_attn_cute.py (the FP4 template the
    file-level comment claims to mirror) and flash_attn_no_pad.py (the
    masked/varlen wrappers, already in utils/).
  - A future backend that wants the same traceable FA2/FA3/FA4 op can
    import from utils/ without depending on the FA backend's dispatcher.
  - Cleaner separation between backend registry/dispatch logic and
    per-kernel custom-op plumbing.

No semantic change. Verified there is exactly one registration of
`fastvideo::_flash_attn_default_forward` after the move (grep clean).
Update `test_flash_attn_default_custom_op.py` to import from the utils
module (the test referenced `fa_backend._fa_default`, a name that lived
inside the moved block).
@Mister-Raggs
Mister-Raggs force-pushed the perf/flash-attn-fa2-real-backward branch from d938ed5 to 9435fc3 Compare July 10, 2026 22:07
Follow-up to hao-ai-lab#1373 + its CI carve-out (commit fae1cd1). The custom op
fastvideo::_flash_attn_default_forward shipped with a forward + fake
kernel but no register_autograd, so it was opaque to autograd; the
carve-out routed grad-enabled calls back to the original FA2
flash_attn_func (an autograd.Function) at the cost of a graph break on
the training path. This commit closes that gap for FA2 by mirroring the
FP4 cute template's 4-piece pattern:

  - forward returns (out, softmax_lse), obtained via flash_attn_func's
    return_attn_probs=True path so we don't touch private FA2 fwd APIs;
  - register_fake returns the matching tuple, with softmax_lse fixed at
    [batch, nheads, seqlen_q] fp32;
  - setup_context saves q,k,v,out,lse + softmax_scale, causal;
  - the backward calls flash_attn.flash_attn_interface._flash_attn_backward
    (FA2's private bwd) with all version-fragile kwargs pinned to the
    flash-attn==2.8.1 defaults (the version FastVideo pins).

With autograd registered, flash_attn_func_compilable can drop its carve-
out — grad-enabled calls go through the op like inference calls, so the
training path is also dynamo-traceable. Numerics unchanged (lse is saved-
for-backward only, never differentiated; backward writes the same dq/dk/
dv FA2's autograd.Function would produce).

FA3 keeps the carve-out from fae1cd1 untouched. FA3 exposes a different
private _flash_attn_backward signature that wants validation on a real
Hopper box; once Kuan-Hao's Modal FA3 setup PR lands we mirror the FA2
pattern there.

Tests: extend test_flash_attn_default_custom_op.py with
  - test_default_op_backward_through_registered_autograd: calls
    torch.ops.fastvideo._flash_attn_default_forward directly with
    requires_grad inputs, autograd.grad through the op, asserts grads
    match the original flash_attn_func to dtype-appropriate tolerance.
  - test_default_op_opcheck_with_grad_inputs: torch.library.opcheck
    with requires_grad inputs (exercises test_autograd_registration —
    the gap that let hao-ai-lab#1373's first revision ship without a backward).
Both gate on fa_version=="2"; FA3 retains the carve-out tests as-is.

Directional confirmation from Will Lin (FastVideo maintainer) 2026-05-22:
"it definitely is [wanted], but testing the correctness and performance
is more involved".
Builds on the FA2 default-path real-backward (commit 7dbc1e0). Wraps
the two remaining flash-attn entry points FlashAttentionImpl.forward
calls — flash_attn_no_pad (masked self-attn) and
flash_attn_varlen_qk_no_pad (cross-attn / unequal q-k seqlen) — as
torch.library.custom_ops with full register_autograd on FA2. Same
trade as the default path: dynamo sees one traceable node, the
internal unpad/pad bookkeeping runs eager inside, and training
backprops through the op (no graph break on either the inference or
training path).

The non-trivial part vs the default leg is `softmax_lse`. FA2 varlen
returns lse in the unpadded form ([nheads, total_q]); to keep the
custom op's outputs statically-shaped (so register_fake matches),
we pad lse out to [batch, nheads, seqlen] before returning and
re-unpad in backward using the saved mask. The backward then re-unpads
qkv/out/dout via unpad_input and calls FA2's
flash_attn.flash_attn_interface._flash_attn_varlen_backward on the
unpadded form, then re-pads d{q,k,v} back to the input shape.

softmax_scale=None is resolved to `head_dim**-0.5` in setup_context
(FA2's varlen backward demands a concrete float, same as the default
leg).

FA3 / FA4 keep the autograd carve-out pattern from hao-ai-lab#1373: forward+fake
only, dispatcher falls back to the original autograd.Function for
grad-enabled calls. Those legs ship as separate follow-ups gated on
Hopper / Blackwell box validation.

Wire FlashAttentionImpl.forward's masked branch to call the
*_compilable wrappers instead of the originals, so the graph-break
elimination + autograd parity actually takes effect at the call site.

Add fastvideo/tests/attention/test_flash_attn_no_pad_custom_op.py:
  - inference parity (atol=0, rtol=0) for both ops
  - backward-through-registered-autograd for both ops (FA2-only;
    dq/dk/dv match the original autograd.Function within dtype tol)
  - torch.library.opcheck with and without grad inputs (the
    with-grad case exercises test_autograd_registration —
    structurally identical to the gap that let hao-ai-lab#1373's first
    revision ship without a backward).

GPU-gated on CUDA + FA2 v2.8.1. Heavy SSIM gate (HunyuanVideo-1.5)
on a 40-80 GB box is the integration check for the masked path
end-to-end.
…e indices in backward

Two improvements, both from the gemini-code-assist + Copilot reviews on hao-ai-lab#1388:

(a) Mark `softmax_lse` non-differentiable in all 3 custom-op setup_contexts
    (default + masked + varlen_qk). We return lse alongside out so it can be
    saved for backward; nobody should differentiate through it. `del grad_lse`
    in backward silently drops grads if a caller wires lse into a loss —
    `ctx.mark_non_differentiable(lse)` makes autograd error loudly instead.
    The public `*_compilable` dispatchers already drop lse (`out, _ = op(...)`),
    so this only matters for callers using `torch.ops.fastvideo._*_forward`
    directly, but it's free defensive hygiene.

(b) Stop re-running `unpad_input` for tensors that share a mask. The first
    `unpad_input(mask)` call returns `indices` + `cu_seqlens` + `max_s`;
    subsequent unpads for out/dout/lse on the same mask just re-derived
    the same indices (and the `.max().item()` call inside each unpad does
    a GPU→CPU sync). Replace those with `tensor.flatten(0,1)[indices].view(...)`,
    which is what `unpad_input` does internally anyway:

      - flash_attn_no_pad backward: 4 unpad_input calls -> 1
      - flash_attn_varlen_qk_no_pad backward: 6 unpad_input calls -> 2
        (one per distinct mask; v reuses k_indices, out/dout/lse reuse
        q_indices, the final repad of dk/dv reuses k_indices instead of
        recomputing).

Semantics are preserved — `unpad_input(x, mask)[0]` is equivalent to
`x.flatten(0,1)[indices]` for the same mask. The existing 28 unit tests
(inference parity atol=0 rtol=0, training-backward parity, opcheck with
and without grad inputs, all on FA2 + A100) cover the regressions.
Addresses the S2 finding from the post-merge review on hao-ai-lab#1373: per FastVideo
precedent (fastvideo/attention/utils/flash_attn_cute.py — the FP4 cute
custom-op wrapper hao-ai-lab#1388 says it mirrors) and vLLM's similar pattern
(vllm/utils/flashinfer.py), per-kernel custom-op wrappers live in
attention/utils/, not inline in the backend dispatcher.

Move the entire `if fa_version == "2": ... elif "3": ... elif "4": ... else:`
block — including the FA version probe, custom_op + register_fake +
setup_context + register_autograd, and `flash_attn_func_compilable` —
from `attention/backends/flash_attn.py` to a new sibling at
`attention/utils/flash_attn_default.py`. The backend now imports
`fa_version` and `flash_attn_func_compilable` from there; the rest of the
backend (FlashAttentionBackend, FlashAttentionImpl, FP4 quantize helpers,
masked-branch routing) is unchanged.

Benefits:
  - Internal consistency with flash_attn_cute.py (the FP4 template the
    file-level comment claims to mirror) and flash_attn_no_pad.py (the
    masked/varlen wrappers, already in utils/).
  - A future backend that wants the same traceable FA2/FA3/FA4 op can
    import from utils/ without depending on the FA backend's dispatcher.
  - Cleaner separation between backend registry/dispatch logic and
    per-kernel custom-op plumbing.

No semantic change. Verified there is exactly one registration of
`fastvideo::_flash_attn_default_forward` after the move (grep clean).
Update `test_flash_attn_default_custom_op.py` to import from the utils
module (the test referenced `fa_backend._fa_default`, a name that lived
inside the moved block).
Rebase merge into flash_attn_no_pad.py left the file not-quite yapf-clean;
also clears two pre-existing lints that were blocking the pre-commit gate:

- yapf: reformat the merged top-of-file region (precedence-probe function
  signature: tuple[Any, str]) and downstream sections touched by the rebase.
  No semantic change.
- ruff F841: drop unused `sk = key.shape[1]` in
  QkVarlenNoPadForward.setup_context; only the backward needs it (computed
  fresh there at line ~436).
- codespell: add `dout` to ignore-words-list. Standard attention-backward
  gradient-of-output convention; the variable name recurs across the
  masked/varlen op backwards in this file.
…, type _repad

1. register_fake for _flash_attn_varlen_qk_no_pad_forward built `out` from
   query.shape, so its head_dim was d_q. The real forward's out takes head_dim
   from value (out_padded is [b, sq, h, d_v]). Harmless while d_q == d_v (all
   current models), but under torch.compile a d_v != d_q case would trace the
   wrong output shape — exactly what register_fake guards. Build out as
   query.new_empty(b, sq, h, value.shape[-1]) and stop del-ing value before
   reading its shape. Fixed in both the FA2 and FA3/else branch fakes.

2. test_varlen_qk_inference_matches_original and test_varlen_qk_forward_opcheck
   weren't gated on FA2, so on Hopper + a recent flash_attn_3 (probe resolves to
   FA3) they ran and hit the pre-existing FA3 varlen `dropout_p` incompatibility
   instead of skipping. Gate both on _fa2_only, matching the autograd tests. The
   underlying FA3 dropout_p fix is out of scope here.

Also annotate the two new `_repad` backward closures (-> Tensor) so mypy's
no-untyped-call is satisfied in this PR's own code.
@Mister-Raggs
Mister-Raggs force-pushed the perf/flash-attn-fa2-real-backward branch from 9435fc3 to 52eed2b Compare July 11, 2026 08:49
@mergify
mergify Bot merged commit 0555867 into hao-ai-lab:main Jul 12, 2026
29 checks passed
SolitaryThinker added a commit that referenced this pull request Jul 13, 2026
…resolver tests

#1388 changed _resolve_flash_attn_varlen_func() to return (func, fa_version)
so the module can gate the FA2-only backward registration, but the resolver
unit tests from #1540 still assert __name__/__module__ on the bare return
value. The two changes never textually conflicted, so the merge was clean and
every PR lane based on current main now fails microscope-unit-tests on these
two tests (first seen: pr-fastcheck build 671). Unpack the tuple and assert
the version leg alongside the function.
SolitaryThinker added a commit that referenced this pull request Jul 13, 2026
…resolver tests

#1388 changed _resolve_flash_attn_varlen_func() to return (func, fa_version)
so the module can gate the FA2-only backward registration, but the resolver
unit tests from #1540 still assert __name__/__module__ on the bare return
value. The two changes never textually conflicted, so the merge was clean and
every PR lane based on current main now fails microscope-unit-tests on these
two tests (first seen: pr-fastcheck build 671). Unpack the tuple and assert
the version leg alongside the function.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready PR is ready to merge scope: attention Attention backends (VSA, STA, Flash, etc.) scope: infra CI, tests, Docker, build type: perf Performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants