[perf] avoid full-sequence materialization in VSA coarse/sparse combine - #1813
[perf] avoid full-sequence materialization in VSA coarse/sparse combine#1813boxwrench wants to merge 4 commits into
Conversation
The compression branch computed a per-block coarse output and then expanded it across the full sequence with repeat() before combining it with the sparse output. The gated combine then allocated two more [B, H, S, D] tensors for the product and the sum. Keep the coarse result at block resolution, [B, H, n_blocks, 1, D], and let it broadcast over the intra-block axis during the combine. This mirrors the BSHD 128/256 path, which already broadcasts out_c_blk.unsqueeze(2) rather than repeating. Under no_grad the combine accumulates into the sparse output with addcmul_, removing the remaining two full-sequence temporaries; with grad enabled it stays out-of-place, for the same reason the BSHD path documents (the sparse output is saved by FA4's autograd node for backward). Ungated the result is bit-exact. Gated, addcmul_ fuses the multiply-add instead of rounding the intermediate product to bf16, so the rounding differs from the old path; in the added tests the fused result is the more accurate of the two when both are compared against fp32. Top-k routing, shapes and dtypes are unchanged, and 64/128/256 dispatch is unaffected. Isolated combine microbenchmark at an H3-like shape (bf16, 56 heads, dim 128, 15488 tokens, block 64), gated: 4.06 -> 2.12 ms and 848 -> 212 MiB peak allocated. This measures the combine alone, not an end-to-end workload. Adds tests/test_vsa_combine.py (17 cases, CI-sized, no model or pipeline dependency) and benchmarks/bench_vsa_combine.py.
Merge Protections🔴 1 of 1 protections blocking · waiting on 👀 reviews and 🤖 CI
🔴 PR merge requirementsWaiting for
This rule is failing.
|
There was a problem hiding this comment.
Welcome to FastVideo! Thanks for your first pull request.
How our CI works:
PRs run a three-tier CI system:
- Pre-commit — formatting (yapf), linting (ruff), type checking (mypy). Runs immediately on every PR.
- Fastcheck — six core GPU lanes run automatically via Buildkite (~10-15 min).
- Merge gate — a reviewer adds
ready; changed paths select only the relevant integration, training, golden, or SSIM coverage.
Before your PR is reviewed:
-
pre-commit run --all-filespasses locally - You've added or updated tests for your changes
- The PR description explains what and why
If pre-commit fails, a bot comment will explain how to fix it. Fastcheck and merge-gate results appear in the Checks section below.
Useful links:
Follow-up to the block-resolution combine in video_sparse_attn: - one helper for both layouts (seq_dim=2 for the BHSD entry, seq_dim=1 for video_sparse_attn_bshd), so the 128/256 BSHD path also stops allocating combine temporaries at inference - the grad branch uses out-of-place torch.addcmul: one temporary instead of two, and bit-identical to the in-place addcmul_ path, so results no longer depend on the caller's grad mode - the in-place path is keyed on out_s.requires_grad (the actual aliasing invariant) rather than the global grad mode, and skipped when dtype promotion would otherwise downcast into out_s - strict shape validation of out_s and the gate (same-numel wrong-layout inputs used to be accepted silently); the dead .contiguous() fallback is gone because splitting the sequence axis is always expressible as a view - traceable by torch.compile(fullgraph=True) Tests (GB200): 77 cases on the default Triton route and 44 on the FA4 CuTe route. Ungated results are bit-exact with the old path; every gated element is within half a bf16 ulp of the fp32 truth in all four grad/no-grad modes; gradients match the old path; exact peak-allocation pins (0 bytes in place, one full tensor out of place); end-to-end value, routing and gradient checks for 64/128/256 tiles in both layouts; a fullgraph compile check. The benchmark now uses triton.testing.do_bench, measures peak memory outside the timed window, adds end-to-end video_sparse_attn / video_sparse_attn_bshd cases, and drops the MiniMax H3 label (that backend does not call this combine). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NjFimeedTQWxgqSzP5xo4j
The 64-tile BHSD path copied gate_compress into a contiguous BHSD tensor on every call. The coarse/sparse combine views the gate at block resolution, which never needs contiguity, so the transposed BSHD view is passed as is. This saves one full-sequence copy per attention layer with bit-identical output. On a GB200 at 1x39936x12x128 (Wan 1.3B, 480p) the per-call peak drops from 639.8 to 521.8 MiB and latency from 11.64 to 11.29 ms. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NjFimeedTQWxgqSzP5xo4j
…inference torch.addcmul keeps one full-sequence temporary under grad instead of two, and when the sparse output is not tracked by autograd the gate branch accumulates into it directly, allocating nothing. The product is no longer rounded to bf16 before the add, so gated values move by at most half a bf16 ulp (towards the fp32 value). test_vsa_h3_backward.py passes on the Triton and FA4 CuTe backends on a GB200. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NjFimeedTQWxgqSzP5xo4j
|
Pushed three follow-up commits after review (all verified on a GB200):
The benchmark now uses
End-to-end |
Problem
The VSA compression branch computes a per-block coarse output, then expands it across the full sequence with
repeat()before combining it with the sparse output:The coarse value is constant within a block, so the expansion is avoidable. Gated, the combine then allocates two further
[B, H, S, D]tensors for the product and the sum — three full-size temporaries where none is needed.Change
Keep the coarse result at block resolution,
[B, H, n_blocks, 1, D], and let it broadcast over the intra-block axis during the combine.This mirrors the BSHD 128/256 combine, which already broadcasts
out_c_blk.unsqueeze(2)instead of repeating. Underno_gradthe combine accumulates into the sparse output withaddcmul_, removing the remaining two full-sequence temporaries. With grad enabled it stays out-of-place, for the same reason the BSHD path documents in its comment — the sparse output is saved by FA4's autograd node for backward, so mutating it there would invalidate the graph.The combine is shared by the 64/128/256 dispatch (the
repeat()happened before the block-size branch), so all three paths benefit.Correctness
New
tests/test_vsa_combine.py, 17 cases, all CI-sized synthetic tensors with no model, checkpoint or pipeline dependency:video_sparse_attn.addcmul_fuses the multiply-add instead of rounding the intermediate product to bf16, so the rounding differs from the old path and the two disagree on a fraction of elements. In these tests the fused result is the more accurate of the two when both are compared against an fp32 reference (mean error 1.52e-3 → 1.29e-3, max 2.29e-2 → 1.56e-2). The test asserts accuracy against fp32 rather than agreement with the old rounding.block_sparse_attn, and the 128/256 kernels are still dispatched.Performance
benchmarks/bench_vsa_combine.py. These numbers are an isolated microbenchmark of the combine alone — not an end-to-end workload. At an H3-like shape (bf16, 56 heads, head dim 128, 15488 tokens, block size 64):The 636 MiB saved in the gated case is exactly the three full
[B, H, S, D]bf16 tensors that are no longer materialized (3 × 211.8 MiB). The residual 212 MiB is the benchmark's own input clone.For a full-workload data point, MiniMax H3 at 864×480/124f with topk 0.20 measured end-to-end:
That end-to-end measurement is from an AMD Radeon AI PRO R9700 (gfx1201, ROCm 7.2.1, PyTorch 2.9.1, Triton 3.5.1) and is hardware-specific — it should not be extrapolated to other GPUs. The change itself is not platform-specific; the mechanism is allocation count and memory traffic.
Notes
video_sparse_attn_bshdstill uses.float().sum(dim=2)for its block means, which materializes an fp32 copy of q/k/v. That is a separate concern on a 128/256-only path and is left alone here to keep this diff minimal.test_vmoba_correctness, 2test_attn_qat_train, and one intermittenttest_vsa_varlen.py::TestVSAVarlenBackward::test_backward_different_lengthsthat also fails on unmodifiedmain). Those are unrelated to this change and appear to be ROCm-environment failures.