Skip to content

Commit db8c0cd

Browse files
committed
Add Sparse VideoGen attention for Wan on TPUs
Implement opt-in SVG attention for Wan inference with per-head spatial and temporal routing, head-local token layouts, and TPU sparse-attention kernels. Round sparse support to hardware tiles while preserving the approximate pair budget, retain valid support for every query, and use a padding-only cleanup path for sequence tails. Add configuration and scheduling controls, distributed inference integration, cache and unsupported-mode guards, AOT cache identity handling, documentation, and CPU/TPU regression coverage.
1 parent 1bc5481 commit db8c0cd

28 files changed

Lines changed: 4147 additions & 20 deletions
328 KB
Loading

‎docs/images/svg/dense-vs-svg.png‎

719 KB
Loading

‎docs/images/svg/head-patterns.png‎

394 KB
Loading

‎docs/svg.md‎

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Sparse VideoGen attention on TPUs
2+
3+
Video diffusion models generate a video through a sequence of denoising steps. At each step, attention lets each video token gather information from other tokens across space and time. This becomes expensive as the resolution and number of frames grow: dense attention considers every query–key pair, even though many interactions contribute very little to the output.
4+
5+
Sparse spatiotemporal attention takes advantage of this structure. Instead of attending everywhere, a query attends to a smaller set of positions chosen to capture the spatial and temporal information it needs.
6+
7+
## How SVG chooses where to attend
8+
9+
[Sparse VideoGen (SVG)](https://arxiv.org/abs/2502.01776) observes that attention heads often favor different patterns. Spatial heads concentrate attention within a frame or nearby frames. Temporal heads concentrate attention around corresponding spatial positions across frames. These patterns let us approximate dense attention while computing fewer interactions.
10+
11+
![Attention from the same query in a spatial head and a temporal head, shown across six latent frames.](images/svg/head-patterns.png)
12+
13+
*Observed attention weights for the same query in two heads. The spatial head concentrates 94.8% of its attention on the query frame, while the temporal head places substantial attention near corresponding positions in other frames. Cyan squares mark the query's spatial position; `m` is the attention mass in each displayed frame. Colors use a shared logarithmic scale. These are examples of observed attention, rather than the masks themselves.*
14+
15+
SVG makes the choice separately for each head:
16+
17+
1. **Profile a few queries.** Compute dense attention outputs for a small sample of query tokens, using all keys.
18+
2. **Compare two patterns.** Compute the sampled outputs under spatial and temporal masks, then measure each one's error relative to dense attention.
19+
3. **Use the better approximation.** Select the lower-error pattern and apply sparse attention to all queries in that head.
20+
21+
The choice is recomputed at each active layer and denoising step. A head does not need to keep the same assignment throughout generation. Sparsity is configurable, so the same method can trade a smaller approximation error for a larger reduction in computation.
22+
23+
This implementation reimplements routing, token placement, and kernel execution for Wan in MaxDiffusion. The [original SVG implementation](https://github.com/svg-project/Sparse-VideoGen) provides the reference method.
24+
25+
## Making sparse attention efficient on TPU
26+
27+
Skipping query–key interactions only helps if the hardware can skip the corresponding work efficiently. Our implementation arranges tokens so that both spatial and temporal patterns can use the same local-band attention kernel. Spatial heads keep frame-major order; temporal heads group corresponding spatial positions across frames. Outputs are restored to their original order afterward.
28+
29+
TPUs compute attention in tiles. A tile can lie entirely inside the sparse pattern, entirely outside it, or cross its boundary.
30+
31+
<img src="images/svg/attention-tiles.png" alt="A local attention band over a query–key tile grid, highlighting full, boundary, and skipped tiles." width="520">
32+
33+
*Sparse pattern before tile rounding. Query and key indices refer to the selected token layout. Blue indicates retained interactions and gray indicates skipped interactions. The prefix anchor is omitted for clarity.*
34+
35+
We round boundary tiles to either keep or skip them, approximately preserving the attention-pair budget of the original pattern. This slightly changes which interactions are retained, but lets all selected interior tiles run through one kernel without per-token sparse masking. Only tiles touching sequence padding need an additional validity mask; their outputs are combined with the main result using a numerically stable merge.
36+
37+
Token placement and restoration run after the Ulysses exchange, on each device's local heads. This limits the layout work to the heads that device will actually process.
38+
39+
The masks also include an optional prefix anchor. The `svg_include_first_frame` option retains the first `H × W` keys in the selected layout. For temporal heads, that prefix spans spatial positions across frames, so it does not correspond to the original first video frame.
40+
41+
## Configuration and usage
42+
43+
SVG is disabled by default. To enable it, set `use_svg_attention=True` and choose the densities and the steps and layers where sparsity should be active. Calls outside that interval continue to use dense attention.
44+
45+
For example, these overrides select the moderate Wan2.2 policy:
46+
47+
```yaml
48+
use_svg_attention: True
49+
svg_high_noise_density: 0.50
50+
svg_low_noise_density: 0.20
51+
svg_active_start_step: 11
52+
svg_active_end_step: 40
53+
svg_active_start_layer: 1
54+
svg_active_end_layer: 40
55+
svg_profile_query_count: 64
56+
svg_sample_max_row: 10000
57+
svg_profile_seed: 0
58+
svg_include_first_frame: True
59+
```
60+
61+
Step and layer intervals are zero-based and half-open: `[11, 40)` includes steps 11 through 39. Steps refer to denoising iterations, not noise-timestep values.
62+
63+
| Option | What it controls |
64+
|---|---|
65+
| `svg_spatial_density` | Density for single-expert Wan models; applies to either selected head pattern. |
66+
| `svg_high_noise_density`, `svg_low_noise_density` | Separate densities for Wan2.2's two experts. |
67+
| `svg_active_start_step`, `svg_active_end_step` | Denoising steps where SVG is enabled. |
68+
| `svg_active_start_layer`, `svg_active_end_layer` | Transformer layers where SVG is enabled. |
69+
| `svg_profile_query_count` | Number of queries sampled when choosing each head's pattern. |
70+
| `svg_sample_max_row` | Limits query sampling to this prefix of the sequence. |
71+
| `svg_profile_seed` | Seed for reproducible query sampling. |
72+
| `svg_include_first_frame` | Enables the prefix anchor in the selected layout. |
73+
| `svg_flash_block_sizes` | Sparse kernel tiling; an empty mapping uses `flash_block_sizes`. |
74+
75+
Density controls the local-band width. The anchor and tile rounding affect the actual number of retained interactions, so density is not itself the fraction of total transformer FLOPs retained.
76+
77+
The following is an example attention configuration for eight devices. Add it and the policy above to a Wan2.2 configuration used by `src/maxdiffusion/generate_wan.py`:
78+
79+
```yaml
80+
attention: ulysses_ring_custom_fixed_m
81+
ici_data_parallelism: 2
82+
ici_context_parallelism: 4
83+
ulysses_shards: 2
84+
flash_block_sizes:
85+
block_q: 6400
86+
block_kv: 2048
87+
block_kv_compute: 2048
88+
block_kv_compute_in: 1024
89+
heads_per_tile: 1
90+
vmem_limit_bytes: 67108864
91+
svg_flash_block_sizes:
92+
block_q: 3328
93+
block_kv: 2816
94+
block_kv_compute: 256
95+
block_kv_compute_in: 256
96+
heads_per_tile: 1
97+
vmem_limit_bytes: 67108864
98+
```
99+
100+
Dense calls retain their configured ring split. SVG exchanges over the whole context axis, giving Ring2/Ulysses2 for dense calls and Ring1/Ulysses4 for sparse calls in this example. Tile sizes may need tuning for other shapes and devices.
101+
102+
### Supported configurations
103+
104+
SVG supports inference through the four custom Ulysses/ring attention backends. It requires matching self-attention QKV shapes, a matching video-token grid, and `heads_per_tile=1`. Heads and sequence length must divide evenly across the context shards, including any heads created by folding an unsharded batch.
105+
106+
Training, Animate, external attention masks, periodic support, and chunked Ulysses are unsupported. SVG also cannot be combined with CFG cache or MagCache.
107+
108+
## Performance and quality
109+
110+
At 720p, SVG offers a configurable tradeoff between denoising latency and similarity to dense generation. The following Wan2.2 results use TPU v6e-8, 81 frames, and 40 denoising steps. Times are medians of three warm runs against same-node optimized fixed-M dense controls, using one prompt and seed.
111+
112+
| Policy | Estimated total transformer FLOPs saved | Dense denoising | SVG denoising | Denoising speedup | PSNR (dB) |
113+
|---|---:|---:|---:|---:|---:|
114+
| Conservative | ≈27.2% | 153.46 s | 136.11 s | **1.13×** | **26.47** |
115+
| Moderate | ≈32.2% | 153.37 s | 127.94 s | **1.20×** | **26.14** |
116+
| Aggressive | ≈37.3% | 153.50 s | 119.86 s | **1.28×** | **24.80** |
117+
118+
*PSNR is measured against dense outputs using FFmpeg's aggregate YUV metric.*
119+
120+
In an earlier evaluation using the moderate SVG policy, Wan2.2 at 720p retained **98.1% of the dense baseline’s mean VBench dimension score** across a 31-prompt, 16-dimension screening subset.
121+
122+
## Qualitative example
123+
124+
![Three rows of giraffe video frames, with dense attention on the left and SVG on the right.](images/svg/dense-vs-svg.png)
125+
126+
*Dense attention (left) and SVG (right), shown at three video frames. The overall scene and subject arrangement remain similar, with visible differences in ground texture, background detail, and coat patterns.*
127+
128+
This illustration is separate from the benchmark measurements above.
129+
130+
## Tests and profiling
131+
132+
Run the SVG tests from the repository root:
133+
134+
```bash
135+
python -m pytest -q \
136+
src/maxdiffusion/tests/wan/svg_attention_test.py \
137+
src/maxdiffusion/tests/wan/svg_balanced_rounding_test.py \
138+
src/maxdiffusion/tests/wan/svg_config_propagation_test.py \
139+
src/maxdiffusion/tests/wan/svg_head_local_test.py \
140+
src/maxdiffusion/tests/wan/wan_pipeline_signature_test.py
141+
```
142+
143+
The tests cover routing and layout, configuration propagation, schedules, sharding, unsupported configurations, and numerical agreement with attention references. Production-kernel tests cover sparse and density-one support, aligned and padded sequences, and natural and base-2 exponentials.
144+
145+
For CPU semantics checks, set `JAX_PLATFORMS=cpu` and `XLA_FLAGS=--xla_force_host_platform_device_count=8`. Run the suite separately on an eight-device TPU host to exercise the compiled kernels. Tests restricted to one platform are skipped on the other.
146+
147+
For profiling, enable `enable_jax_named_scopes=True` and capture a short warm denoising interval. Routing, placement, main attention, padding cleanup, merging, and restoration have named scopes, including `svg_route_profile`, `svg_layout_place`, `svg_union_main`, `svg_tail_cleanup`, `svg_lse_merge`, and `svg_layout_restore`. The `svg_kernel_c_tiles…` scope reports the fraction of physical tiles executed, which differs from attention-pair density and total transformer FLOP savings.

‎src/maxdiffusion/aot_cache.py‎

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,77 @@ def transformer_forward_pass(...):
7373
_FORMAT_VERSION = 1
7474

7575

76+
def _is_graphdef(x: Any) -> bool:
77+
return hasattr(x, "attributes") and hasattr(x, "nodes")
78+
79+
80+
def _graphdef_desc(gd: Any) -> str:
81+
"""Extracts a process-deterministic digest of static attributes in an nnx.GraphDef."""
82+
items = []
83+
for k, v in getattr(gd, "attributes", ()):
84+
if str(k).startswith("_pytree__"):
85+
continue
86+
if hasattr(v, "value"):
87+
try:
88+
s = json.dumps(
89+
v.value,
90+
sort_keys=True,
91+
default=lambda o: re.sub(r"0x[0-9a-fA-F]+", "@", repr(o)),
92+
)
93+
except Exception: # noqa: BLE001
94+
s = re.sub(r"0x[0-9a-fA-F]+", "@", repr(v.value))
95+
items.append(f"{k}:{s}")
96+
return "GraphDef(" + ",".join(items) + ")"
97+
98+
99+
def extract_svg_meta(config: Any, pipeline: Any = None) -> dict[str, Any]:
100+
"""Extracts all graph-affecting SVG configuration settings for AOT cache identity."""
101+
svg_keys = (
102+
"use_svg_attention",
103+
"svg_implementation",
104+
"svg_spatial_density",
105+
"svg_high_noise_density",
106+
"svg_low_noise_density",
107+
"svg_sample_max_row",
108+
"svg_profile_query_count",
109+
"svg_profile_seed",
110+
"svg_dense_layer_fraction",
111+
"svg_dense_timestep_fraction",
112+
"svg_active_start_step",
113+
"svg_active_end_step",
114+
"svg_active_start_layer",
115+
"svg_active_end_layer",
116+
"svg_num_train_timesteps",
117+
"svg_num_layers",
118+
"svg_include_first_frame",
119+
"svg_global_stride",
120+
"svg_global_offset",
121+
"svg_flash_block_sizes",
122+
)
123+
meta: dict[str, Any] = {}
124+
if config is not None:
125+
for k in svg_keys:
126+
val = getattr(config, k, None) if not isinstance(config, dict) else config.get(k, None)
127+
if val is not None:
128+
meta[k] = str(val)
129+
if pipeline is not None:
130+
for attr in ("transformer", "high_noise_transformer", "low_noise_transformer"):
131+
t = getattr(pipeline, attr, None)
132+
if t is not None:
133+
t_cfg = getattr(t, "config", None)
134+
attn_cfg = (
135+
getattr(t_cfg, "attention_config", None)
136+
or (t_cfg.get("attention_config") if isinstance(t_cfg, dict) else None)
137+
)
138+
if isinstance(attn_cfg, dict):
139+
meta[f"{attr}_svg_config"] = json.dumps(
140+
{k: v for k, v in attn_cfg.items() if "svg" in str(k)},
141+
sort_keys=True,
142+
default=str,
143+
)
144+
return meta
145+
146+
76147
def _metadata_fingerprint(meta: dict[str, Any]) -> str:
77148
"""Returns the stable filename fingerprint for install-time metadata."""
78149
serialized = json.dumps(meta, sort_keys=True, default=str)
@@ -87,15 +158,18 @@ def _dynamic_signature(args: tuple, kwargs: dict) -> str:
87158
addresses and hash-order-dependent content that differ per process and
88159
made signatures never match across restarts (measured: every array
89160
part stable, only the treedef part unstable). Static graph metadata
90-
not visible in key paths (attention kernel, dtypes, model path) is
91-
covered by the install-time config fingerprint in the filename.
161+
in GraphDef leaves is extracted deterministically via ``_graphdef_desc``.
92162
Array leaves contribute shape/dtype; non-array leaves (python scalars,
93163
None flags) contribute an address-stripped repr.
94164
"""
95-
leaves_with_paths = jax.tree_util.tree_flatten_with_path((args, kwargs))[0]
165+
leaves_with_paths = jax.tree_util.tree_flatten_with_path(
166+
(args, kwargs), is_leaf=_is_graphdef
167+
)[0]
96168
parts = []
97169
for path, leaf in leaves_with_paths:
98-
if hasattr(leaf, "shape") and hasattr(leaf, "dtype"):
170+
if _is_graphdef(leaf):
171+
desc = _graphdef_desc(leaf)
172+
elif hasattr(leaf, "shape") and hasattr(leaf, "dtype"):
99173
desc = f"{tuple(leaf.shape)}:{leaf.dtype}"
100174
else:
101175
desc = re.sub(r"0x[0-9a-fA-F]+", "@", repr(leaf))

‎src/maxdiffusion/configs/base_wan_14b.yml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,26 @@ jit_initializers: True
8383
# Set true to load weights from pytorch
8484
from_pt: True
8585
split_head_dim: True
86+
# Sparse VideoGen (SVG) Attention configuration
87+
use_svg_attention: False
88+
svg_spatial_density: 0.25
89+
svg_sample_max_row: 10000
90+
svg_profile_query_count: 64
91+
svg_profile_seed: 0
92+
svg_active_start_step: -1
93+
svg_active_end_step: -1
94+
svg_active_start_layer: -1
95+
svg_active_end_layer: -1
96+
svg_include_first_frame: True
97+
svg_high_noise_density: -1.0
98+
svg_low_noise_density: -1.0
99+
# Tiling for the sparse SVG kernel only. Empty means "reuse flash_block_sizes",
100+
# which is rarely what you want: the dense ring kernel is tuned for large kv
101+
# compute blocks and the sparse kernel for small ones. Must stay a dict so the
102+
# command line can override it with JSON.
103+
svg_flash_block_sizes: {}
86104
attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cudnn_flash_te, ring, tokamax_ring, ulysses, ulysses_custom, ulysses_ring
105+
87106
use_base2_exp: True
88107
use_experimental_scheduler: True
89108
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.

‎src/maxdiffusion/configs/base_wan_1_3b.yml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,26 @@ jit_initializers: True
8080
# Set true to load weights from pytorch
8181
from_pt: True
8282
split_head_dim: True
83+
# Sparse VideoGen (SVG) Attention configuration
84+
use_svg_attention: False
85+
svg_spatial_density: 0.25
86+
svg_sample_max_row: 10000
87+
svg_profile_query_count: 64
88+
svg_profile_seed: 0
89+
svg_active_start_step: -1
90+
svg_active_end_step: -1
91+
svg_active_start_layer: -1
92+
svg_active_end_layer: -1
93+
svg_include_first_frame: True
94+
svg_high_noise_density: -1.0
95+
svg_low_noise_density: -1.0
96+
# Tiling for the sparse SVG kernel only. Empty means "reuse flash_block_sizes",
97+
# which is rarely what you want: the dense ring kernel is tuned for large kv
98+
# compute blocks and the sparse kernel for small ones. Must stay a dict so the
99+
# command line can override it with JSON.
100+
svg_flash_block_sizes: {}
83101
attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cudnn_flash_te, ring, tokamax_ring, ulysses, ulysses_custom, ulysses_ring
102+
84103
use_base2_exp: True
85104
use_experimental_scheduler: True
86105
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.

‎src/maxdiffusion/configs/base_wan_27b.yml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,29 @@ jit_initializers: True
8383
# Set true to load weights from pytorch
8484
from_pt: True
8585
split_head_dim: True
86+
# Sparse VideoGen (SVG) Attention configuration
87+
use_svg_attention: False
88+
svg_spatial_density: 0.35
89+
svg_sample_max_row: 10000
90+
svg_profile_query_count: 64
91+
svg_profile_seed: 0
92+
svg_active_start_step: -1
93+
svg_active_end_step: -1
94+
svg_active_start_layer: -1
95+
svg_active_end_layer: -1
96+
svg_include_first_frame: True
97+
svg_high_noise_density: -1.0
98+
svg_low_noise_density: -1.0
99+
# Tiling for the sparse SVG kernel only. Empty means "reuse flash_block_sizes",
100+
# which is rarely what you want: the dense ring kernel is tuned for large kv
101+
# compute blocks and the sparse kernel for small ones. Must stay a dict so the
102+
# command line can override it with JSON. The tuned v6e-8 720p values are
103+
# {"block_q":3328,"block_kv":2816,"block_kv_compute":256,
104+
# "block_kv_compute_in":256,"heads_per_tile":1,"vmem_limit_bytes":67108864}.
105+
svg_flash_block_sizes: {}
86106
attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cudnn_flash_te, ring, tokamax_ring, tokamax_ring_custom, ulysses, ulysses_custom, ulysses_ring, ulysses_ring_custom, ulysses_ring_custom_bidir
107+
108+
87109
#
88110
# Best 2D-ring / USP (Ulysses x ring) configs for WAN2.2-T2V-A14B (720x1280, 81 frames)
89111
# Set attention=ulysses_ring_custom and ulysses_shards=U (ring degree R=CP/U):
@@ -314,6 +336,7 @@ max_train_steps: 1500
314336
num_train_epochs: 1
315337
seed: 0
316338
output_dir: 'sdxl-model-finetuned'
339+
tensorboard_dir: ''
317340
per_device_batch_size: 1.0
318341
# If global_batch_size % jax.device_count is not 0, use FSDP sharding.
319342
global_batch_size: 0

‎src/maxdiffusion/generate_wan.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
314314
"activations_dtype": str(config.activations_dtype),
315315
"scan_layers": str(config.scan_layers),
316316
"jax": jax.__version__,
317+
**aot_cache.extract_svg_meta(config, pipeline),
317318
},
318319
mesh=pipeline.mesh,
319320
)

0 commit comments

Comments
 (0)