Skip to content

Commit c58b736

Browse files
committed
[perf]: address hao-ai-lab#1388 bot review — mark lse non-diff + reuse 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.
1 parent 76b6ff6 commit c58b736

2 files changed

Lines changed: 33 additions & 26 deletions

File tree

fastvideo/attention/backends/flash_attn.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ def _flash_attn_default_setup_context(ctx, inputs, output):
9393
q, k, v, softmax_scale, causal = inputs
9494
out, lse = output
9595
ctx.save_for_backward(q, k, v, out, lse)
96+
# `lse` is an auxiliary output we save to feed FA2's backward; nobody
97+
# should differentiate through it. Mark it non-differentiable so
98+
# autograd errors loudly if a caller wires it into a loss, rather
99+
# than silently producing zero/None grads through the `del grad_lse`
100+
# in our backward.
101+
ctx.mark_non_differentiable(lse)
96102
# FA2's *forward* substitutes `1 / sqrt(head_dim)` for `softmax_scale=None`
97103
# internally; FA2's *backward* (`_flash_attn_backward`) demands a concrete
98104
# float in its C++ schema and rejects None at the binding boundary. Resolve

fastvideo/attention/utils/flash_attn_no_pad.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ def _flash_attn_no_pad_setup_context(ctx, inputs, output):
263263
qkv, key_padding_mask, causal, dropout_p, softmax_scale, deterministic = inputs
264264
out, lse = output
265265
ctx.save_for_backward(qkv, out, lse, key_padding_mask)
266+
# Auxiliary output, not differentiable — see default-path note.
267+
ctx.mark_non_differentiable(lse)
266268
# FA2's varlen backward requires a concrete float for softmax_scale.
267269
if softmax_scale is None:
268270
softmax_scale = qkv.shape[-1] ** -0.5 # head_dim from qkv's last dim
@@ -277,23 +279,20 @@ def _flash_attn_no_pad_backward(ctx, grad_out, grad_lse):
277279
qkv, out_padded, lse_padded, key_padding_mask = ctx.saved_tensors
278280
b, s, _three, h, d = qkv.shape
279281

280-
# Re-unpad qkv -> q, k, v unpadded ([nnz, h, d] each).
282+
# One `unpad_input` call (on qkv) gives us indices + cu_seqlens + max_s;
283+
# reuse those for out / dout / lse below via direct indexing instead
284+
# of redundant `unpad_input` calls (each of which would re-run
285+
# `nonzero` + `cumsum` + a `.max().item()` GPU→CPU sync).
281286
x = rearrange(qkv, "b s three h d -> b s (three h d)")
282287
x_unpad, indices, cu_seqlens, max_s, _ = unpad_input(x, key_padding_mask)
283288
x_unpad = rearrange(x_unpad, "nnz (three h d) -> nnz three h d", three=3, h=h)
284289
q_unpad, k_unpad, v_unpad = (t.contiguous() for t in x_unpad.unbind(dim=1))
285290

286-
# Re-unpad out and dout using the same mask.
287-
out_unpad = rearrange(
288-
unpad_input(rearrange(out_padded, "b s h d -> b s (h d)"), key_padding_mask)[0],
289-
"nnz (h d) -> nnz h d", h=h).contiguous()
290-
dout_unpad = rearrange(
291-
unpad_input(rearrange(grad_out, "b s h d -> b s (h d)"), key_padding_mask)[0],
292-
"nnz (h d) -> nnz h d", h=h).contiguous()
293-
294-
# Re-unpad lse: [b, h, s] -> [b, s, h] -> [nnz, h] -> [h, nnz].
295-
lse_unpad = unpad_input(lse_padded.permute(0, 2, 1).contiguous(),
296-
key_padding_mask)[0].t().contiguous()
291+
# Direct-index variants reuse `indices` (computed above).
292+
out_unpad = out_padded.flatten(0, 1)[indices].view(-1, h, d).contiguous()
293+
dout_unpad = grad_out.flatten(0, 1)[indices].view(-1, h, d).contiguous()
294+
# lse_padded [b, h, s] -> [b, s, h] -> [nnz, h] -> [h, nnz].
295+
lse_unpad = lse_padded.permute(0, 2, 1).contiguous().flatten(0, 1)[indices].t().contiguous()
297296

298297
dq_unpad = torch.empty_like(q_unpad)
299298
dk_unpad = torch.empty_like(k_unpad)
@@ -395,6 +394,8 @@ def _flash_attn_varlen_qk_no_pad_setup_context(ctx, inputs, output):
395394
out, lse = output
396395
ctx.save_for_backward(query, key, value, out, lse,
397396
query_padding_mask, key_padding_mask)
397+
# Auxiliary output, not differentiable — see default-path note.
398+
ctx.mark_non_differentiable(lse)
398399
if softmax_scale is None:
399400
softmax_scale = query.shape[-1] ** -0.5
400401
ctx.softmax_scale = softmax_scale
@@ -409,26 +410,24 @@ def _flash_attn_varlen_qk_no_pad_backward(ctx, grad_out, grad_lse):
409410
b, sq, h, d = query.shape
410411
sk = key.shape[1]
411412

412-
# Re-unpad q with q_mask; k, v with k_mask.
413+
# One `unpad_input` call per distinct mask; reuse the returned
414+
# indices via direct indexing for everything else that shares
415+
# the same mask (v with k_mask; out/dout/lse with q_mask; the
416+
# final repad of dk/dv also reuses k_indices). Avoids ~4
417+
# redundant `unpad_input` calls + their GPU→CPU `.max().item()`
418+
# syncs.
413419
q_unpad, q_indices, cu_seqlens_q, max_seqlen_q, _ = unpad_input(
414420
rearrange(query, "b s h d -> b s (h d)"), query_padding_mask)
415421
k_unpad, k_indices, cu_seqlens_k, max_seqlen_k, _ = unpad_input(
416422
rearrange(key, "b s h d -> b s (h d)"), key_padding_mask)
417-
v_unpad, _, _, _, _ = unpad_input(
418-
rearrange(value, "b s h d -> b s (h d)"), key_padding_mask)
419423
q_unpad = rearrange(q_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
420424
k_unpad = rearrange(k_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
421-
v_unpad = rearrange(v_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
422-
423-
# out and lse follow q's shape, so re-unpad with q_mask.
424-
out_unpad = rearrange(
425-
unpad_input(rearrange(out_padded, "b s h d -> b s (h d)"), query_padding_mask)[0],
426-
"nnz (h d) -> nnz h d", h=h).contiguous()
427-
dout_unpad = rearrange(
428-
unpad_input(rearrange(grad_out, "b s h d -> b s (h d)"), query_padding_mask)[0],
429-
"nnz (h d) -> nnz h d", h=h).contiguous()
430-
lse_unpad = unpad_input(lse_padded.permute(0, 2, 1).contiguous(),
431-
query_padding_mask)[0].t().contiguous()
425+
v_unpad = value.flatten(0, 1)[k_indices].view(-1, h, d).contiguous()
426+
427+
# out / dout / lse follow q's shape, so index with q_indices.
428+
out_unpad = out_padded.flatten(0, 1)[q_indices].view(-1, h, d).contiguous()
429+
dout_unpad = grad_out.flatten(0, 1)[q_indices].view(-1, h, d).contiguous()
430+
lse_unpad = lse_padded.permute(0, 2, 1).contiguous().flatten(0, 1)[q_indices].t().contiguous()
432431

433432
dq_unpad = torch.empty_like(q_unpad)
434433
dk_unpad = torch.empty_like(k_unpad)
@@ -447,6 +446,8 @@ def _flash_attn_varlen_qk_no_pad_backward(ctx, grad_out, grad_lse):
447446
rng_state=None,
448447
)
449448

449+
# k_indices is already available from the unpad_input above —
450+
# no need to recompute it for the dk/dv repad.
450451
def _repad(dt_unpad, indices, batch, seqlen):
451452
padded = pad_input(rearrange(dt_unpad, "nnz h d -> nnz (h d)"), indices, batch, seqlen)
452453
return rearrange(padded, "b s (h d) -> b s h d", h=h)

0 commit comments

Comments
 (0)