Skip to content

Commit d0dcd31

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 d5b7860 commit d0dcd31

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
@@ -270,6 +270,8 @@ def _flash_attn_no_pad_setup_context(ctx, inputs, output):
270270
qkv, key_padding_mask, causal, dropout_p, softmax_scale, deterministic = inputs
271271
out, lse = output
272272
ctx.save_for_backward(qkv, out, lse, key_padding_mask)
273+
# Auxiliary output, not differentiable — see default-path note.
274+
ctx.mark_non_differentiable(lse)
273275
# FA2's varlen backward requires a concrete float for softmax_scale.
274276
if softmax_scale is None:
275277
softmax_scale = qkv.shape[-1] ** -0.5 # head_dim from qkv's last dim
@@ -284,23 +286,20 @@ def _flash_attn_no_pad_backward(ctx, grad_out, grad_lse):
284286
qkv, out_padded, lse_padded, key_padding_mask = ctx.saved_tensors
285287
b, s, _three, h, d = qkv.shape
286288

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

293-
# Re-unpad out and dout using the same mask.
294-
out_unpad = rearrange(
295-
unpad_input(rearrange(out_padded, "b s h d -> b s (h d)"), key_padding_mask)[0],
296-
"nnz (h d) -> nnz h d", h=h).contiguous()
297-
dout_unpad = rearrange(
298-
unpad_input(rearrange(grad_out, "b s h d -> b s (h d)"), key_padding_mask)[0],
299-
"nnz (h d) -> nnz h d", h=h).contiguous()
300-
301-
# Re-unpad lse: [b, h, s] -> [b, s, h] -> [nnz, h] -> [h, nnz].
302-
lse_unpad = unpad_input(lse_padded.permute(0, 2, 1).contiguous(),
303-
key_padding_mask)[0].t().contiguous()
298+
# Direct-index variants reuse `indices` (computed above).
299+
out_unpad = out_padded.flatten(0, 1)[indices].view(-1, h, d).contiguous()
300+
dout_unpad = grad_out.flatten(0, 1)[indices].view(-1, h, d).contiguous()
301+
# lse_padded [b, h, s] -> [b, s, h] -> [nnz, h] -> [h, nnz].
302+
lse_unpad = lse_padded.permute(0, 2, 1).contiguous().flatten(0, 1)[indices].t().contiguous()
304303

305304
dq_unpad = torch.empty_like(q_unpad)
306305
dk_unpad = torch.empty_like(k_unpad)
@@ -402,6 +401,8 @@ def _flash_attn_varlen_qk_no_pad_setup_context(ctx, inputs, output):
402401
out, lse = output
403402
ctx.save_for_backward(query, key, value, out, lse,
404403
query_padding_mask, key_padding_mask)
404+
# Auxiliary output, not differentiable — see default-path note.
405+
ctx.mark_non_differentiable(lse)
405406
if softmax_scale is None:
406407
softmax_scale = query.shape[-1] ** -0.5
407408
ctx.softmax_scale = softmax_scale
@@ -416,26 +417,24 @@ def _flash_attn_varlen_qk_no_pad_backward(ctx, grad_out, grad_lse):
416417
b, sq, h, d = query.shape
417418
sk = key.shape[1]
418419

419-
# Re-unpad q with q_mask; k, v with k_mask.
420+
# One `unpad_input` call per distinct mask; reuse the returned
421+
# indices via direct indexing for everything else that shares
422+
# the same mask (v with k_mask; out/dout/lse with q_mask; the
423+
# final repad of dk/dv also reuses k_indices). Avoids ~4
424+
# redundant `unpad_input` calls + their GPU→CPU `.max().item()`
425+
# syncs.
420426
q_unpad, q_indices, cu_seqlens_q, max_seqlen_q, _ = unpad_input(
421427
rearrange(query, "b s h d -> b s (h d)"), query_padding_mask)
422428
k_unpad, k_indices, cu_seqlens_k, max_seqlen_k, _ = unpad_input(
423429
rearrange(key, "b s h d -> b s (h d)"), key_padding_mask)
424-
v_unpad, _, _, _, _ = unpad_input(
425-
rearrange(value, "b s h d -> b s (h d)"), key_padding_mask)
426430
q_unpad = rearrange(q_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
427431
k_unpad = rearrange(k_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
428-
v_unpad = rearrange(v_unpad, "nnz (h d) -> nnz h d", h=h).contiguous()
429-
430-
# out and lse follow q's shape, so re-unpad with q_mask.
431-
out_unpad = rearrange(
432-
unpad_input(rearrange(out_padded, "b s h d -> b s (h d)"), query_padding_mask)[0],
433-
"nnz (h d) -> nnz h d", h=h).contiguous()
434-
dout_unpad = rearrange(
435-
unpad_input(rearrange(grad_out, "b s h d -> b s (h d)"), query_padding_mask)[0],
436-
"nnz (h d) -> nnz h d", h=h).contiguous()
437-
lse_unpad = unpad_input(lse_padded.permute(0, 2, 1).contiguous(),
438-
query_padding_mask)[0].t().contiguous()
432+
v_unpad = value.flatten(0, 1)[k_indices].view(-1, h, d).contiguous()
433+
434+
# out / dout / lse follow q's shape, so index with q_indices.
435+
out_unpad = out_padded.flatten(0, 1)[q_indices].view(-1, h, d).contiguous()
436+
dout_unpad = grad_out.flatten(0, 1)[q_indices].view(-1, h, d).contiguous()
437+
lse_unpad = lse_padded.permute(0, 2, 1).contiguous().flatten(0, 1)[q_indices].t().contiguous()
439438

440439
dq_unpad = torch.empty_like(q_unpad)
441440
dk_unpad = torch.empty_like(k_unpad)
@@ -454,6 +453,8 @@ def _flash_attn_varlen_qk_no_pad_backward(ctx, grad_out, grad_lse):
454453
rng_state=None,
455454
)
456455

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

0 commit comments

Comments
 (0)