|
| 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, 4096), 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, 4096), 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