Skip to content

Commit be6859b

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

4 files changed

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

0 commit comments

Comments
 (0)