Skip to content

Commit 0b094c9

Browse files
committed
LTX2.3 improvements and bug fixes
1 parent bda41e1 commit 0b094c9

10 files changed

Lines changed: 627 additions & 361 deletions

File tree

src/maxdiffusion/configs/ltx2_3_video.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
#hardware
22
hardware: 'tpu'
33
skip_jax_distributed_system: False
4-
attention: 'flash'
4+
attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cudnn_flash_te, ring, tokamax_ring, ulysses, ulysses_custom, ulysses_ring
5+
6+
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
7+
ulysses_shards: -1
8+
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
9+
ulysses_attention_chunks: 1
510
a2v_attention_kernel: 'flash'
611
v2a_attention_kernel: 'dot_product'
712
attention_sharding_uniform: True

src/maxdiffusion/configs/ltx2_video.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
#hardware
22
hardware: 'tpu'
33
skip_jax_distributed_system: False
4-
attention: 'flash'
4+
attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cudnn_flash_te, ring, tokamax_ring, ulysses, ulysses_custom, ulysses_ring
5+
6+
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
7+
ulysses_shards: -1
8+
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
9+
ulysses_attention_chunks: 1
510
a2v_attention_kernel: 'dot_product'
611
v2a_attention_kernel: 'dot_product'
712
attention_sharding_uniform: True

src/maxdiffusion/generate_ltx2.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import os
2020
import subprocess
2121
from maxdiffusion.checkpointing.ltx2_checkpointer import LTX2Checkpointer
22-
from maxdiffusion import pyconfig, max_logging, max_utils
22+
from maxdiffusion import aot_cache, pyconfig, max_logging, max_utils
2323
from absl import app
2424
from google.cloud import storage
2525
from google.api_core.exceptions import GoogleAPIError
@@ -189,14 +189,37 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
189189
original_enable_mld = config.get_keys().get("enable_ml_diagnostics", False)
190190
original_num_steps = config.get_keys().get("num_inference_steps", 40)
191191

192+
# Per-shape AOT executable cache
193+
aot_cache.install(
194+
getattr(config, "aot_cache_dir", ""),
195+
meta={
196+
"model": config.pretrained_model_name_or_path,
197+
"attention": getattr(config, "attention", ""),
198+
"flash_block_sizes": str(getattr(config, "flash_block_sizes", "")),
199+
"mesh_shape": str(pipeline.mesh.shape) if pipeline and hasattr(pipeline, "mesh") and pipeline.mesh else "",
200+
"weights_dtype": str(getattr(config, "weights_dtype", "bfloat16")),
201+
"activations_dtype": str(getattr(config, "activations_dtype", "bfloat16")),
202+
"scan_layers": str(getattr(config, "scan_layers", True)),
203+
"jax": jax.__version__,
204+
},
205+
mesh=pipeline.mesh if pipeline else None,
206+
)
207+
aot_cache.wait_for_loads()
208+
192209
# ---------------------------------------------------------
193210
# Run 1: Warmup Compilation (Original steps, NO profiling)
194211
# ---------------------------------------------------------
195212
config.get_keys()["enable_profiler"] = False
196213
config.get_keys()["enable_ml_diagnostics"] = False
214+
warmup_steps = min(2, original_num_steps)
215+
config.get_keys()["num_inference_steps"] = warmup_steps
216+
217+
max_logging.log(f"🚀 Starting warmup compilation pass ({warmup_steps} steps)...")
218+
with aot_cache.warmup_mode():
219+
_ = call_pipeline(config, pipeline, prompt, negative_prompt)
197220

198-
max_logging.log(f"🚀 Starting warmup compilation pass ({original_num_steps} steps)...")
199-
_ = call_pipeline(config, pipeline, prompt, negative_prompt)
221+
aot_cache.save_pending()
222+
config.get_keys()["num_inference_steps"] = original_num_steps
200223

201224
compile_time = time.perf_counter() - s0
202225
max_logging.log(f"compile_time: {compile_time}")
@@ -237,7 +260,9 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
237260

238261
# Export videos
239262
for i in range(len(videos)):
240-
video_path = f"{filename_prefix}ltx2_output_{getattr(config, 'seed', 0)}_{i}.mp4"
263+
model_name = getattr(config, "model_name", "ltx2") or "ltx2"
264+
model_name_prefix = model_name.replace(".", "_")
265+
video_path = f"{filename_prefix}{model_name_prefix}_output_{getattr(config, 'seed', 0)}_{i}.mp4"
241266
audio_i = audios[i] if audios is not None else None
242267

243268
audio_format = getattr(config, "audio_format", "s16")

src/maxdiffusion/models/attention_flax.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,7 +1914,7 @@ def __init__(
19141914
self,
19151915
mesh: Mesh,
19161916
attention_kernel: str,
1917-
scale: int,
1917+
scale: float,
19181918
heads: int,
19191919
dim_head: int,
19201920
use_memory_efficient_attention: bool = False,
@@ -2009,7 +2009,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
20092009
class AttentionOp(nn.Module):
20102010
mesh: Mesh
20112011
attention_kernel: str
2012-
scale: int
2012+
scale: float
20132013
heads: int
20142014
dim_head: int
20152015
use_memory_efficient_attention: bool = False

src/maxdiffusion/models/ltx2/attention_ltx2.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,8 @@ def apply_split_rotary_emb(x: Array, freqs: Tuple[Array, Array]) -> Array:
8888
first_x = split_x[..., 0, :]
8989
second_x = split_x[..., 1, :]
9090

91-
cos_u = jnp.expand_dims(cos, axis=-2)
92-
sin_u = jnp.expand_dims(sin, axis=-2)
93-
94-
out = split_x * cos_u
95-
96-
out_first = out[..., 0, :] - second_x * sin_u.squeeze(-2)
97-
out_second = out[..., 1, :] + first_x * sin_u.squeeze(-2)
91+
out_first = first_x * cos - second_x * sin
92+
out_second = second_x * cos + first_x * sin
9893

9994
out = jnp.stack([out_first, out_second], axis=-2)
10095
out = out.reshape(*out.shape[:-2], last_dim)
@@ -176,12 +171,6 @@ def prepare_video_coords(
176171
patch_ends = grid + patch_size_delta
177172

178173
# Combine start and end coordinates
179-
latent_coords = jnp.stack([grid, patch_ends], axis=-1) # [3, N_F, N_H, N_W, 2]
180-
latent_coords = latent_coords.transpose(1, 2, 3, 0, 4) # [N_F, N_H, N_W, 3, 2]
181-
latent_coords = latent_coords.reshape(-1, 3, 2) # [num_patches, 3, 2]
182-
latent_coords = jnp.expand_dims(latent_coords, 0) # [1, num_patches, 3, 2]
183-
latent_coords = jnp.tile(latent_coords, (batch_size, 1, 1, 1)) # [B, num_patches, 3, 2]
184-
185174
latent_coords = jnp.stack([grid, patch_ends], axis=-1) # [3, N_F, N_H, N_W, 2]
186175
latent_coords = latent_coords.reshape(3, -1, 2) # [3, num_patches, 2]
187176
latent_coords = jnp.expand_dims(latent_coords, 0) # [1, 3, num_patches, 2]
@@ -352,6 +341,8 @@ def __init__(
352341
flash_min_seq_length: int = 4096,
353342
sharding_specs: Optional[LTX2DiTShardingSpecs] = None,
354343
gated_attn: bool = False,
344+
ulysses_shards: int = -1,
345+
ulysses_attention_chunks: int = 1,
355346
):
356347
self.heads = heads
357348
self.rope_type = rope_type
@@ -445,17 +436,37 @@ def __init__(
445436
dtype=dtype,
446437
)
447438

439+
is_self_attention = context_dim is None
440+
if not is_self_attention:
441+
if attention_kernel in ("tokamax_ring", "tokamax_ring_custom", "ulysses_ring"):
442+
attention_kernel = "tokamax_flash" # do not use ring attention for cross attention
443+
if attention_kernel in ("ulysses_ring_custom", "ulysses_ring_custom_bidir"):
444+
attention_kernel = "ulysses_custom" # plain ulysses (no ring) for cross attention
445+
446+
axis_names_q = (common_types.BATCH, common_types.CROSS_ATTN_HEAD, common_types.CROSS_ATTN_Q_LENGTH, common_types.D_KV)
447+
axis_names_kv = (
448+
common_types.BATCH,
449+
common_types.CROSS_ATTN_HEAD,
450+
common_types.CROSS_ATTN_KV_LENGTH,
451+
common_types.D_KV,
452+
)
453+
else:
454+
axis_names_q = (common_types.BATCH, common_types.SELF_ATTN_HEAD, common_types.SELF_ATTN_Q_LENGTH, common_types.D_KV)
455+
axis_names_kv = (common_types.BATCH, common_types.SELF_ATTN_HEAD, common_types.SELF_ATTN_KV_LENGTH, common_types.D_KV)
456+
448457
self.attention_op = NNXAttentionOp(
449458
mesh=mesh,
450459
attention_kernel=attention_kernel,
451460
scale=dim_head**-0.5,
452461
heads=heads,
453462
dim_head=dim_head,
454463
dtype=dtype,
455-
axis_names_q=(common_types.BATCH, common_types.SELF_ATTN_HEAD, common_types.SELF_ATTN_Q_LENGTH, common_types.D_KV),
456-
axis_names_kv=(common_types.BATCH, common_types.SELF_ATTN_HEAD, common_types.SELF_ATTN_KV_LENGTH, common_types.D_KV),
464+
axis_names_q=axis_names_q,
465+
axis_names_kv=axis_names_kv,
457466
flash_block_sizes=flash_block_sizes,
458467
flash_min_seq_length=flash_min_seq_length,
468+
ulysses_shards=ulysses_shards,
469+
ulysses_attention_chunks=ulysses_attention_chunks,
459470
)
460471

461472
def __call__(
@@ -485,7 +496,7 @@ def __call__(
485496
# 3. Apply RoPE
486497
with jax.named_scope("Apply RoPE"):
487498
if rotary_emb is not None:
488-
if hasattr(self, "rope_type") and self.rope_type == "split":
499+
if self.rope_type == "split":
489500
# Split RoPE: passing full freqs [B, H, S, D//2]
490501
# apply_split_rotary_emb handles reshaping query/key
491502

src/maxdiffusion/models/ltx2/ltx2_utils.py

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import json
1818
from typing import Optional
1919
import torch
20+
import numpy as np
2021
import jax
2122
import jax.numpy as jnp
2223
from maxdiffusion import max_logging
@@ -184,6 +185,14 @@ def load_sharded_checkpoint(pretrained_model_name_or_path, subfolder, device, fi
184185
return tensors
185186

186187

188+
def _torch_tensor_to_numpy(tensor: torch.Tensor) -> np.ndarray:
189+
import ml_dtypes
190+
191+
if tensor.dtype == torch.bfloat16:
192+
return tensor.view(torch.uint16).numpy().view(ml_dtypes.bfloat16)
193+
return tensor.numpy()
194+
195+
187196
def load_transformer_weights(
188197
pretrained_model_name_or_path: str,
189198
eval_shapes: dict,
@@ -193,38 +202,110 @@ def load_transformer_weights(
193202
scan_layers: bool = True,
194203
subfolder: str = "transformer",
195204
):
205+
import threading
206+
import concurrent.futures
207+
import time
208+
196209
device = jax.local_devices(backend=device)[0]
197210
max_logging.log(f"Load and port {pretrained_model_name_or_path} {subfolder} on {device}")
198211

199-
with jax.default_device(device):
200-
# Support sharded loading
201-
tensors = load_sharded_checkpoint(pretrained_model_name_or_path, subfolder, device)
212+
index_file = "diffusion_pytorch_model.safetensors.index.json"
213+
try:
214+
index_path = hf_hub_download(pretrained_model_name_or_path, subfolder=subfolder, filename=index_file)
215+
with open(index_path, "r") as f:
216+
index_data = json.load(f)
217+
weight_map = index_data["weight_map"]
218+
shards = sorted(set(weight_map.values()))
202219

203-
flax_state_dict = {}
204-
cpu = jax.local_devices(backend="cpu")[0]
205-
flattened_dict = flatten_dict(eval_shapes)
220+
def resolve_shard_path(model_file):
221+
return hf_hub_download(pretrained_model_name_or_path, subfolder=subfolder, filename=model_file)
206222

207-
random_flax_state_dict = {}
208-
for key in flattened_dict:
209-
random_flax_state_dict[tuple(str(item) for item in key)] = flattened_dict[key]
223+
except EntryNotFoundError:
224+
shards = ["diffusion_pytorch_model.safetensors"]
210225

211-
for pt_key, tensor in tensors.items():
212-
renamed_pt_key = rename_key(pt_key)
213-
renamed_pt_key = rename_for_ltx2_transformer(renamed_pt_key)
226+
def resolve_shard_path(model_file):
227+
try:
228+
return hf_hub_download(pretrained_model_name_or_path, subfolder=subfolder, filename=model_file)
229+
except EntryNotFoundError:
230+
return hf_hub_download(pretrained_model_name_or_path, subfolder=subfolder, filename="diffusion_pytorch_model.bin")
214231

215-
pt_tuple_key = tuple(renamed_pt_key.split("."))
232+
t_start = time.perf_counter()
216233

217-
flax_key, flax_tensor = get_key_and_value(
218-
pt_tuple_key, tensor, flax_state_dict, random_flax_state_dict, scan_layers, num_layers
219-
)
234+
flattened_dict = flatten_dict(eval_shapes)
235+
random_flax_state_dict = {}
236+
for key in flattened_dict:
237+
random_flax_state_dict[tuple(str(item) for item in key)] = flattened_dict[key]
220238

221-
flax_state_dict[flax_key] = jax.device_put(jnp.asarray(flax_tensor), device=cpu)
239+
flax_state_dict = {}
240+
dict_lock = threading.Lock()
241+
242+
def convert_chunk(ckpt_shard_path, chunk_keys):
243+
if ckpt_shard_path.endswith(".safetensors"):
244+
with safe_open(ckpt_shard_path, framework="pt") as f:
245+
for pt_key in chunk_keys:
246+
tensor = _torch_tensor_to_numpy(f.get_tensor(pt_key))
247+
process_tensor(pt_key, tensor)
248+
else:
249+
loaded_state_dict = torch.load(ckpt_shard_path, map_location="cpu")
250+
for pt_key in chunk_keys:
251+
tensor = _torch_tensor_to_numpy(loaded_state_dict[pt_key])
252+
process_tensor(pt_key, tensor)
253+
254+
def process_tensor(pt_key, tensor):
255+
renamed_pt_key = rename_key(pt_key)
256+
renamed_pt_key = rename_for_ltx2_transformer(renamed_pt_key)
257+
pt_tuple_key = tuple(renamed_pt_key.split("."))
258+
259+
block_index = None
260+
if scan_layers and len(pt_tuple_key) > 0 and "transformer_blocks_" in pt_tuple_key[0]:
261+
import re
262+
263+
m = re.match(r"transformer_blocks_(\d+)", pt_tuple_key[0])
264+
if m:
265+
block_index = int(m.group(1))
266+
pt_tuple_key = ("transformer_blocks",) + pt_tuple_key[1:]
222267

223-
validate_flax_state_dict(eval_shapes, flax_state_dict)
224-
flax_state_dict = unflatten_dict(flax_state_dict)
225-
del tensors
226-
jax.clear_caches()
227-
return flax_state_dict
268+
flax_key, flax_tensor = rename_key_and_reshape_tensor(pt_tuple_key, tensor, random_flax_state_dict, scan_layers)
269+
flax_key_str = [str(k) for k in flax_key]
270+
if "scale_shift_table" in flax_key_str and flax_key_str[-1] in ["kernel", "weight"]:
271+
flax_key_str.pop()
272+
flax_key = tuple(flax_key_str)
273+
flax_key = _tuple_str_to_int(flax_key)
274+
275+
if block_index is not None:
276+
with dict_lock:
277+
stacked = flax_state_dict.get(flax_key)
278+
if stacked is None:
279+
stacked = np.empty((num_layers,) + flax_tensor.shape, dtype=flax_tensor.dtype)
280+
flax_state_dict[flax_key] = stacked
281+
stacked[block_index] = flax_tensor
282+
else:
283+
value = np.array(flax_tensor, dtype=flax_tensor.dtype, copy=True, order="C")
284+
with dict_lock:
285+
flax_state_dict[flax_key] = value
286+
287+
chunk_size = 32
288+
tasks = []
289+
for model_file in shards:
290+
ckpt_shard_path = resolve_shard_path(model_file)
291+
if ckpt_shard_path.endswith(".safetensors"):
292+
with safe_open(ckpt_shard_path, framework="pt") as f:
293+
shard_keys = list(f.keys())
294+
else:
295+
loaded_state_dict = torch.load(ckpt_shard_path, map_location="cpu")
296+
shard_keys = list(loaded_state_dict.keys())
297+
for i in range(0, len(shard_keys), chunk_size):
298+
tasks.append((ckpt_shard_path, shard_keys[i : i + chunk_size]))
299+
300+
with concurrent.futures.ThreadPoolExecutor(max_workers=32) as executor:
301+
futures = [executor.submit(convert_chunk, path, keys) for path, keys in tasks]
302+
for future in concurrent.futures.as_completed(futures):
303+
future.result()
304+
305+
validate_flax_state_dict(eval_shapes, flax_state_dict)
306+
flax_state_dict = unflatten_dict(flax_state_dict)
307+
max_logging.log(f"Converted weights in {time.perf_counter() - t_start:.1f}s")
308+
return flax_state_dict
228309

229310

230311
def load_vae_weights(

src/maxdiffusion/models/ltx2/text_encoders/torchax_text_encoder.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@
2020
import jax
2121
from torchax import interop, default_env
2222

23-
# --- Monkeypatch transformers masking_utils to avoid torchax integer tracing bug ---
23+
import contextlib
2424
import transformers.masking_utils
2525

26-
_orig_sliding_window_overlay = transformers.masking_utils.sliding_window_overlay
27-
2826

2927
def _patched_sliding_window_overlay(sliding_window: int):
3028
# pylint: disable=unused-argument
@@ -44,6 +42,16 @@ def inner_mask(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool:
4442
return inner_mask
4543

4644

45+
@contextlib.contextmanager
46+
def patch_sliding_window_overlay():
47+
orig = transformers.masking_utils.sliding_window_overlay
48+
transformers.masking_utils.sliding_window_overlay = _patched_sliding_window_overlay
49+
try:
50+
yield
51+
finally:
52+
transformers.masking_utils.sliding_window_overlay = orig
53+
54+
4755
class TorchaxGemma3TextEncoder(interop.JittableModule):
4856
"""
4957
A jittable Torchax module for wrapping the HuggingFace PyTorch
@@ -57,8 +65,7 @@ def __call__(
5765
self, input_ids: jax.Array, attention_mask: jax.Array, output_hidden_states: bool = True
5866
) -> Tuple[jax.Array, ...]:
5967
# Dynamically patch transformers.masking_utils only during the duration of this call
60-
transformers.masking_utils.sliding_window_overlay = _patched_sliding_window_overlay
61-
try:
68+
with patch_sliding_window_overlay():
6269
with default_env():
6370
input_ids = interop.torch_view(input_ids)
6471
attention_mask = interop.torch_view(attention_mask)
@@ -72,9 +79,6 @@ def __call__(
7279
output_hidden_states=output_hidden_states,
7380
)
7481
return interop.jax_view(output)
75-
finally:
76-
# Restore original behavior to prevent side effects on other potential models in same env
77-
transformers.masking_utils.sliding_window_overlay = _orig_sliding_window_overlay
7882

7983
@staticmethod
8084
def _forward_inner(model, input_ids, attention_mask, output_hidden_states=True):

0 commit comments

Comments
 (0)