[perf]: register a real backward for FA2 default + masked/varlen custom ops (training-under-compile) - #1388
Conversation
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 PR merge requirements
|
There was a problem hiding this comment.
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.
| 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() |
There was a problem hiding this comment.
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()| 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) |
There was a problem hiding this comment.
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]| 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() |
There was a problem hiding this comment.
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()There was a problem hiding this comment.
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_funcbehind atorch.library.custom_op(FA2: real backward; FA3: carve-out for grads). - Add masked/varlen custom ops (
flash_attn_no_pad*) with FA2register_autogradand 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: |
| def _flash_attn_varlen_qk_no_pad_backward(ctx, grad_out, grad_lse): | ||
| del grad_lse |
| # 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) |
| 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) |
…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.
|
Two threads from the automated review worth addressing in narrative, since per-line replies don't capture the full reasoning. (The other threads — Version-dependent op schemaThe same custom-op name registers different schemas across FA versions — FA2 returns Three options weighed:
Picked (3). If the FA3 leg lands a real backward later (gated on a Hopper box for grad-check), it'll converge on Inference-parity test asserts
|
|
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 |
c58b736 to
3626db1
Compare
…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).
|
Force-pushed S2 (file placement): Moved the entire Rebase conflict: tiny — took main's version (long carve-out comment + yapf-clean single-line Commits on top of clean main:
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; |
|
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). |
|
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 |
…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).
3626db1 to
90e35c9
Compare
alexzms
left a comment
There was a problem hiding this comment.
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 shape2. 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.
|
Thanks for the careful pass! Both addressed in b1846a1:
Also typed the two new |
alexzms
left a comment
There was a problem hiding this comment.
LGTM. Thanks for iterative contribution!
|
/merge |
alexzms
left a comment
There was a problem hiding this comment.
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.
…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).
b1846a1 to
4cc6023
Compare
|
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 |
…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).
4cc6023 to
d938ed5
Compare
|
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 ( @alexzms since that resolver is new vs the version you approved, worth a quick re-look at |
…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).
d938ed5 to
9435fc3
Compare
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.
9435fc3 to
52eed2b
Compare
…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.
…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.
Summary
Adds
register_autogradparity 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
maincurrently includes #1373's commits; will rebase ontomainonce #1373 merges.Scope (FA2 only — FA3 is a separate follow-up)
flash_attn_funcregister_autograd; training also traceableflash_attn_no_padregister_autogradflash_attn_varlen_qk_no_pad(cross-attn)register_autogradThe carve-out (
flash_attn_func_compilablecheckingis_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_lsein unpadded form[nheads, total_q]. To keep the custom op's outputs statically-shaped (soregister_fakematches dynamo's expectation), we padlseto[batch, nheads, seqlen]on the way out and re-unpad it in backward using the saved mask. Backward then re-unpadsqkv/out/dout, allocatesdq/dk/dvon the unpadded form, callsflash_attn.flash_attn_interface._flash_attn_varlen_backward, and re-pads gradients back.softmax_scale=Noneis resolved tohead_dim ** -0.5insetup_context(FA2's backward demands a concrete float).Validation matrix
atol=0 rtol=0, grad through op matches original autograd.Function, opcheck × {with grad, without grad}MIN=1.00000049/49 framestext_encoding.py:221crashes 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/FA4Inference 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):3a67319c)2f3ca8aa)3626db1b)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.000000in 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_autogradfor the default path is invisible to inference (autograd doesn't run underinference_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
register_autogradis 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 +opcheckwith grad inputs, which is the correctness floor.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*_compilabledispatchers.fastvideo/attention/utils/flash_attn_no_pad.py— wrapsflash_attn_no_padandflash_attn_varlen_qk_no_padas 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.