Skip to content

Commit 71ff539

Browse files
committed
Port PR 447 (Tile-size auto-tuning) to LTX2 and LTX2.3
1 parent 510b87c commit 71ff539

4 files changed

Lines changed: 333 additions & 0 deletions

File tree

src/maxdiffusion/configs/ltx2_3_video.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,11 @@ upsampler_output_type: "pil"
183183
aot_cache_dir: ''
184184
converted_weights_dir: ''
185185

186+
187+
# Tile-size auto-tuning. When enable_tile_search: True, generate_ltx2 runs a fast one-DiT-block
188+
# grid search (maxdiffusion/utils/tile_size_grid_search.py) before inference and overwrites
189+
# flash_block_sizes' block_q/block_kv/block_kv_compute with the winner. Default off (no-op).
190+
enable_tile_search: False
191+
tile_search_mode: 'smart' # 'smart' (VMEM-capped candidate ladders) | 'full' (2D sweep)
192+
tile_search_iters: 10
193+
tile_search_out: '' # dir for the results CSV; '' -> print only

src/maxdiffusion/configs/ltx2_video.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,11 @@ upsampler_output_type: "pil"
180180
aot_cache_dir: ''
181181
converted_weights_dir: ''
182182

183+
184+
# Tile-size auto-tuning. When enable_tile_search: True, generate_ltx2 runs a fast one-DiT-block
185+
# grid search (maxdiffusion/utils/tile_size_grid_search.py) before inference and overwrites
186+
# flash_block_sizes' block_q/block_kv/block_kv_compute with the winner. Default off (no-op).
187+
enable_tile_search: False
188+
tile_search_mode: 'smart' # 'smart' (VMEM-capped candidate ladders) | 'full' (2D sweep)
189+
tile_search_iters: 10
190+
tile_search_out: '' # dir for the results CSV; '' -> print only

src/maxdiffusion/generate_ltx2.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,48 @@ def call_pipeline(config, pipeline, prompt, negative_prompt):
113113
return out
114114

115115

116+
def maybe_tune_block_sizes(config):
117+
"""If enable_tile_search, run a fast one-DiT-block tile-size grid search and overwrite
118+
flash_block_sizes' block_q/block_kv/block_kv_compute with the winner IN PLACE.
119+
"""
120+
keys = config.get_keys()
121+
val = keys.get("enable_tile_search", False)
122+
if str(val).lower() not in ("true", "1", "yes"):
123+
return
124+
from maxdiffusion.utils.tile_size_grid_search import grid_search
125+
from maxdiffusion.utils.ltx2_block_benchmark import LTX2BlockBenchmark
126+
127+
mesh = jax.sharding.Mesh(max_utils.create_device_mesh(config), config.mesh_axes)
128+
bench = LTX2BlockBenchmark.from_config(config, mesh)
129+
max_logging.log(f"[tile-search] tuning block sizes for {bench.label} before inference...")
130+
result = grid_search(
131+
bench,
132+
mode=keys.get("tile_search_mode", "smart"),
133+
iters=keys.get("tile_search_iters", 10),
134+
out_dir=(keys.get("tile_search_out", "") or None),
135+
log=max_logging.log,
136+
)
137+
if result.best is None:
138+
max_logging.log("[tile-search] no config succeeded; keeping configured flash_block_sizes")
139+
return
140+
fbs = dict(config.flash_block_sizes)
141+
fbs.update({
142+
"block_q": result.best.bq,
143+
"block_kv": result.best.bkv,
144+
"block_kv_compute": result.best.bkv_compute,
145+
"block_kv_compute_in": result.best.bkv_compute,
146+
})
147+
config.get_keys()["flash_block_sizes"] = fbs
148+
max_logging.log(
149+
f"[tile-search] using block_q={result.best.bq} block_kv={result.best.bkv} "
150+
f"(block-bench {result.best.mean_ms:.2f} ms)"
151+
)
152+
153+
116154
def run(config, pipeline=None, filename_prefix="", commit_hash=None):
155+
if pipeline is None:
156+
maybe_tune_block_sizes(config)
157+
117158
writer = max_utils.initialize_summary_writer(config)
118159
if jax.process_index() == 0 and writer:
119160
max_logging.log(f"TensorBoard logs will be written to: {config.tensorboard_dir}")
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
"""
2+
LTX2 implementation of the model-agnostic `BlockBenchmark` (see tile_size_grid_search.py).
3+
"""
4+
import os
5+
os.environ.setdefault(
6+
"LIBTPU_INIT_ARGS",
7+
" ".join([
8+
"--xla_tpu_dvfs_p_state=7",
9+
"--xla_tpu_spmd_rng_bit_generator_unsafe=true",
10+
"--xla_tpu_enable_dot_strength_reduction=true",
11+
"--xla_tpu_enable_async_collective_fusion_fuse_all_gather=true",
12+
"--xla_enable_async_collective_permute=true",
13+
"--xla_tpu_enable_async_collective_fusion=true",
14+
"--xla_tpu_enable_async_collective_fusion_multiple_steps=true",
15+
"--xla_tpu_overlap_compute_collective_tc=true",
16+
"--xla_enable_async_all_gather=true",
17+
"--xla_tpu_scoped_vmem_limit_kib=65536",
18+
"--xla_tpu_enable_async_all_to_all=true",
19+
"--xla_tpu_enable_all_experimental_scheduler_features=true",
20+
"--xla_tpu_enable_latency_hiding_scheduler=true",
21+
"--xla_tpu_enable_megacore_fusion=true",
22+
]),
23+
)
24+
os.environ.setdefault("JAX_DEFAULT_MATMUL_PRECISION", "bfloat16")
25+
os.environ.setdefault("XLA_PYTHON_CLIENT_MEM_FRACTION", "0.95")
26+
27+
import jax
28+
import jax.numpy as jnp
29+
from flax import linen as nn
30+
from flax import nnx
31+
from flax.linen import partitioning as nn_partitioning
32+
33+
from maxdiffusion import max_logging, max_utils, pyconfig
34+
from maxdiffusion.models.ltx2.transformer_ltx2 import LTX2VideoTransformer3DModel
35+
from maxdiffusion.utils.tile_size_grid_search import (
36+
BenchResult,
37+
BlockBenchmark,
38+
grid_search,
39+
time_callable,
40+
)
41+
42+
_IN_CHANNELS = 128 # LTX2 uses 128 channels in latent space
43+
_VAE_T, _VAE_S = 8, 32 # LTX2 spatial/temporal compression: patch_size_t=1, patch_size=1 (it actually operates on 32x32 compressed) Wait, LTX2 spatial downsampling is 32x32 in pixel space, so in latent space it's 1x1 patch? No, let's just use the latent shape.
44+
45+
def latent_seq_len(num_frames: int, height: int, width: int) -> int:
46+
lf = (num_frames - 1) // 8 + 1
47+
lh, lw = height // 32, width // 32
48+
return lf * lh * lw
49+
50+
def tiled_seq_len(full_seq: int, attention: str, context_shards: int, ulysses_shards: int) -> int:
51+
_RING_VARIANTS = {"ulysses_ring_custom", "ulysses_ring_custom_bidir", "tokamax_ring", "ring"}
52+
if attention in _RING_VARIANTS and ulysses_shards >= 1 and context_shards >= 1:
53+
ring_shards = max(1, context_shards // max(1, ulysses_shards))
54+
return full_seq // ring_shards
55+
return full_seq
56+
57+
@nnx.jit
58+
def _forward(model, latents, timestep, prompt_embeds, prompt_attention_mask, audio_latents, audio_prompt_embeds, audio_prompt_attention_mask, num_frames, height, width):
59+
return model(
60+
hidden_states=latents,
61+
audio_hidden_states=audio_latents,
62+
encoder_hidden_states=prompt_embeds,
63+
audio_encoder_hidden_states=audio_prompt_embeds,
64+
timestep=timestep,
65+
audio_timestep=None,
66+
sigma=None,
67+
audio_sigma=None,
68+
encoder_attention_mask=prompt_attention_mask,
69+
audio_encoder_attention_mask=audio_prompt_attention_mask,
70+
num_frames=num_frames,
71+
height=height,
72+
width=width,
73+
fps=24.0,
74+
audio_num_frames=128,
75+
video_coords=None,
76+
audio_coords=None,
77+
attention_kwargs=None,
78+
use_cross_timestep=False,
79+
deterministic=True,
80+
rngs=None,
81+
)
82+
83+
class LTX2BlockBenchmark(BlockBenchmark):
84+
def __init__(
85+
self,
86+
config,
87+
mesh,
88+
*,
89+
num_frames,
90+
height,
91+
width,
92+
batch=None,
93+
vmem_limit_bytes=64 * 1024 * 1024,
94+
):
95+
self._config = config
96+
self._mesh = mesh
97+
self._rules = config.logical_axis_rules
98+
self._attention = getattr(config, "attention", "flash")
99+
self._context_shards = int(mesh.shape.get("context", 1))
100+
self._ulysses_shards = int(getattr(config, "ulysses_shards", 1) or 1)
101+
self._vmem = int(vmem_limit_bytes)
102+
self.label = f"ltx2/{self._attention}/u{self._ulysses_shards}"
103+
104+
self._lf = (num_frames - 1) // 8 + 1
105+
self._lh, self._lw = height // 32, width // 32
106+
self._full_seq = latent_seq_len(num_frames, height, width)
107+
self._num_frames_orig = num_frames
108+
self._height_orig = height
109+
self._width_orig = width
110+
111+
data_shards = int(mesh.shape.get("data", 1)) * int(mesh.shape.get("fsdp", 1))
112+
self._batch = batch if batch is not None else max(1, data_shards)
113+
self._hf_cfg = LTX2VideoTransformer3DModel.load_config(config.pretrained_model_name_or_path, subfolder="transformer")
114+
self._inputs = self._make_inputs()
115+
116+
@classmethod
117+
def from_config(cls, config, mesh, **overrides):
118+
return cls(
119+
config,
120+
mesh,
121+
num_frames=config.num_frames,
122+
height=config.height,
123+
width=config.width,
124+
**overrides,
125+
)
126+
127+
def tiled_seq_lens(self):
128+
s = tiled_seq_len(self._full_seq, self._attention, self._context_shards, self._ulysses_shards)
129+
return (s, s)
130+
131+
def vmem_bytes(self):
132+
return self._vmem
133+
134+
def run(self, bq, bkv, *, bkv_compute=None, iters=10, warmup=2):
135+
cmp = bkv_compute or bkv
136+
try:
137+
with self._mesh:
138+
model = self._build_model(bq, bkv, cmp)
139+
latents, timestep, prompt_embeds, prompt_attention_mask, audio_latents, audio_prompt_embeds, audio_prompt_attention_mask = self._inputs
140+
with self._mesh, nn_partitioning.axis_rules(self._rules):
141+
mean, std, times, compile_ms = time_callable(
142+
lambda: _forward(model, latents, timestep, prompt_embeds, prompt_attention_mask, audio_latents, audio_prompt_embeds, audio_prompt_attention_mask, self._lf, self._lh, self._lw),
143+
iters=iters,
144+
warmup=warmup,
145+
sync=jax.block_until_ready,
146+
)
147+
return BenchResult(
148+
bq,
149+
bkv,
150+
cmp,
151+
"ok",
152+
mean_ms=mean,
153+
std_ms=std,
154+
times_ms=times,
155+
compile_ms=compile_ms,
156+
)
157+
except Exception as e:
158+
import traceback
159+
traceback.print_exc()
160+
msg = str(e)
161+
oom = any(t in msg for t in ("RESOURCE_EXHAUSTED", "out of memory", "Mosaic", "VMEM"))
162+
return BenchResult(bq, bkv, cmp, "oom" if oom else "error", detail=msg[:200])
163+
164+
def _flash_block_sizes(self, bq, bkv, cmp):
165+
from maxdiffusion.max_utils import CustomFlashBlockSizes
166+
return CustomFlashBlockSizes(
167+
block_q=bq,
168+
block_kv=bkv,
169+
block_kv_compute=cmp,
170+
block_kv_compute_in=cmp,
171+
heads_per_tile=1,
172+
vmem_limit_bytes=self._vmem,
173+
)
174+
175+
def _build_model(self, bq, bkv, cmp):
176+
c = self._config
177+
ltx2_config = dict(self._hf_cfg)
178+
if ltx2_config.get("activation_fn") == "gelu-approximate":
179+
ltx2_config["activation_fn"] = "gelu"
180+
ltx2_config.update(
181+
mesh=self._mesh,
182+
dtype=c.activations_dtype,
183+
weights_dtype=c.weights_dtype,
184+
attention_kernel=c.attention,
185+
a2v_attention_kernel=getattr(c, "a2v_attention_kernel", "flash"),
186+
v2a_attention_kernel=getattr(c, "v2a_attention_kernel", "dot_product"),
187+
precision=max_utils.get_precision(c),
188+
flash_block_sizes=self._flash_block_sizes(bq, bkv, cmp),
189+
flash_min_seq_length=getattr(c, "flash_min_seq_length", 4096),
190+
ulysses_shards=getattr(c, "ulysses_shards", -1),
191+
ulysses_attention_chunks=getattr(c, "ulysses_attention_chunks", 1),
192+
remat_policy=getattr(c, "remat_policy", "NONE"),
193+
scan_layers=False,
194+
num_layers=1,
195+
)
196+
model = LTX2VideoTransformer3DModel(**ltx2_config, rngs=nnx.Rngs(params=0))
197+
gd, state, rest = nnx.split(model, nnx.Param, ...)
198+
shardings = nn.logical_to_mesh_sharding(nnx.get_partition_spec(state), self._mesh, self._rules)
199+
return nnx.merge(gd, jax.device_put(state, shardings), rest)
200+
201+
def _make_inputs(self):
202+
dtype = self._config.activations_dtype
203+
k1, k2, k3, k4 = jax.random.split(jax.random.key(0), 4)
204+
seq_len = self._lf * self._lh * self._lw
205+
latents = jax.random.normal(k1, (self._batch, seq_len, _IN_CHANNELS), dtype)
206+
207+
# Audio latents
208+
audio_seq_len = 128
209+
audio_latents = jax.random.normal(k3, (self._batch, audio_seq_len, _IN_CHANNELS), dtype)
210+
211+
# Prompts
212+
prompt_embeds = jax.random.normal(k2, (self._batch, 1024, 4096), dtype)
213+
prompt_attention_mask = jnp.ones((self._batch, 1024), dtype=jnp.int32)
214+
215+
audio_prompt_embeds = jax.random.normal(k4, (self._batch, 1024, 4096), dtype)
216+
audio_prompt_attention_mask = jnp.ones((self._batch, 1024), dtype=jnp.int32)
217+
218+
timestep = jnp.zeros((self._batch,), jnp.float32)
219+
repl = jax.sharding.NamedSharding(self._mesh, jax.sharding.PartitionSpec())
220+
return (
221+
jax.device_put(latents, repl),
222+
jax.device_put(timestep, repl),
223+
jax.device_put(prompt_embeds, repl),
224+
jax.device_put(prompt_attention_mask, repl),
225+
jax.device_put(audio_latents, repl),
226+
jax.device_put(audio_prompt_embeds, repl),
227+
jax.device_put(audio_prompt_attention_mask, repl),
228+
)
229+
230+
def _build_cli_config(argv_yaml, attention, ulysses_shards, num_frames, height, width):
231+
pyconfig.initialize([
232+
"ltx2_block_benchmark",
233+
argv_yaml,
234+
f"attention={attention}",
235+
f"ulysses_shards={ulysses_shards}",
236+
"skip_jax_distributed_system=True",
237+
"weights_dtype=bfloat16",
238+
"activations_dtype=bfloat16",
239+
"per_device_batch_size=1",
240+
f"num_frames={num_frames}",
241+
f"height={height}",
242+
f"width={width}",
243+
"ici_data_parallelism=1",
244+
"ici_fsdp_parallelism=1",
245+
f"ici_context_parallelism={jax.device_count()}",
246+
"ici_tensor_parallelism=1",
247+
"allow_split_physical_axes=True",
248+
"use_base2_exp=true",
249+
"use_experimental_scheduler=true",
250+
"enable_jax_named_scopes=false",
251+
])
252+
return pyconfig.config
253+
254+
def main():
255+
import argparse
256+
parser = argparse.ArgumentParser()
257+
parser.add_argument("config", help="Path to yaml config (e.g. configs/ltx2_video.yml)")
258+
parser.add_argument("--attention", default="ulysses_ring_custom")
259+
parser.add_argument("--ulysses-shards", type=int, default=1)
260+
parser.add_argument("--num-frames", type=int, default=161)
261+
parser.add_argument("--height", type=int, default=512)
262+
parser.add_argument("--width", type=int, default=768)
263+
parser.add_argument("--smart-search", action="store_true")
264+
parser.add_argument("--full-search", action="store_true")
265+
parser.add_argument("--out-dir", default="")
266+
args = parser.parse_args()
267+
268+
config = _build_cli_config(args.config, args.attention, args.ulysses_shards, args.num_frames, args.height, args.width)
269+
mesh = jax.sharding.Mesh(max_utils.create_device_mesh(config), config.mesh_axes)
270+
bench = LTX2BlockBenchmark.from_config(config, mesh)
271+
272+
mode = "full" if args.full_search else "smart"
273+
grid_search(bench, mode=mode, out_dir=args.out_dir or None, log=max_logging.log)
274+
275+
if __name__ == "__main__":
276+
main()

0 commit comments

Comments
 (0)