Skip to content

Commit 329504f

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 b5a5398 commit 329504f

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
@@ -123,6 +123,12 @@ def _flash_attn_default_setup_context(ctx, inputs, output):
123123
q, k, v, softmax_scale, causal = inputs
124124
out, lse = output
125125
ctx.save_for_backward(q, k, v, out, lse)
126+
# `lse` is an auxiliary output we save to feed FA2's backward; nobody
127+
# should differentiate through it. Mark it non-differentiable so
128+
# autograd errors loudly if a caller wires it into a loss, rather
129+
# than silently producing zero/None grads through the `del grad_lse`
130+
# in our backward.
131+
ctx.mark_non_differentiable(lse)
126132
# FA2's *forward* substitutes `1 / sqrt(head_dim)` for `softmax_scale=None`
127133
# internally; FA2's *backward* (`_flash_attn_backward`) demands a concrete
128134
# 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
@@ -281,6 +281,8 @@ def _flash_attn_no_pad_setup_context(ctx, inputs, output):
281281
qkv, key_padding_mask, causal, dropout_p, softmax_scale, deterministic = inputs
282282
out, lse = output
283283
ctx.save_for_backward(qkv, out, lse, key_padding_mask)
284+
# Auxiliary output, not differentiable — see default-path note.
285+
ctx.mark_non_differentiable(lse)
284286
# FA2's varlen backward requires a concrete float for softmax_scale.
285287
if softmax_scale is None:
286288
softmax_scale = qkv.shape[-1] ** -0.5 # head_dim from qkv's last dim
@@ -295,23 +297,20 @@ def _flash_attn_no_pad_backward(ctx, grad_out, grad_lse):
295297
qkv, out_padded, lse_padded, key_padding_mask = ctx.saved_tensors
296298
b, s, _three, h, d = qkv.shape
297299

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

304-
# Re-unpad out and dout using the same mask.
305-
out_unpad = rearrange(
306-
unpad_input(rearrange(out_padded, "b s h d -> b s (h d)"), key_padding_mask)[0],
307-
"nnz (h d) -> nnz h d", h=h).contiguous()
308-
dout_unpad = rearrange(
309-
unpad_input(rearrange(grad_out, "b s h d -> b s (h d)"), key_padding_mask)[0],
310-
"nnz (h d) -> nnz h d", h=h).contiguous()
311-
312-
# Re-unpad lse: [b, h, s] -> [b, s, h] -> [nnz, h] -> [h, nnz].
313-
lse_unpad = unpad_input(lse_padded.permute(0, 2, 1).contiguous(),
314-
key_padding_mask)[0].t().contiguous()
309+
# Direct-index variants reuse `indices` (computed above).
310+
out_unpad = out_padded.flatten(0, 1)[indices].view(-1, h, d).contiguous()
311+
dout_unpad = grad_out.flatten(0, 1)[indices].view(-1, h, d).contiguous()
312+
# lse_padded [b, h, s] -> [b, s, h] -> [nnz, h] -> [h, nnz].
313+
lse_unpad = lse_padded.permute(0, 2, 1).contiguous().flatten(0, 1)[indices].t().contiguous()
315314

316315
dq_unpad = torch.empty_like(q_unpad)
317316
dk_unpad = torch.empty_like(k_unpad)
@@ -413,6 +412,8 @@ def _flash_attn_varlen_qk_no_pad_setup_context(ctx, inputs, output):
413412
out, lse = output
414413
ctx.save_for_backward(query, key, value, out, lse,
415414
query_padding_mask, key_padding_mask)
415+
# Auxiliary output, not differentiable — see default-path note.
416+
ctx.mark_non_differentiable(lse)
416417
if softmax_scale is None:
417418
softmax_scale = query.shape[-1] ** -0.5
418419
ctx.softmax_scale = softmax_scale
@@ -427,26 +428,24 @@ def _flash_attn_varlen_qk_no_pad_backward(ctx, grad_out, grad_lse):
427428
b, sq, h, d = query.shape
428429
sk = key.shape[1]
429430

430-
# Re-unpad q with q_mask; k, v with k_mask.
431+
# One `unpad_input` call per distinct mask; reuse the returned
432+
# indices via direct indexing for everything else that shares
433+
# the same mask (v with k_mask; out/dout/lse with q_mask; the
434+
# final repad of dk/dv also reuses k_indices). Avoids ~4
435+
# redundant `unpad_input` calls + their GPU→CPU `.max().item()`
436+
# syncs.
431437
q_unpad, q_indices, cu_seqlens_q, max_seqlen_q, _ = unpad_input(
432438
rearrange(query, "b s h d -> b s (h d)"), query_padding_mask)
433439
k_unpad, k_indices, cu_seqlens_k, max_seqlen_k, _ = unpad_input(
434440
rearrange(key, "b s h d -> b s (h d)"), key_padding_mask)
435-
v_unpad, _, _, _, _ = unpad_input(
436-
rearrange(value, "b s h d -> b s (h d)"), key_padding_mask)
437441
q_unpad = rearrange(q_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
438442
k_unpad = rearrange(k_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
439-
v_unpad = rearrange(v_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
440-
441-
# out and lse follow q's shape, so re-unpad with q_mask.
442-
out_unpad = rearrange(
443-
unpad_input(rearrange(out_padded, "b s h d -> b s (h d)"), query_padding_mask)[0],
444-
"nnz (h d) -> nnz h d", h=h).contiguous()
445-
dout_unpad = rearrange(
446-
unpad_input(rearrange(grad_out, "b s h d -> b s (h d)"), query_padding_mask)[0],
447-
"nnz (h d) -> nnz h d", h=h).contiguous()
448-
lse_unpad = unpad_input(lse_padded.permute(0, 2, 1).contiguous(),
449-
query_padding_mask)[0].t().contiguous()
443+
v_unpad = value.flatten(0, 1)[k_indices].view(-1, h, d).contiguous()
444+
445+
# out / dout / lse follow q's shape, so index with q_indices.
446+
out_unpad = out_padded.flatten(0, 1)[q_indices].view(-1, h, d).contiguous()
447+
dout_unpad = grad_out.flatten(0, 1)[q_indices].view(-1, h, d).contiguous()
448+
lse_unpad = lse_padded.permute(0, 2, 1).contiguous().flatten(0, 1)[q_indices].t().contiguous()
450449

451450
dq_unpad = torch.empty_like(q_unpad)
452451
dk_unpad = torch.empty_like(k_unpad)
@@ -465,6 +464,8 @@ def _flash_attn_varlen_qk_no_pad_backward(ctx, grad_out, grad_lse):
465464
rng_state=None,
466465
)
467466

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

0 commit comments

Comments
 (0)