Skip to content

Commit 2de1082

Browse files
committed
Feat: Optinal tile size search before e2e inference
1 parent 5fe8bcf commit 2de1082

20 files changed

Lines changed: 1339 additions & 15 deletions

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
[![Unit Tests](https://github.com/AI-Hypercomputer/maxdiffusion/actions/workflows/UnitTests.yml/badge.svg)](https://github.com/AI-Hypercomputer/maxdiffusion/actions/workflows/UnitTests.yml)
1818

1919
# What's new?
20+
- **`2026/07/14`**: Automatic attention tile-size (`block_q`/`block_kv`) search for Wan is now supported.
21+
- **`2026/06/26`**: 2D ring (USP) attention with a custom splash kernel is now supported for Wan (`ulysses_ring_custom`), splitting context parallelism into an intra-chip Ulysses axis and a cross-chip ring axis.
2022
- **`2026/04/16`**: Support for Tokamax Ring Attention kernel is now added.
2123
- **`2026/03/31`**: Wan2.2 SenCache inference is now supported for T2V and I2V (up to 1.4x speedup)
2224
- **`2026/03/25`**: Wan2.1 and Wan2.2 Magcache inference is now supported
@@ -690,6 +692,10 @@ We added ring attention support for Wan models. Below are the stats for one `720
690692

691693
(* There are some known stability issues for ring attention on 16 TPUs, please use `tokamax_flash` attention instead.)
692694

695+
### Automatic Tile-Size Search
696+
697+
The optimal attention tile sizes (`block_q` / `block_kv`) depend on the sequence length, VMEM, sharding, and accelerator, and the feasibility edge is a VMEM OOM with no clean closed form — so we tune them empirically. Passing `enable_tile_search=true` to `generate_wan.py` runs a fast one-DiT-block grid search before inference and injects the winning block sizes into `flash_block_sizes` (`tile_search_mode=smart` by default; the search is opt-in and off by default). The core is model-agnostic (`utils/tile_size_grid_search.py`) with a per-model plug (`utils/wan_block_benchmark.py`), which also runs standalone via `python -m maxdiffusion.utils.wan_block_benchmark ... --smart-search`.
698+
693699
## Flux
694700

695701
First make sure you have permissions to access the Flux repos in Huggingface.

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,19 @@ packages = ["src/maxdiffusion", "src/install_maxdiffusion_extra_deps"]
9292

9393
[tool.ruff]
9494
# Never enforce `E501` (line length violations).
95+
line-length = 119
96+
97+
[tool.ruff.lint]
9598
ignore = ["C901", "E501", "E741", "F402", "F823", "E402", "I001"]
9699
select = ["C", "E", "F", "I", "W"]
97-
line-length = 119
98100

99101
# Ignore import violations in all `__init__.py` files.
100-
[tool.ruff.per-file-ignores]
102+
[tool.ruff.lint.per-file-ignores]
101103
"__init__.py" = ["E402", "F401", "F403", "F811"]
102104
"src/maxdiffusion/utils/dummy_*.py" = ["F401"]
103105
"src/maxdiffusion/pyconfig.py" = ["E721"]
104106

105-
[tool.ruff.isort]
107+
[tool.ruff.lint.isort]
106108
lines-after-imports = 2
107109
known-first-party = ["maxdiffusion"]
108110

src/maxdiffusion/configs/base_wan_14b.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ use_base2_exp: True
8888
use_experimental_scheduler: True
8989
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
9090
ulysses_shards: -1
91+
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
92+
# For communication-compute overlap to be effective, enable the following XLA flags:
93+
# --xla_tpu_enable_async_all_to_all=true
94+
# --xla_tpu_overlap_compute_collective_tc=true
95+
# (Refer to README.md for the full recommended XLA_FLAGS list)
96+
ulysses_attention_chunks: 1
9197
flash_min_seq_length: 4096
9298
dropout: 0.0
9399

@@ -136,6 +142,14 @@ flash_block_sizes: {
136142
# "block_kv_dq" : 3072
137143
# }
138144
# GroupNorm groups
145+
# Tile-size auto-tuning. When enable_tile_search: True, generate_wan runs a fast one-DiT-block
146+
# grid search (maxdiffusion/utils/tile_size_grid_search.py) before inference and overwrites
147+
# flash_block_sizes' block_q/block_kv/block_kv_compute with the winner. Default off (no-op).
148+
enable_tile_search: False
149+
tile_search_mode: 'smart' # 'smart' (VMEM-capped candidate ladders) | 'full' (2D sweep)
150+
tile_search_iters: 10
151+
tile_search_out: '' # dir for the results CSV; '' -> print only
152+
139153
norm_num_groups: 32
140154

141155
# train text_encoder - Currently not supported for SDXL

src/maxdiffusion/configs/base_wan_1_3b.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ use_base2_exp: True
8585
use_experimental_scheduler: True
8686
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
8787
ulysses_shards: -1
88+
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
89+
# For communication-compute overlap to be effective, enable the following XLA flags:
90+
# --xla_tpu_enable_async_all_to_all=true
91+
# --xla_tpu_overlap_compute_collective_tc=true
92+
# (Refer to README.md for the full recommended XLA_FLAGS list)
93+
ulysses_attention_chunks: 1
8894
flash_min_seq_length: 0
8995

9096
# If mask_padding_tokens is True, we pass in segment ids to splash attention to avoid attending to padding tokens.
@@ -110,6 +116,14 @@ flash_block_sizes: {
110116
"use_fused_bwd_kernel": False,
111117
}
112118
# GroupNorm groups
119+
# Tile-size auto-tuning. When enable_tile_search: True, generate_wan runs a fast one-DiT-block
120+
# grid search (maxdiffusion/utils/tile_size_grid_search.py) before inference and overwrites
121+
# flash_block_sizes' block_q/block_kv/block_kv_compute with the winner. Default off (no-op).
122+
enable_tile_search: False
123+
tile_search_mode: 'smart' # 'smart' (VMEM-capped candidate ladders) | 'full' (2D sweep)
124+
tile_search_iters: 10
125+
tile_search_out: '' # dir for the results CSV; '' -> print only
126+
113127
norm_num_groups: 32
114128

115129
# train text_encoder - Currently not supported for SDXL

src/maxdiffusion/configs/base_wan_27b.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ use_base2_exp: True
9494
use_experimental_scheduler: True
9595
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
9696
ulysses_shards: -1
97+
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
98+
# For communication-compute overlap to be effective, enable the following XLA flags:
99+
# --xla_tpu_enable_async_all_to_all=true
100+
# --xla_tpu_overlap_compute_collective_tc=true
101+
# (Refer to README.md for the full recommended XLA_FLAGS list)
102+
ulysses_attention_chunks: 1
97103
flash_min_seq_length: 4096
98104
dropout: 0.0
99105

@@ -130,6 +136,14 @@ flash_block_sizes: {
130136
# "block_kv_dq" : 2048
131137
# "use_fused_bwd_kernel": False,
132138
# }
139+
# Tile-size auto-tuning. When enable_tile_search: True, generate_wan runs a fast one-DiT-block
140+
# grid search (maxdiffusion/utils/tile_size_grid_search.py) before inference and overwrites
141+
# flash_block_sizes' block_q/block_kv/block_kv_compute with the winner. Default off (no-op).
142+
enable_tile_search: False
143+
tile_search_mode: 'smart' # 'smart' (VMEM-capped candidate ladders) | 'full' (2D sweep)
144+
tile_search_iters: 10
145+
tile_search_out: '' # dir for the results CSV; '' -> print only
146+
133147
# GroupNorm groups
134148
norm_num_groups: 32
135149

src/maxdiffusion/configs/base_wan_animate.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ use_base2_exp: True
8686
use_experimental_scheduler: True
8787
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
8888
ulysses_shards: -1
89+
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
90+
# For communication-compute overlap to be effective, enable the following XLA flags:
91+
# --xla_tpu_enable_async_all_to_all=true
92+
# --xla_tpu_overlap_compute_collective_tc=true
93+
# (Refer to README.md for the full recommended XLA_FLAGS list)
94+
ulysses_attention_chunks: 1
8995
flash_min_seq_length: 4096
9096
# If mask_padding_tokens is True, we pass in segment ids to splash attention to avoid attending to padding tokens.
9197
# Else we do not pass in segment ids and on vpu bound hardware like trillium this is faster.
@@ -125,6 +131,14 @@ flash_block_sizes: {
125131
# "use_fused_bwd_kernel": False,
126132
# }
127133
# GroupNorm groups
134+
# Tile-size auto-tuning. When enable_tile_search: True, generate_wan runs a fast one-DiT-block
135+
# grid search (maxdiffusion/utils/tile_size_grid_search.py) before inference and overwrites
136+
# flash_block_sizes' block_q/block_kv/block_kv_compute with the winner. Default off (no-op).
137+
enable_tile_search: False
138+
tile_search_mode: 'smart' # 'smart' (VMEM-capped candidate ladders) | 'full' (2D sweep)
139+
tile_search_iters: 10
140+
tile_search_out: '' # dir for the results CSV; '' -> print only
141+
128142
norm_num_groups: 32
129143

130144
# Text encoder training keys are unused by generate_wan_animate.py.

src/maxdiffusion/configs/base_wan_i2v_14b.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ use_base2_exp: True
8888
use_experimental_scheduler: True
8989
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
9090
ulysses_shards: -1
91+
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
92+
# For communication-compute overlap to be effective, enable the following XLA flags:
93+
# --xla_tpu_enable_async_all_to_all=true
94+
# --xla_tpu_overlap_compute_collective_tc=true
95+
# (Refer to README.md for the full recommended XLA_FLAGS list)
96+
ulysses_attention_chunks: 1
9197
flash_min_seq_length: 4096
9298
dropout: 0.0
9399

@@ -123,6 +129,14 @@ flash_block_sizes: {
123129
# "use_fused_bwd_kernel": False,
124130
# }
125131
# GroupNorm groups
132+
# Tile-size auto-tuning. When enable_tile_search: True, generate_wan runs a fast one-DiT-block
133+
# grid search (maxdiffusion/utils/tile_size_grid_search.py) before inference and overwrites
134+
# flash_block_sizes' block_q/block_kv/block_kv_compute with the winner. Default off (no-op).
135+
enable_tile_search: False
136+
tile_search_mode: 'smart' # 'smart' (VMEM-capped candidate ladders) | 'full' (2D sweep)
137+
tile_search_iters: 10
138+
tile_search_out: '' # dir for the results CSV; '' -> print only
139+
126140
norm_num_groups: 32
127141

128142
# train text_encoder - Currently not supported for SDXL

src/maxdiffusion/configs/base_wan_i2v_27b.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ use_base2_exp: True
8888
use_experimental_scheduler: True
8989
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
9090
ulysses_shards: -1
91+
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
92+
# For communication-compute overlap to be effective, enable the following XLA flags:
93+
# --xla_tpu_enable_async_all_to_all=true
94+
# --xla_tpu_overlap_compute_collective_tc=true
95+
# (Refer to README.md for the full recommended XLA_FLAGS list)
96+
ulysses_attention_chunks: 1
9197
flash_min_seq_length: 4096
9298
dropout: 0.0
9399

@@ -124,6 +130,14 @@ flash_block_sizes: {
124130
# "use_fused_bwd_kernel": False,
125131
# }
126132
# GroupNorm groups
133+
# Tile-size auto-tuning. When enable_tile_search: True, generate_wan runs a fast one-DiT-block
134+
# grid search (maxdiffusion/utils/tile_size_grid_search.py) before inference and overwrites
135+
# flash_block_sizes' block_q/block_kv/block_kv_compute with the winner. Default off (no-op).
136+
enable_tile_search: False
137+
tile_search_mode: 'smart' # 'smart' (VMEM-capped candidate ladders) | 'full' (2D sweep)
138+
tile_search_iters: 10
139+
tile_search_out: '' # dir for the results CSV; '' -> print only
140+
127141
norm_num_groups: 32
128142

129143
# train text_encoder - Currently not supported for SDXL

src/maxdiffusion/generate_wan.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,50 @@ def inference_generate_video(config, pipeline, filename_prefix=""):
194194
return
195195

196196

197+
def maybe_tune_block_sizes(config):
198+
"""If enable_tile_search, run a fast one-DiT-block tile-size grid search and overwrite
199+
flash_block_sizes' block_q/block_kv/block_kv_compute with the winner IN PLACE, before the
200+
transformer (which bakes block sizes in at construction) is built.
201+
202+
Flags are read defensively so this is a safe no-op (grid search OFF) for any config that
203+
doesn't declare them -- not every WAN yaml carries the tile_search_* keys."""
204+
keys = config.get_keys()
205+
if not keys.get("enable_tile_search", False):
206+
return
207+
from maxdiffusion.utils.tile_size_grid_search import grid_search
208+
from maxdiffusion.utils.wan_block_benchmark import WanBlockBenchmark
209+
210+
mesh = jax.sharding.Mesh(max_utils.create_device_mesh(config), config.mesh_axes)
211+
bench = WanBlockBenchmark.from_config(config, mesh)
212+
max_logging.log(f"[tile-search] tuning block sizes for {bench.label} before inference...")
213+
result = grid_search(
214+
bench,
215+
mode=keys.get("tile_search_mode", "smart"),
216+
iters=keys.get("tile_search_iters", 10),
217+
out_dir=(keys.get("tile_search_out", "") or None),
218+
log=max_logging.log,
219+
)
220+
if result.best is None:
221+
max_logging.log("[tile-search] no config succeeded; keeping configured flash_block_sizes")
222+
return
223+
fbs = dict(config.flash_block_sizes)
224+
fbs.update({
225+
"block_q": result.best.bq,
226+
"block_kv": result.best.bkv,
227+
"block_kv_compute": result.best.bkv_compute,
228+
"block_kv_compute_in": result.best.bkv_compute,
229+
})
230+
config.get_keys()["flash_block_sizes"] = fbs # config is immutable via setattr; mutate raw dict
231+
max_logging.log(
232+
f"[tile-search] using block_q={result.best.bq} block_kv={result.best.bkv} "
233+
f"(block-bench {result.best.mean_ms:.2f} ms)"
234+
)
235+
236+
197237
def run(config, pipeline=None, filename_prefix="", commit_hash=None):
198238
model_key = config.model_name
239+
if pipeline is None:
240+
maybe_tune_block_sizes(config)
199241
writer = max_utils.initialize_summary_writer(config)
200242
if jax.process_index() == 0 and writer:
201243
max_logging.log(f"TensorBoard logs will be written to: {config.tensorboard_dir}")
@@ -236,7 +278,10 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
236278
pipeline, _, _ = checkpoint_loader.load_checkpoint(checkpoint_step)
237279
else:
238280
pipeline = checkpoint_loader.load_pretrained_pipeline_or_diffusers(
239-
config, pipeline_cls, pretrained_state_sources, pretrained_config_transformer_attr
281+
config,
282+
pipeline_cls,
283+
pretrained_state_sources,
284+
pretrained_config_transformer_attr,
240285
)
241286
load_time = time.perf_counter() - load_start
242287
max_logging.log(f"load_time: {load_time:.1f}s")
@@ -340,7 +385,11 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
340385
num_videos = num_devices * config.per_device_batch_size
341386
if num_videos > 0:
342387
generation_time_per_video = generation_time / num_videos
343-
writer.add_scalar("inference/generation_time_per_video", generation_time_per_video, global_step=0)
388+
writer.add_scalar(
389+
"inference/generation_time_per_video",
390+
generation_time_per_video,
391+
global_step=0,
392+
)
344393
max_logging.log(f"generation time per video: {generation_time_per_video}")
345394
else:
346395
max_logging.log("Warning: Number of videos is zero, cannot calculate generation_time_per_video.")
@@ -384,7 +433,11 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
384433
generation_time_with_profiler = time.perf_counter() - s0
385434
max_logging.log(f"generation_time_with_profiler: {generation_time_with_profiler}")
386435
if writer and jax.process_index() == 0:
387-
writer.add_scalar("inference/generation_time_with_profiler", generation_time_with_profiler, global_step=0)
436+
writer.add_scalar(
437+
"inference/generation_time_with_profiler",
438+
generation_time_with_profiler,
439+
global_step=0,
440+
)
388441

389442
return saved_video_path
390443

0 commit comments

Comments
 (0)