Skip to content

FlashVSR: From 17.2 FPS to 57.3 FPS at 768x1408#108

Open
hamuzhan wants to merge 35 commits into
OpenImagingLab:mainfrom
hamuzhan:hopper-acceleration
Open

FlashVSR: From 17.2 FPS to 57.3 FPS at 768x1408#108
hamuzhan wants to merge 35 commits into
OpenImagingLab:mainfrom
hamuzhan:hopper-acceleration

Conversation

@hamuzhan

@hamuzhan hamuzhan commented Jun 17, 2026

Copy link
Copy Markdown

Alright, I know this repo looks abandoned. I got obsessed with this model anyway, and honestly, finding performance wins has been a ridiculous dopamine loop. It is a little addictive. So I spent the last few days back in the code with one goal: squeeze Hopper until it has nothing left to give. Same model. Same quality target. Much higher speed. This time FlashVSR should finally earn its name. Here are the updates.

Hopper Tiny Inference Acceleration

Before, Then, Now

Point in time GH200 throughput at 768x1408 What changed
Before 17.2 FPS Stock FlashVSR path with cuDNN Conv3D and the bundled HMMA sparse-attention kernel
Then 38.585 FPS First complete Hopper stack with tensor-core LQ Conv3D, channels-last decoder, WGMMA sparse attention, cache, and layout work
Now 57.256 FPS Current recommended stack with streamed decode, decoder kernel work, DiT row fusion, threshold reuse, split-K MemBlocks, and strict route validation

That is 17.2 -> 38.585 -> 57.256 FPS on the same GH200 and 768x1408 target. The current result is a 3.33x increase over the original stock number and a further +48.4% over the first complete Hopper stack.

The 17.2 FPS number is the original GH200 denoise benchmark recorded when this work started. The 38.585 and 57.256 values use the current F=81 output-FPS harness. They describe the same model, GPU, and resolution, but the current harness is more complete and reproducible. The strict same-harness comparison for the latest work is 55.910 -> 57.256 FPS.

What Was Actually Slow

A big Hopper GPU does not automatically make FlashVSR fast. I profiled the real streaming workload instead of trusting isolated FLOP counts and found several very specific problems.

Problem What I observed Why it mattered
LQ Conv3D cuDNN selected a path around 28 TFLOP/s for the projector shapes The projector spent a large part of denoise time while barely using Hopper tensor cores
Sparse attention The bundled path used Ampere-style HMMA behavior on Hopper Attention remained the largest single compute block
Attention glue Layout conversions, KV concatenation, pooling, mask selection, and RoPE moved data without advancing model work Small costs repeated across 30 DiT blocks and every chunk
Decoder Decoding was serialized, paid layout churn, copied recurrent state, and repeatedly materialized concat buffers Decoder work blocked end-to-end throughput after denoise improved
Measurement Fast paths could silently fall back and overlap could distort naive per-chunk timing Optimizing an inactive route would create false wins

I added NVTX coverage, strict route counters, Nsight Systems and Nsight Compute harnesses, input provenance, per-chunk timing, tail latency, memory reporting, and fallback reporting before treating any optimization as real.

How I Fixed It

Put the LQ projector on Hopper tensor cores

The LQ projector Conv3D shapes were a poor cuDNN match. I reformulated the core operation as im2col plus BF16 GEMM, added H-axis tiling to bound transient memory, used persistent causal-pad buffers, and added an exact Triton im2col packer. This was the first major unlock.

Rebuild sparse attention for Hopper

The model depends on locality-constrained sparse attention, so dense attention was not an acceptable default replacement. I added Hopper WGMMA sparse attention, then a warp-specialized producer and consumer path with TMA loads, fused CSR construction, FFMA-form softmax math, strided IO, incremental pooled-K caching, a coalesced RoPE kernel, and zero-copy raster V handling.

Each attention route keeps the sparse-mask contract or is explicitly quality-gated. FP8 attention was investigated and rejected because it fell far below the 49 dB quality bar.

Stop paying decoder overhead every frame

The decoder now runs channels-last, overlaps chunk decode with denoise on a side stream, rotates recurrent state pointers instead of copying tensors, writes decoded chunks directly to final output, uses Triton pointwise and layout kernels, moves TGrow work to low resolution, and uses cuDNN fused Conv, Bias, Add, and ReLU paths when quality permits it.

The split-K MemBlock path removes the recurrent cat materialization. It splits the first convolution into current and past weight halves, then recombines both contributions through cuDNN.

Remove the remaining control-plane tax

The final GH200 set adds a Triton row-wise DiT LayerNorm to AdaLN fusion, a gated-residual kernel, and a steady mask-threshold cache. These are smaller wins than the original attention and decoder work, but they are measured, routed, and quality-gated like the rest.

Measured Results

768x1408, F=81, 77 output frames

xychart-beta
    title "GH200 throughput at 768x1408, F=81"
    x-axis ["Stock GH200", "First Hopper stack", "Current Hopper stack", "Goal"]
    y-axis "FPS" 0 --> 60
    bar [17.2, 38.585, 57.256, 60]
Loading
Configuration FPS Delta Evidence
Stock GH200 path 17.200 baseline Original GH200 denoise benchmark
First complete Hopper stack 38.585 +124.3% Three fresh-process runs, median
Previous recommended preset 55.910 +225.1% Three fresh-process runs, median
Updated recommended preset 57.256 +232.9% from stock, +48.4% from first Hopper stack, +2.41% from previous 57.256 / 57.213 / 57.358 FPS
Final strict smoke run 57.435 n/a All requested fast paths executed, no fallback

1536x2560

xychart-beta
    title "GH200 throughput at 1536x2560"
    x-axis ["Initial stack", "Previous recommended", "Updated recommended"]
    y-axis "FPS" 0 --> 16
    bar [11.01, 15.07, 15.415]
Loading
Workload FPS Peak memory Quality
1536x2560, F=41 15.415 46.46 GiB Strict route pass
1536x2560, F=29 n/a n/a 49.95 dB versus preceding preset

Quality And Correctness

Area Result
Decoder cumulative stack Bit-exact at F=29 and F=89
DiT row fusion, threshold cache, split-K combined 49.81 dB at F=29, 49.59 dB at F=89
1536x2560 quality spot 49.95 dB at F=29
Split-K isolated decoder tests 68.86 to 68.99 dB across production shapes
Strict route enforcement Expected routes executed with no fallback errors

The new GH200 preset is intentionally explicit about this. The flags are opt-in, unsupported Hopper paths retain their existing fallback behavior, and the quality-gated preset is not described as lossless.

Recommended GH200 Controls

Control What it does Constraint
FLASHVSR_CONV3D_BACKEND=gemm Uses the im2col plus tensor-core projector path Hopper only
FLASHVSR_ATTN_BACKEND=triton2 Uses warp-specialized block-sparse attention Hopper only, quality-gated against the reference attention implementation
FLASHVSR_DECODER_OVERLAP=1 Decodes chunks on a side stream while denoise continues Bit-exact
FLASHVSR_TCDECODER_CUDNN_FUSED=1 Fuses decoder Conv, Bias, Add, and ReLU operations Quality-gated
FLASHVSR_DIT_ROW_FUSION=1 Fuses affine-free LayerNorm, AdaLN, and gated residual work Quality-gated
FLASHVSR_MASKGEN_THRESHOLD_CACHE=1 Reuses the previous steady-chunk threshold per DiT block Quality-gated
FLASHVSR_TCDECODER_SPLITK_CONV=1 Removes recurrent decoder concat through split-weight convolution Requires TCDECODER_CUDNN_FUSED=1, quality-gated

The full preset lives in examples/WanVSR/run_flashvsr_v1.1_tiny_gh200.sh.

Things I Tried And Rejected

Experiment Result Decision
1980 MHz graphics-clock lock 56.028 FPS versus 56.085 FPS fresh baseline Rejected
Direct implicit Conv3D contraction Improved isolated conv2, but dropped end-to-end throughput to 56.741 FPS Rejected
Larger KV arena spare Removed compactions, but did not repeat its probe win and added 8.7 GiB Rejected
FP8 GEMM Faster in isolation, but below the locked 49 dB quality gate Experimental only
FP8 attention 34.96 dB optimistic probe Rejected before kernel work
Aggressive attention overlap variants Limited by softmax and WGMMA dependency behavior Not promoted

Reproducibility And Profiling

  • NVTX ranges cover chunk scheduling, DiT blocks, sparse attention, mask generation, LQ projection, decoder work, and color correction.
  • run_pipe_target.py records input provenance, per-chunk timing, tail latency, peak memory, active backend routes, and fallback errors.
  • FLASHVSR_REQUIRE_FASTPATHS=1 makes a benchmark fail if a requested route silently falls back.
  • bench_phase7.sh runs the final GH200 stack and automatically selects a valid steady window for short clips.
  • PHASE_BENCH_LOG.md contains the complete measurement ledger, quality probes, rejected experiments, and resolution spot checks.

Verification

venv/bin/python examples/WanVSR/test_phase5_lossless.py phase6

FLASHVSR_TEST_MASKGEN_THRESHOLD_CACHE=1 \
FLASHVSR_TEST_SPLITK_CONV=1 \
venv/bin/python examples/WanVSR/test_dit_row_fusion.py 89

FLASHVSR_REQUIRE_FASTPATHS=1 \
examples/WanVSR/profiling/bench_phase7.sh hopper-pr 3

FLASHVSR_PROF_W=1536 FLASHVSR_PROF_H=2560 \
FLASHVSR_PROF_FRAMES=41 FLASHVSR_BENCH_STEADY=2:3 \
FLASHVSR_REQUIRE_FASTPATHS=1 \
examples/WanVSR/profiling/bench_phase7.sh hopper-pr-1536 1

Commit Organization

Commit subjects are grouped by responsibility using perf, feat, docs, and test prefixes with scopes such as attention, decoder, rope, kv, lq, mask, and profiling.

hamuzhan added 8 commits June 17, 2026 11:15
The LQ-projector CausalConv3d kernels (4x3x3, stride (2,1,1), large channels)
run at ~27 TFLOP/s (~2.8% peak) on GH200 because cuDNN can't pick a tensor-core
implicit-GEMM for these shapes. They account for ~50% of denoise time.

Reformulate the core convolution as explicit im2col (unfold) + bf16 GEMM, which
saturates Hopper WGMMA tensor cores (~400+ TFLOP/s, ~15-17x faster) with
bit-identical math (parity cosine 0.999996).

- Phase 1 (D1): _conv3d_gemm + FLASHVSR_CONV3D_BACKEND={auto|gemm} knob +
  sm_90 guard + silent fallback. Causal replicate-pad + streaming cache
  preserved; only the padding-free core conv is rerouted. Single-point change
  benefits conv1/conv2 in both Buffer_/Causal_ projectors.
- Phase 2 (D1.5): chunked im2col over the output-H axis bounded by
  FLASHVSR_CONV3D_IM2COL_BUDGET_GB (default 2 GB) -> conv transient mem -51%
  at 1920x2560 with no speed/parity loss.

E2E (v1.1 Tiny): 1.92x @1536, 1.91x @2560x1920; norm-FPS ~17 -> ~33-34;
PSNR(auto,gemm) 47.6-49.5 dB. Default 'auto' = no regression.

Also fix transformers PretrainedConfig import (renamed -> PreTrainedConfig)
so the pipeline import chain works on current transformers.
Compares GH200 against the README's A100 reference (~17 FPS @ 768x1408) at the
exact same resolution, with both conv3d backends.

Results @ 768x1408:
- GH200 auto (cuDNN):       16.54 FPS  (0.97x A100 - parity, no Hopper gain)
- GH200 gemm (tensor-core): 31.56 FPS  (1.86x A100)
- gemm vs auto:             1.91x

Confirms the thesis: before the im2col+GEMM backend, GH200 was stuck at A100
parity despite 3x the hardware; the tensor-core conv path unlocks the real
Hopper advantage.
After the conv3d im2col+GEMM work (1.86x A100), profiled the full denoise
pipeline @768x1408 to find the next bottlenecks. GPU is ~91% busy (compute-
bound). Breakdown: attention ~27%, GEMM ~21%, TCDecoder conv2d ~19%,
norm/elementwise ~17%, copy/layout ~15%.

3-A) TCDecoder channels_last (NHWC):
  The TCDecoder is a pure Conv2d (TAEHV) graph running contiguous (NCHW), which
  made cuDNN insert nchwToNhwc/nhwcToNchw around every bf16 conv (~226ms /9%)
  and run the convs ~1.5x slower. Run the decoder in channels_last:
  - isolated decode: 231 -> 189 ms (1.22x), bit-identical (max|diff|=0)
  - E2E: 31.6 -> 32.9 FPS, 1.86 -> 1.93x A100, -0.5 GB peak
  Knob FLASHVSR_TCDECODER_CHANNELS_LAST (default on).

3-B) Adaptive attention backend (FLASHVSR_ATTN_BACKEND, default 'sparse'):
  Measured the real self-attn: seq=25344, 12 heads, dim=128, block-mask
  density ~0.606 (only 39% sparse). At that density cuDNN fused dense SDPA
  (6.5ms, 605 TFLOP/s) beats block_sparse (7.3ms) AND uses full context;
  FA2 dense is 10.6ms (cuDNN wins by 1.64x). Crossover is density ~0.5.
  Added density-adaptive routing (opt-in). E2E gain at default topk is
  negligible (~+0.5%) and dense changes the output (full vs trained sparse
  pattern, PSNR 41 dB), so default stays 'sparse' (no quality change).

Tooling added (isolated + E2E):
  profile_e2e_bottlenecks.py, probe_attention_shapes.py,
  test_tcdecoder_channels_last.py, test_attention_backend.py, test_topk_sweep.py

Note (not committed as default): topk/sparsity sweep shows sparse_ratio=1.5
gives 2.07x A100 @ PSNR 42.8 dB vs baseline (a documented 'faster' setting).
The DiT block spends ~17% of denoise GPU time in memory-bound elementwise
kernels: RMSNorm (q/k, with an fp32 up/down cast), modulate (x*(1+scale)+shift)
and the gate (x + gate*residual). Fuse each via torch.compile(dynamic=True).

Isolated (dim=1536, seq=25344):
  RMSNorm        0.556 -> 0.263 ms (2.12x, cos 1.000000)
  modulate+gate  0.438 -> 0.313 ms (1.40x, cos 0.999993)

E2E @768x1408 (conv3d=gemm, TCDecoder NHWC):
  32.9 -> 35.5 FPS, 1.94 -> 2.09x A100, PSNR(off,on) 49.2 dB (bf16-level).

These fused fns contain no attention / custom kernels, so they don't interact
with the block_sparse path or streaming cache. Knob FLASHVSR_FUSE_NORM
(default off, opt-in due to compile warmup + tiny bf16 reorder diff).

Adds test_fuse_norm.py (E2E parity + speed).
The bundled block_sparse_attn CUDA kernel is FlashAttention-2 style and emits
only Ampere HMMA tensor-core ops even when compiled for sm_90 (verified in SASS:
380928 HMMA, 0 WGMMA), reaching only ~33% of bf16 peak (~327 TFLOP/s) at the
real self-attn shape (seq=25344, 12 heads, dim=128, block-mask density ~0.606).
cuDNN dense reaches ~62% peak via WGMMA but can't express FlashVSR's 2D-spatial
block mask (cuDNN block_mask unsupported on sm_90; 1D band masks don't cover a
2D-local pattern efficiently).

Add a Triton block-sparse FlashAttention kernel that honors the exact
per-(q_block, kv_block) boolean mask and compiles to Hopper WGMMA (verified in
PTX/SASS). It uses a CSR-style per-q-block kept-kv-index list so masked tiles
are skipped entirely.

Isolated (vs the real block_sparse_attn kernel, same mask):
  cos 0.99999, max|diff| 0.0005; 7.36 -> 6.06 ms (1.21x), ~41% peak.

E2E @768x1408 (conv3d=gemm, TCDecoder NHWC, fuse_norm on):
  35.5 -> 38.0 FPS, 2.09 -> 2.23x A100; PSNR(sparse,triton) 49.97 dB (bf16-level).

Opt-in via FLASHVSR_ATTN_BACKEND=triton, guarded to sm_90, silent fallback to
the original block_sparse kernel on non-Hopper / triton-missing / any error.
Default remains 'sparse' (zero regression; Ampere keeps the original path).
Add a Tensor Memory Accelerator (TMA) fast path to the Triton block-sparse
attention kernel. Q/K/V tiles are loaded via device TMA descriptors
(triton TensorDescriptor), overlapping bulk global->shared loads with WGMMA.

Isolated (real shape 25344x12x128, density 0.606): 6.11 -> 5.6 ms, ~40% -> ~44%
of bf16 peak, parity cos 0.99999 (math unchanged; TMA only changes how memory
is fetched).

E2E @768x1408 (conv3d=gemm, TCDecoder NHWC, fuse_norm on):
  2.23 -> 2.29x A100; PSNR(sparse, triton+TMA) 49.97 dB.

TMA is the default when available (Triton TensorDescriptor present); guarded by
FLASHVSR_ATTN_TMA (set 0 to disable) and falls back silently to the non-TMA
kernel on older Triton / SMEM limits / any error. Still gated to sm_90 via the
attention backend; Ampere keeps the original block_sparse path.
Two opt-in, bit-identical caches for per-block elementwise results that are
recomputed every denoise step but depend only on fixed inputs (not q/k/x):

- FLASHVSR_CACHE_MOD (default 0): cache (modulation + t_mod).chunk(...) per
  DiTBlock and Head. t_mod is computed once in init_cross_kv and is constant.
- FLASHVSR_CACHE_MASK_BIAS (default 0): cache the geometry-only 0/-inf additive
  bias in generate_draft_block_mask (repeat + masked_fill), keyed on shape only.

Both default OFF, pure elementwise, silent fallback; verified max|diff|==0 on
all test clips (test_cache_lossless.py). These remove ~2169 small kernels per
denoise.

Also: profile_e2e_bottlenecks.py categorize() now classifies the Triton
_bsfa kernel as attention and xmma_fprop as TCDecoder conv.
Add a "Hopper Acceleration" section to the README so users can discover and
enable the opt-in fast paths: the env var table (all default OFF), the
recommended full-speed one-liner, and notes on sm_90 guarding and parity
(bit-identical vs ~49-50 dB PSNR paths).
@napervays

Copy link
Copy Markdown

Dude you’ve really done an amazing job

@FurkanGozukara

Copy link
Copy Markdown

amazing would any of these work on other gpus too? like rtx 3000 4000 series? 5000 series?

hamuzhan added 20 commits July 8, 2026 06:59
- FLASHVSR_NVTX-gated nvtx ranges (default OFF, zero-effect) across the DiT
  block, pipeline chunk loop and LQ projector for nsys/ncu attribution
- FLASHVSR_PROFILER_START/STOP_CHUNK cudaProfiler window in flashvsr_tiny
- pipeline now honours progress_bar_cmd (needed for per-chunk timing)
- profiling/ harness: run_pipe_target.py, nsys/ncu drivers (with the triton
  ldconfig deadlock workaround), gap analyzer, ncu extractor, ceiling benches
- ANALYSIS.md (Phase-1 findings), PHASE_ROADMAP.md, PHASE_BENCH_LOG.md
- gitignore model weights / results / large profiling artifacts
Lossless (max|diff|==0) on-device assembly of the per-chunk RoPE freqs
tensor: one-time H2D of the per-axis tables, persistent per-(f,h,w,device)
buffer, h/w columns written once, f columns rewritten only when the chunk
temporal offset changes. Removes per-chunk CPU-side complex128 assembly and
the ~8.6MB/chunk H2D. FPS-neutral @768 untraced (CPU cost rode under GPU
work); kept behind flag (default OFF) — prerequisite for graph capture and
useful under CPU load. Adds the generic Phase-2A parity harness.
Same fp64 complex multiply expressed in real arithmetic and compiled with
torch.compile: reads bf16 x + strided fp64 freqs views, writes bf16, no
104MB fp64/complex128 intermediates in DRAM. Lazy compile, eager fallback.
Kernel 0.181->0.128 ms/call; E2E 38.585->39.023 FPS (+1.14%), steady chunk
156.28->153.62 ms, output bit-identical (max|diff|==0). Keep-enabled.
Preallocated (kv_len+1+SPARE)-slot arena per block/tensor: new KV windows
are partition-written directly into the tail (replacing the partition's own
contiguous() materialization), the live window is a contiguous slice view,
trim advances a pointer, and the only remaining copy is an overlap-safe
compaction amortized once per SPARE chunks. Values/order bit-identical to
the cat path (max|diff|==0 over 9 chunks incl. rotation+compaction).
E2E 39.023->39.429 FPS, steady 153.62->150.76 ms; peak mem 12.6->15.5 GiB
(2 spare slots, tunable via FLASHVSR_KV_RINGBUF_SPARE). Keep-enabled.
Strided-IO variant of the WGMMA block-sparse kernel: TMA descriptors cover
the glue's natural 2D (S, n*d) view and load per-(head, block) tiles at
[row, head*HEAD_DIM]; output is stored with explicit strides straight into
(S, n, d). Kills the three (S,n,d)->(n,S,d) .contiguous() copies plus the
output transpose (~11.6 ms/chunk attribution). Non-TMA fallback uses the
existing stride-general kernel with transposed views; failures fall back to
the contiguous triton path, then sparse. Kernel and E2E outputs bit-identical
(max|diff|==0). E2E 39.429->41.099 FPS (+4.24%), steady 150.76->141.14 ms.
Keep-enabled.
Exact-semantics lean pass on the draft-mask chain: (a) threshold via a
single kthvalue radix-select (same order statistic as topk(k+1)[:,-1],
verified equal incl. heavy ties; 0.187->0.076 ms/call, no values/indices
materialization), (b) drop the no-op repeat copy of the boolean mask for
B==1, (c) persistent cu_seqlens/head_mask_type for the sparse backend
(removes a per-call H2D sync on that path). Mask equality + E2E
max|diff|==0. E2E 41.099->41.477 FPS, steady 141.14->139.00 ms.
Keep-enabled.
Copies-only cleanup around the (mandatory) im2col+GEMM conv path: the
causal pad + streaming-cache cat is assembled with slice writes into a
persistent per-conv buffer (one materialization instead of two, replicate
corner semantics preserved), and the stream cache slices keep views instead
of clones (sources never mutated in place). A third sub-item — skipping the
GEMM output .contiguous() — was measured at 49.9 dB (F.normalize reduction
accumulation order changes with layout) and rejected against this item's
lossless gate; documented in code. E2E max|diff|==0 across a full clip.
41.477->41.580 FPS, steady 139.00->138.46 ms. Keep-enabled.
- 2A-6 go/no-go: corrected steady idle 0.6-2.5% with the full stack (below
  the >=2% gate) and the steady body is not capture-stable (arena slice
  offsets, per-chunk LQ slices) -> postponed with rationale in the log
- @1536x2560 closure spot-check: 11.01 -> 11.489 FPS, 531.5 -> 501.9 ms
- test_phase2a_lossless.py ALL: combined 2A stack bit-identical vs all-OFF
- README: document the six new knobs + updated recommended config/numbers
- PHASE_BENCH_LOG: 2A-6 entry, closure summary, closure spot-check table
Phase 2B-1: TCDecoder decode now runs per-chunk on a dedicated side CUDA
stream (FLASHVSR_DECODER_OVERLAP, default OFF), overlapping with the
denoise loop's later chunks instead of running as a fully serialized tail
after it. Event-gated handoff (ready/done events per chunk), preallocated
chunk-index-addressed result slots (exact output ordering preserved),
explicit tensor-lifetime comments. No decoder math/weights/precision
touched; serialized path is untouched and remains the default.

Correctness: bit-identical (max|diff|==0) vs the serialized path at
768x1408 and 1536x2560, for both a 1-chunk and 8-chunk clip, stable across
3 repeated ON runs (no race condition). test_phase2a_lossless.py ALL still
passes (flag-OFF path unaffected).

Benchmark (768x1408, F=81, full Phase 2A stack, 3-run medians): 41.708 ->
42.375 FPS (+1.60%), decode tail 559.4 -> 125.7 ms (-77.5%), peak memory
15.62 -> 15.16 GiB (decrease). @1536x2560: +1.31% FPS, tail -76.9%, peak
memory decrease. Nsight confirms genuine concurrent GPU execution on two
streams with no new hidden synchronization, but only ~30-36% of decode's
own GPU time is truly hidden in steady state (rest is CPU-dispatch-bound
relocation) - gain is real but more modest than the roadmap ceiling.

Decision: keep-behind-flag. See PHASE_BENCH_LOG.md Phase 2B-1 section for
full methodology, Nsight evidence, and written interpretation.

Files: diffsynth/pipelines/flashvsr_tiny.py (side-stream decode path),
examples/WanVSR/profiling/run_pipe_target.py ([tail] timing metric),
examples/WanVSR/profiling/test_decoder_overlap_lossless.py (new),
examples/WanVSR/profiling/PHASE_BENCH_LOG.md (Phase 2B-1 log entry).
Phase 2B-1: with FLASHVSR_DECODER_OVERLAP=1 (default OFF), each chunk's
latents are decoded by TCDecoder on a persistent side CUDA stream as soon
as they are finalized, event-gated (per-chunk ready/done events), with the
single main-stream wait_event right before output assembly. Decoded chunks
land in chunk-id-indexed slots, so output ordering is structural. Decoder
math/state semantics untouched (per-chunk cond slicing identical to
flashvsr_tiny_long.py); serialized path preserved verbatim behind the flag.

Correctness (test_decoder_overlap_lossless.py): max|diff|==0 vs serialized
on full clip (9 chunks) and short clip (1 chunk, trim edge), 3x repeated ON
runs bit-identical (no races); test_phase2a_lossless.py ALL still PASS.

Benchmark @768x1408 F=81, full 2A stack, 3-run medians, same commit:
41.662 -> 42.373 FPS (+1.71%), decode tail 561.2 -> 125.1 ms, steady chunk
138.30 -> 190.58 ms (absorbs decode), peak mem 15.62 -> 15.16 GiB (lower).
@1536x2560: 11.485 -> 11.712 FPS (+2.0%), tail 2046 -> 470 ms, peak
48.39 -> 46.72 GiB. Nsight (reports/phase2b_decoder_overlap): decode on
side stream across the whole loop, 34% of decode busy time co-executes
with denoise, GPU idle 1.9%, decode_wait 0.1 ms - gain is bounded by SM
time-sharing (attn kernel owns full SMs), not by hidden syncs.

Decision: keep-behind-flag; re-evaluate promotion after the Phase-3
attention kernel frees SM/smem headroom. Supersedes the concurrent
duplicate variant committed as 7cecb65 (same design; this tree is the one
the logged numbers and parity runs were produced against).

Files: diffsynth/pipelines/flashvsr_tiny.py (flag + overlap path),
examples/WanVSR/profiling/run_pipe_target.py ([tail] metric),
examples/WanVSR/profiling/test_decoder_overlap_lossless.py (harness),
examples/WanVSR/profiling/PHASE_BENCH_LOG.md (2B-1 entry + tables).
Phase 2B-2: e4m3 torch._scaled_mm path for the DiT hot GEMMs (self-attn
q/k/v with one shared input quantization, o, ffn1/ffn2, LQ-projector
30x linears with one shared quantization). Weights pre-cast once with
per-out-channel scales (+1.03 GiB); activations quantized per call with
per-row scales by a single Triton kernel (eager quantize chains measured
~5x off bandwidth and erased the GEMM win); ffn2's input quantization is
fused with GELU-tanh (fp32 gelu -> e4m3, replacing the eager bf16 GELU
pass). Bias fused in the scaled_mm epilogue; sticky eager fallback on any
error; scope bisection via FLASHVSR_FP8_GEMM_SCOPE.

Benchmark @768x1408 F=81, full 2A stack, 3-run medians: 41.704 -> 42.986
FPS (+3.07%), steady chunk 137.98 -> 132.96 ms (-5.0 ms). Kernel evidence
(torch.profiler + ncu): nvjet_sm90_qqtst_*_ovscale e4m3 kernels dispatch
in the live steady chunk.

Quality (Phase-4 protocol measurement): full scope PSNR 40.70 dB;
per-site qkv 45.58 / o 45.05 / ffn 42.37 / ffn1 44.15 / lq 49.08 dB —
distilled one-step model is fp8-sensitive, no configuration currently
clears the 45 dB enable floor. Ships permanently default-OFF pending the
Phase-4 audit (blockwise scales, smoothquant rebalancing, block
exclusion). Flag-OFF paths bit-identical (test_phase2a_lossless.py ALL
and test_decoder_overlap_lossless.py both PASS).

Decision: keep-behind-flag. See PHASE_BENCH_LOG.md Phase 2B-2 entry.
Phase 2B-3: a Triton radix-select+compare kernel replacing the
kthvalue/broadcast/compare tail of generate_draft_block_mask proved
bit-exact (mask + E2E max|diff|==0 across adversarial tie/shape/k cases)
but performance-negative: 69.2 us vs 59.7 us eager at the real shape
(rows=12 -> one-CTA-per-row select is latency-bound by 4 dependent
histogram rounds; torch kthvalue is already a single efficient
radix-select), E2E -0.4..-0.9%. Below the pre-registered +0.5% keep gate:
code reverted, no flag left behind. The exact-mask gate makes everything
upstream of the select (pool/einsum/softmax) bitwise-untouchable, so
remaining mask_gen headroom moves to Phase-3 (fold into attention kernel)
or Phase-4 (E2E-neutral mask changes). See PHASE_BENCH_LOG.md 2B-3 entry.
@hamuzhan hamuzhan force-pushed the hopper-acceleration branch from 58653a5 to 1f37a68 Compare July 9, 2026 23:56
@hamuzhan hamuzhan changed the title Hopper (GH200 / sm_90) inference acceleration: opt-in, lossless by default Hopper: add Tiny decoder and DiT fast paths Jul 10, 2026
@hamuzhan hamuzhan changed the title Hopper: add Tiny decoder and DiT fast paths FlashVSR Goes Brrr: Squeezing Every Last Drop Out of Hopper Jul 10, 2026
@hamuzhan hamuzhan changed the title FlashVSR Goes Brrr: Squeezing Every Last Drop Out of Hopper GH200 FlashVSR: 57.3 FPS at 768x1408 with Quality-Gated Hopper Kernels Jul 10, 2026
@hamuzhan hamuzhan changed the title GH200 FlashVSR: 57.3 FPS at 768x1408 with Quality-Gated Hopper Kernels GH200 FlashVSR: From 17.2 FPS to 57.3 FPS at 768x1408 Jul 10, 2026
@hamuzhan hamuzhan changed the title GH200 FlashVSR: From 17.2 FPS to 57.3 FPS at 768x1408 FlashVSR: From 17.2 FPS to 57.3 FPS at 768x1408 Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants