linoyts HF Staff commited on
Commit
8077a04
·
verified ·
1 Parent(s): 5d1731a

Switch to HDR IC-LoRA app with on-the-fly PromptEncoder + bundled ltx-2-internal

Browse files

Replaces distilled app with HDRICLoraPipeline. Prompt encoded through Gemma each run. Bundles ltx-2-internal for the HDR code path (not on public Lightricks/LTX-2 main).

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +4 -4
  2. app.py +281 -193
  3. ltx-2-internal/LICENSE +381 -0
  4. ltx-2-internal/README.md +66 -0
  5. ltx-2-internal/README.public.md +121 -0
  6. ltx-2-internal/conftest.py +217 -0
  7. ltx-2-internal/internal/ltx-kernels/MANIFEST.in +1 -0
  8. ltx-2-internal/internal/ltx-kernels/README.md +43 -0
  9. ltx-2-internal/internal/ltx-kernels/csrc/all2all/all2all.cpp +408 -0
  10. ltx-2-internal/internal/ltx-kernels/csrc/all2all/all2all.hpp +231 -0
  11. ltx-2-internal/internal/ltx-kernels/csrc/all2all/cuda/all2all_heads.cu +372 -0
  12. ltx-2-internal/internal/ltx-kernels/csrc/all2all/cuda/allgather.cu +197 -0
  13. ltx-2-internal/internal/ltx-kernels/csrc/all2all/cuda/api.cuh +99 -0
  14. ltx-2-internal/internal/ltx-kernels/csrc/include/cuda/configs.cuh +90 -0
  15. ltx-2-internal/internal/ltx-kernels/csrc/include/cuda/exceptions.cuh +170 -0
  16. ltx-2-internal/internal/ltx-kernels/csrc/include/cuda/utils.cuh +356 -0
  17. ltx-2-internal/internal/ltx-kernels/csrc/include/event.hpp +114 -0
  18. ltx-2-internal/internal/ltx-kernels/pyproject.toml +18 -0
  19. ltx-2-internal/internal/ltx-kernels/setup.py +39 -0
  20. ltx-2-internal/internal/ltx-kernels/src/ltx_kernels/__init__.py +6 -0
  21. ltx-2-internal/internal/ltx-kernels/src/ltx_kernels/all_to_all.py +90 -0
  22. ltx-2-internal/internal/test-txt2img-parity/conftest.py +85 -0
  23. ltx-2-internal/internal/test-txt2img-parity/pyproject.toml +38 -0
  24. ltx-2-internal/internal/test-txt2img-parity/src/test_txt2img_parity/__init__.py +0 -0
  25. ltx-2-internal/internal/test-txt2img-parity/src/test_txt2img_parity/testing_facilities.py +282 -0
  26. ltx-2-internal/internal/test-txt2img-parity/uv.lock +0 -0
  27. ltx-2-internal/packages/ltx-bench/README.md +65 -0
  28. ltx-2-internal/packages/ltx-bench/benchmarks/__init__.py +0 -0
  29. ltx-2-internal/packages/ltx-bench/benchmarks/components/__init__.py +0 -0
  30. ltx-2-internal/packages/ltx-bench/benchmarks/components/conftest.py +140 -0
  31. ltx-2-internal/packages/ltx-bench/benchmarks/components/test_audio_vae.py +119 -0
  32. ltx-2-internal/packages/ltx-bench/benchmarks/components/test_component_loading.py +68 -0
  33. ltx-2-internal/packages/ltx-bench/benchmarks/components/test_lora_fusion.py +56 -0
  34. ltx-2-internal/packages/ltx-bench/benchmarks/components/test_text_encoder.py +45 -0
  35. ltx-2-internal/packages/ltx-bench/benchmarks/components/test_transformer.py +282 -0
  36. ltx-2-internal/packages/ltx-bench/benchmarks/components/test_upsampler.py +67 -0
  37. ltx-2-internal/packages/ltx-bench/benchmarks/components/test_video_vae.py +127 -0
  38. ltx-2-internal/packages/ltx-bench/benchmarks/conftest.py +190 -0
  39. ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/__init__.py +0 -0
  40. ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/conftest.py +10 -0
  41. ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/test_pipeline_distilled.py +88 -0
  42. ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/test_pipeline_two_stage.py +178 -0
  43. ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/test_pipeline_two_stage_hq.py +101 -0
  44. ltx-2-internal/packages/ltx-bench/pyproject.toml +14 -0
  45. ltx-2-internal/packages/ltx-bench/src/ltx_bench/__init__.py +31 -0
  46. ltx-2-internal/packages/ltx-bench/src/ltx_bench/conftest.py +215 -0
  47. ltx-2-internal/packages/ltx-bench/src/ltx_bench/core.py +499 -0
  48. ltx-2-internal/packages/ltx-bench/src/ltx_bench/instrument.py +375 -0
  49. ltx-2-internal/packages/ltx-bench/src/ltx_bench/memory_graph.py +146 -0
  50. ltx-2-internal/packages/ltx-bench/src/ltx_bench/nvtx.py +26 -0
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
- title: LTX 2.3 Distilled
3
- emoji: 📚
4
  colorFrom: indigo
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.8.0
8
  python_version: '3.12'
@@ -10,4 +10,4 @@ app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: LTX 2.3 HDR
3
+ emoji: 🎞️
4
  colorFrom: indigo
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.8.0
8
  python_version: '3.12'
 
10
  pinned: false
11
  ---
12
 
13
+ LTX-2.3 HDR IC-LoRA Gradio app (ZeroGPU). Requires `HF_TOKEN` secret with access to `Lightricks/LTX-2.3` and `diffusers-internal-dev/LTX-HDR-LoRA`.
app.py CHANGED
@@ -1,43 +1,77 @@
1
  import os
2
  import subprocess
3
  import sys
 
4
 
5
  # Disable torch.compile / dynamo before any torch import
6
  os.environ["TORCH_COMPILE_DISABLE"] = "1"
7
  os.environ["TORCHDYNAMO_DISABLE"] = "1"
 
 
8
 
9
  # Install xformers for memory-efficient attention
10
- subprocess.run([sys.executable, "-m", "pip", "install", "xformers==0.0.32.post2", "--no-build-isolation"], check=False)
11
-
12
- # Clone LTX-2 repo and install packages
13
- LTX_REPO_URL = "https://github.com/Lightricks/LTX-2.git"
14
- LTX_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LTX-2")
15
- LTX_COMMIT_SHA = "ae855f8538843825f9015a419cf4ba5edaf5eec2"
16
 
17
- if not os.path.exists(LTX_REPO_DIR):
18
- print(f"Cloning {LTX_REPO_URL}...")
19
- os.makedirs(LTX_REPO_DIR)
20
- subprocess.run(["git", "init", LTX_REPO_DIR], check=True)
21
- subprocess.run(["git", "remote", "add", "origin", LTX_REPO_URL], cwd=LTX_REPO_DIR, check=True)
22
- subprocess.run(["git", "fetch", "--depth", "1", "origin", LTX_COMMIT_SHA], cwd=LTX_REPO_DIR, check=True)
23
- subprocess.run(["git", "checkout", LTX_COMMIT_SHA], cwd=LTX_REPO_DIR, check=True)
 
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- print("Installing ltx-core and ltx-pipelines from cloned repo...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  subprocess.run(
28
- [sys.executable, "-m", "pip", "install", "--force-reinstall", "--no-deps", "-e",
29
- os.path.join(LTX_REPO_DIR, "packages", "ltx-core"),
30
- "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines")],
 
31
  check=True,
32
  )
33
-
34
- sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines", "src"))
35
- sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-core", "src"))
36
 
37
  import logging
38
  import random
39
  import tempfile
40
- from pathlib import Path
41
 
42
  import torch
43
  torch._dynamo.config.suppress_errors = True
@@ -48,13 +82,18 @@ import gradio as gr
48
  import numpy as np
49
  from huggingface_hub import hf_hub_download, snapshot_download
50
 
51
- from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number
52
  from ltx_core.quantization import QuantizationPolicy
53
- from ltx_pipelines.distilled import DistilledPipeline
54
- from ltx_pipelines.utils.args import ImageConditioningInput
55
- from ltx_pipelines.utils.media_io import encode_video
 
 
 
 
 
56
 
57
- # Force-patch xformers attention into the LTX attention module.
58
  from ltx_core.model.transformer import attention as _attn_mod
59
  print(f"[ATTN] Before patch: memory_efficient_attention={_attn_mod.memory_efficient_attention}")
60
  try:
@@ -66,253 +105,302 @@ except Exception as e:
66
 
67
  logging.getLogger().setLevel(logging.INFO)
68
 
 
 
 
 
69
  MAX_SEED = np.iinfo(np.int32).max
70
- DEFAULT_PROMPT = (
71
- "An astronaut hatches from a fragile egg on the surface of the Moon, "
72
- "the shell cracking and peeling apart in gentle low-gravity motion. "
73
- "Fine lunar dust lifts and drifts outward with each movement, floating "
74
- "in slow arcs before settling back onto the ground."
75
- )
76
- DEFAULT_FRAME_RATE = 24.0
77
 
78
- # Resolution presets: (width, height)
79
  RESOLUTIONS = {
80
- "high": {"16:9": (1536, 1024), "9:16": (1024, 1536), "1:1": (1024, 1024)},
81
- "low": {"16:9": (768, 512), "9:16": (512, 768), "1:1": (768, 768)},
 
 
82
  }
83
 
84
- # Model repos
85
  LTX_MODEL_REPO = "Lightricks/LTX-2.3"
 
 
86
  GEMMA_REPO = "google/gemma-3-12b-it-qat-q4_0-unquantized"
87
 
88
- # Download model checkpoints
 
 
89
  print("=" * 80)
90
- print("Downloading LTX-2.3 distilled model + Gemma...")
91
  print("=" * 80)
92
 
93
- checkpoint_path = hf_hub_download(repo_id=LTX_MODEL_REPO, filename="ltx-2.3-22b-distilled-1.1.safetensors")
94
- spatial_upsampler_path = hf_hub_download(repo_id=LTX_MODEL_REPO, filename="ltx-2.3-spatial-upscaler-x2-1.1.safetensors")
 
95
  gemma_root = snapshot_download(repo_id=GEMMA_REPO)
96
 
97
  print(f"Checkpoint: {checkpoint_path}")
98
  print(f"Spatial upsampler: {spatial_upsampler_path}")
 
99
  print(f"Gemma root: {gemma_root}")
100
 
101
- # Initialize pipeline WITH text encoder
102
- pipeline = DistilledPipeline(
103
- distilled_checkpoint_path=checkpoint_path,
104
- spatial_upsampler_path=spatial_upsampler_path,
 
 
 
 
 
 
 
 
105
  gemma_root=gemma_root,
106
- loras=[],
107
- quantization=QuantizationPolicy.fp8_cast(),
108
  )
109
 
110
- # Preload all models for ZeroGPU tensor packing.
111
- print("Preloading all models (including Gemma)...")
112
- ledger = pipeline.model_ledger
113
- _transformer = ledger.transformer()
114
- _video_encoder = ledger.video_encoder()
115
- _video_decoder = ledger.video_decoder()
116
- _audio_decoder = ledger.audio_decoder()
117
- _vocoder = ledger.vocoder()
118
- _spatial_upsampler = ledger.spatial_upsampler()
119
- _text_encoder = ledger.text_encoder()
120
- _embeddings_processor = ledger.gemma_embeddings_processor()
121
-
122
- ledger.transformer = lambda: _transformer
123
- ledger.video_encoder = lambda: _video_encoder
124
- ledger.video_decoder = lambda: _video_decoder
125
- ledger.audio_decoder = lambda: _audio_decoder
126
- ledger.vocoder = lambda: _vocoder
127
- ledger.spatial_upsampler = lambda: _spatial_upsampler
128
- ledger.text_encoder = lambda: _text_encoder
129
- ledger.gemma_embeddings_processor = lambda: _embeddings_processor
130
- print("All models preloaded (including Gemma text encoder)!")
131
 
132
- print("=" * 80)
133
- print("Pipeline ready!")
134
- print("=" * 80)
135
 
 
 
 
 
 
 
 
136
 
137
- def log_memory(tag: str):
138
- if torch.cuda.is_available():
139
- allocated = torch.cuda.memory_allocated() / 1024**3
140
- peak = torch.cuda.max_memory_allocated() / 1024**3
141
- free, total = torch.cuda.mem_get_info()
142
- print(f"[VRAM {tag}] allocated={allocated:.2f}GB peak={peak:.2f}GB free={free / 1024**3:.2f}GB total={total / 1024**3:.2f}GB")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
 
145
- def detect_aspect_ratio(image) -> str:
146
- """Detect the closest aspect ratio (16:9, 9:16, or 1:1) from an image."""
147
- if image is None:
 
 
148
  return "16:9"
149
- if hasattr(image, "size"):
150
- w, h = image.size
151
- elif hasattr(image, "shape"):
152
- h, w = image.shape[:2]
153
- else:
154
  return "16:9"
155
- ratio = w / h
156
- candidates = {"16:9": 16 / 9, "9:16": 9 / 16, "1:1": 1.0}
 
 
157
  return min(candidates, key=lambda k: abs(ratio - candidates[k]))
158
 
159
 
160
- def on_image_upload(image, high_res):
161
- """Auto-set resolution when image is uploaded."""
162
- aspect = detect_aspect_ratio(image)
163
  tier = "high" if high_res else "low"
164
- w, h = RESOLUTIONS[tier][aspect]
165
- return gr.update(value=w), gr.update(value=h)
166
 
167
 
168
- def on_highres_toggle(image, high_res):
169
- """Update resolution when high-res toggle changes."""
170
- aspect = detect_aspect_ratio(image)
171
- tier = "high" if high_res else "low"
172
- w, h = RESOLUTIONS[tier][aspect]
173
- return gr.update(value=w), gr.update(value=h)
174
-
175
-
176
- @spaces.GPU(duration=75)
 
 
 
 
 
 
 
177
  @torch.inference_mode()
178
  def generate_video(
179
- input_image,
180
  prompt: str,
181
  duration: float,
182
- enhance_prompt: bool = True,
183
- seed: int = 42,
184
- randomize_seed: bool = True,
185
- height: int = 1024,
186
- width: int = 1536,
 
 
187
  progress=gr.Progress(track_tqdm=True),
188
  ):
 
189
  try:
190
  torch.cuda.reset_peak_memory_stats()
191
- log_memory("start")
192
 
193
- current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
 
194
 
195
- frame_rate = DEFAULT_FRAME_RATE
 
 
196
  num_frames = int(duration * frame_rate) + 1
197
  num_frames = ((num_frames - 1 + 7) // 8) * 8 + 1
198
 
199
- print(f"Generating: {height}x{width}, {num_frames} frames ({duration}s), seed={current_seed}")
200
 
201
- images = []
202
- if input_image is not None:
203
- output_dir = Path("outputs")
204
- output_dir.mkdir(exist_ok=True)
205
- temp_image_path = output_dir / f"temp_input_{current_seed}.jpg"
206
- if hasattr(input_image, "save"):
207
- input_image.save(temp_image_path)
208
- else:
209
- temp_image_path = Path(input_image)
210
- images = [ImageConditioningInput(path=str(temp_image_path), frame_idx=0, strength=1.0)]
211
 
212
- tiling_config = TilingConfig.default()
213
- video_chunks_number = get_video_chunks_number(num_frames, tiling_config)
 
 
 
214
 
215
- log_memory("before pipeline call")
 
216
 
217
- video, audio = pipeline(
218
- prompt=prompt,
219
  seed=current_seed,
220
- height=int(height),
221
- width=int(width),
222
  num_frames=num_frames,
223
- frame_rate=frame_rate,
224
- images=images,
225
  tiling_config=tiling_config,
226
- enhance_prompt=enhance_prompt,
227
  )
228
 
229
- log_memory("after pipeline call")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
- output_path = tempfile.mktemp(suffix=".mp4")
232
- encode_video(
233
- video=video,
234
- fps=frame_rate,
235
- audio=audio,
236
- output_path=output_path,
237
- video_chunks_number=video_chunks_number,
238
- )
239
 
240
- log_memory("after encode_video")
241
- return str(output_path), current_seed
242
 
243
  except Exception as e:
244
  import traceback
245
- log_memory("on error")
246
- print(f"Error: {str(e)}\n{traceback.format_exc()}")
247
- return None, current_seed
248
 
249
 
250
- with gr.Blocks(title="LTX-2.3 Distilled") as demo:
251
- gr.Markdown("# LTX-2.3 Distilled (22B): Fast Audio-Video Generation")
252
- gr.Markdown(
253
- "Fast and high quality video + audio generation "
254
- "[[model]](https://huggingface.co/Lightricks/LTX-2.3) "
255
- "[[code]](https://github.com/Lightricks/LTX-2)"
256
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
  with gr.Row():
259
- with gr.Column():
260
- input_image = gr.Image(label="Input Image (Optional)", type="pil")
 
261
  prompt = gr.Textbox(
262
  label="Prompt",
263
- info="for best results - make it as elaborate as possible",
264
- value="Make this image come alive with cinematic motion, smooth animation",
265
- lines=3,
266
- placeholder="Describe the motion and animation you want...",
267
  )
268
-
269
- with gr.Row():
270
- duration = gr.Slider(label="Duration (seconds)", minimum=1.0, maximum=10.0, value=3.0, step=0.1)
271
- with gr.Column():
272
- enhance_prompt = gr.Checkbox(label="Enhance Prompt", value=False)
273
- high_res = gr.Checkbox(label="High Resolution", value=True)
274
 
275
- generate_btn = gr.Button("Generate Video", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
  with gr.Accordion("Advanced Settings", open=False):
 
 
 
 
 
 
 
 
 
 
 
278
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, value=10, step=1)
279
  randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
280
- with gr.Row():
281
- width = gr.Number(label="Width", value=1536, precision=0)
282
- height = gr.Number(label="Height", value=1024, precision=0)
283
-
284
- with gr.Column():
285
- output_video = gr.Video(label="Generated Video", autoplay=True)
286
-
287
- # Auto-detect aspect ratio from uploaded image and set resolution
288
- input_image.change(
289
- fn=on_image_upload,
290
- inputs=[input_image, high_res],
291
- outputs=[width, height],
292
- )
293
 
294
- # Update resolution when high-res toggle changes
295
- high_res.change(
296
- fn=on_highres_toggle,
297
- inputs=[input_image, high_res],
298
- outputs=[width, height],
 
 
 
299
  )
300
 
301
  generate_btn.click(
302
  fn=generate_video,
303
  inputs=[
304
- input_image, prompt, duration, enhance_prompt,
305
- seed, randomize_seed, height, width,
306
  ],
307
- outputs=[output_video, seed],
308
  )
309
 
310
 
311
- css = """
312
- .fillable{max-width: 1200px !important}
313
- .progress-text {color: white}
314
- """
315
-
316
  if __name__ == "__main__":
317
- demo.launch(theme=gr.themes.Citrus(), css=css)
318
-
 
1
  import os
2
  import subprocess
3
  import sys
4
+ from pathlib import Path
5
 
6
  # Disable torch.compile / dynamo before any torch import
7
  os.environ["TORCH_COMPILE_DISABLE"] = "1"
8
  os.environ["TORCHDYNAMO_DISABLE"] = "1"
9
+ # OpenImageIO / OpenEXR readers need this to be set before cv2 import
10
+ os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
11
 
12
  # Install xformers for memory-efficient attention
13
+ subprocess.run(
14
+ [sys.executable, "-m", "pip", "install", "xformers==0.0.32.post2", "--no-build-isolation"],
15
+ check=False,
16
+ )
 
 
17
 
18
+ # Video / HDR dependencies
19
+ subprocess.run(
20
+ [sys.executable, "-m", "pip", "install",
21
+ "imageio[ffmpeg]", "scikit-image", "opencv-python-headless",
22
+ "decord", "num2words", "OpenImageIO"],
23
+ check=False,
24
+ )
25
+ subprocess.run([sys.executable, "-m", "pip", "install", "num2words"], check=True)
26
 
27
+ # Reinstall torchaudio to match the torch CUDA version on this space.
28
+ _tv = subprocess.run(
29
+ [sys.executable, "-c", "import torch; print(torch.__version__)"],
30
+ capture_output=True, text=True,
31
+ )
32
+ if _tv.returncode == 0:
33
+ _full_ver = _tv.stdout.strip()
34
+ _cuda_suffix = _full_ver.split("+")[-1] if "+" in _full_ver else "cu124"
35
+ _base_ver = _full_ver.split("+")[0]
36
+ print(f"Detected torch {_full_ver}, reinstalling matching torchaudio...")
37
+ subprocess.run(
38
+ [sys.executable, "-m", "pip", "install", "--force-reinstall", "--no-deps",
39
+ f"torchaudio=={_base_ver}",
40
+ "--index-url", f"https://download.pytorch.org/whl/{_cuda_suffix}"],
41
+ check=False,
42
+ )
43
 
44
+ # ─────────────────────────────────────────────────────────────────────────────
45
+ # ltx-core / ltx-pipelines source
46
+ #
47
+ # The HDRICLoraPipeline and its supporting modules (ltx_core.hdr,
48
+ # ltx_pipelines.utils.blocks, load_video_conditioning_hdr, apply_hdr_decode_postprocess,
49
+ # save_exr_tensor, encode_exr_sequence_to_mp4) are NOT on the public main
50
+ # branch at the pinned commit used by the outpaint app. We install from the
51
+ # local ltx-2-internal checkout so the HDR code path actually exists.
52
+ # ─────────────────────────────────────────────────────────────────────────────
53
+ LTX_INTERNAL = Path(os.environ.get(
54
+ "LTX_INTERNAL_PATH",
55
+ str(Path(__file__).resolve().parent / "ltx-2-internal"),
56
+ ))
57
+ LTX_CORE_PKG = LTX_INTERNAL / "packages" / "ltx-core"
58
+ LTX_PIPELINES_PKG = LTX_INTERNAL / "packages" / "ltx-pipelines"
59
+
60
+ print(f"Installing ltx-core + ltx-pipelines from {LTX_INTERNAL}...")
61
  subprocess.run(
62
+ [sys.executable, "-m", "pip", "install",
63
+ "--force-reinstall", "--no-deps",
64
+ "-e", str(LTX_CORE_PKG),
65
+ "-e", str(LTX_PIPELINES_PKG)],
66
  check=True,
67
  )
68
+ sys.path.insert(0, str(LTX_PIPELINES_PKG / "src"))
69
+ sys.path.insert(0, str(LTX_CORE_PKG / "src"))
 
70
 
71
  import logging
72
  import random
73
  import tempfile
74
+ import zipfile
75
 
76
  import torch
77
  torch._dynamo.config.suppress_errors = True
 
82
  import numpy as np
83
  from huggingface_hub import hf_hub_download, snapshot_download
84
 
85
+ from ltx_core.model.video_vae import TilingConfig
86
  from ltx_core.quantization import QuantizationPolicy
87
+ from ltx_pipelines.hdr_ic_lora import HDRICLoraPipeline, _make_tiling_config
88
+ from ltx_pipelines.utils.blocks import PromptEncoder
89
+ from ltx_pipelines.utils.media_io import (
90
+ encode_exr_sequence_to_mp4,
91
+ get_videostream_metadata,
92
+ save_exr_tensor,
93
+ )
94
+ from ltx_pipelines.utils.types import OffloadMode
95
 
96
+ # xformers attention patch (same as the outpaint app).
97
  from ltx_core.model.transformer import attention as _attn_mod
98
  print(f"[ATTN] Before patch: memory_efficient_attention={_attn_mod.memory_efficient_attention}")
99
  try:
 
105
 
106
  logging.getLogger().setLevel(logging.INFO)
107
 
108
+
109
+ # ─────────────────────────────────────────────────────────────────────────────
110
+ # Constants + model downloads
111
+ # ─────────────────────────────────────────────────────────────────────────────
112
  MAX_SEED = np.iinfo(np.int32).max
 
 
 
 
 
 
 
113
 
114
+ # Frames must satisfy (n-1) % 8 == 0. Aspect-ratio canvas sizes (divisible by 32).
115
  RESOLUTIONS = {
116
+ "low": {"16:9": (768, 512), "9:16": (512, 768), "1:1": (768, 768),
117
+ "4:3": (768, 576), "3:4": (576, 768), "21:9": (768, 384)},
118
+ "high": {"16:9": (1536, 1024), "9:16": (1024, 1536), "1:1": (1024, 1024),
119
+ "4:3": (1536, 1152), "3:4": (1152, 1536), "21:9": (1536, 768)},
120
  }
121
 
 
122
  LTX_MODEL_REPO = "Lightricks/LTX-2.3"
123
+ DISTILLED_CHECKPOINT = "ltx-2.3-22b-distilled-1.1.safetensors"
124
+ SPATIAL_UPSCALER = "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"
125
  GEMMA_REPO = "google/gemma-3-12b-it-qat-q4_0-unquantized"
126
 
127
+ HDR_LORA_REPO = "diffusers-internal-dev/LTX-HDR-LoRA"
128
+ HDR_LORA_FILENAME = "comfyui_models_loras_ltxv_ltx2_ltx-2.3-22b-ic-lora-hdr-0.9 (4).safetensors"
129
+
130
  print("=" * 80)
131
+ print("Downloading LTX-2.3 distilled + spatial upsampler + Gemma + HDR IC-LoRA...")
132
  print("=" * 80)
133
 
134
+ checkpoint_path = hf_hub_download(repo_id=LTX_MODEL_REPO, filename=DISTILLED_CHECKPOINT)
135
+ spatial_upsampler_path = hf_hub_download(repo_id=LTX_MODEL_REPO, filename=SPATIAL_UPSCALER)
136
+ hdr_lora_path = hf_hub_download(repo_id=HDR_LORA_REPO, filename=HDR_LORA_FILENAME)
137
  gemma_root = snapshot_download(repo_id=GEMMA_REPO)
138
 
139
  print(f"Checkpoint: {checkpoint_path}")
140
  print(f"Spatial upsampler: {spatial_upsampler_path}")
141
+ print(f"HDR IC-LoRA: {hdr_lora_path}")
142
  print(f"Gemma root: {gemma_root}")
143
 
144
+
145
+ # ─────────────────────────────────────────────────────────────────────────────
146
+ # Text encoding: on-the-fly Gemma -> (video_context, audio_context) for each
147
+ # prompt. HDRICLoraPipeline expects a `.pt` path at __init__, so we bootstrap
148
+ # one from an empty prompt, then overwrite `pipeline.text_embeddings` in
149
+ # memory each generate call.
150
+ # ─────────────────────────────────────────────────────────────────────────────
151
+ _DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
152
+ _DTYPE = torch.bfloat16
153
+
154
+ prompt_encoder = PromptEncoder(
155
+ checkpoint_path=checkpoint_path,
156
  gemma_root=gemma_root,
157
+ dtype=_DTYPE,
158
+ device=_DEVICE,
159
  )
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
+ def encode_prompt_to_contexts(prompt: str) -> tuple[torch.Tensor, torch.Tensor]:
163
+ """Run Gemma + embeddings processor to produce (video_context, audio_context).
 
164
 
165
+ HDRICLoraPipeline only consumes video_context; audio_context is stored for
166
+ shape-compat with the `.pt` interface but ignored during HDR generation.
167
+ """
168
+ (out,) = prompt_encoder([prompt])
169
+ v = out.video_encoding
170
+ a = out.audio_encoding if out.audio_encoding is not None else torch.zeros(0, device=v.device, dtype=v.dtype)
171
+ return v, a
172
 
173
+
174
+ # Bootstrap a dummy embeddings file so HDRICLoraPipeline can construct.
175
+ print("Pre-computing bootstrap text embeddings...")
176
+ _bootstrap_v, _bootstrap_a = encode_prompt_to_contexts("")
177
+ _bootstrap_emb_path = Path(tempfile.gettempdir()) / "ltx_hdr_bootstrap_emb.pt"
178
+ torch.save({"video_context": _bootstrap_v, "audio_context": _bootstrap_a}, _bootstrap_emb_path)
179
+
180
+
181
+ # ─────────────────────────────────────────────────────────────────────────────
182
+ # Initialize pipeline
183
+ # ─────────────────────────────────────────────────────────────────────────────
184
+ # HDRICLoraPipeline is video-only (no audio path). HDR transform (LogC3) and
185
+ # reference_downscale_factor are auto-detected from the LoRA metadata.
186
+ pipeline = HDRICLoraPipeline(
187
+ distilled_checkpoint_path=checkpoint_path,
188
+ spatial_upsampler_path=spatial_upsampler_path,
189
+ hdr_lora=hdr_lora_path,
190
+ text_embeddings_path=str(_bootstrap_emb_path),
191
+ quantization=QuantizationPolicy.fp8_cast(),
192
+ offload_mode=OffloadMode.NONE,
193
+ )
194
+ print(f"HDRICLoraPipeline ready. HDR transform: {pipeline.hdr_transform}, "
195
+ f"ref_downscale={pipeline.reference_downscale_factor}")
196
+ print("=" * 80)
197
 
198
 
199
+ # ─────────────────────────────────────────────────────────────────────────────
200
+ # UI helpers
201
+ # ─────────────────────────────────────────────────────────────────────────────
202
+ def detect_aspect_ratio(video_path) -> str:
203
+ if video_path is None:
204
  return "16:9"
205
+ try:
206
+ meta = get_videostream_metadata(str(video_path))
207
+ ratio = meta.width / meta.height
208
+ except Exception:
 
209
  return "16:9"
210
+ candidates = {
211
+ "16:9": 16 / 9, "9:16": 9 / 16, "1:1": 1.0,
212
+ "4:3": 4 / 3, "3:4": 3 / 4, "21:9": 21 / 9,
213
+ }
214
  return min(candidates, key=lambda k: abs(ratio - candidates[k]))
215
 
216
 
217
+ def get_target_resolution(target_aspect: str, high_res: bool) -> tuple[int, int]:
 
 
218
  tier = "high" if high_res else "low"
219
+ return RESOLUTIONS[tier].get(target_aspect, RESOLUTIONS[tier]["16:9"])
 
220
 
221
 
222
+ def on_video_upload(video):
223
+ if video is None:
224
+ return gr.update(), gr.update(), gr.update()
225
+ try:
226
+ meta = get_videostream_metadata(str(video))
227
+ dur = round(min(meta.frames / meta.fps, 6.7), 1)
228
+ fps = float(meta.fps)
229
+ except Exception:
230
+ dur, fps = 3.0, 24.0
231
+ return gr.update(value=dur), gr.update(value=detect_aspect_ratio(video)), gr.update(value=fps)
232
+
233
+
234
+ # ─────────────────────────────────────────────────────────────────────────────
235
+ # Generation
236
+ # ─────────────────────────────────────────────────────────────────────────────
237
+ @spaces.GPU(duration=300)
238
  @torch.inference_mode()
239
  def generate_video(
240
+ input_video,
241
  prompt: str,
242
  duration: float,
243
+ frame_rate: float,
244
+ target_aspect: str,
245
+ high_res: bool,
246
+ seed: int,
247
+ randomize_seed: bool,
248
+ high_quality_hdr: bool,
249
+ export_exr: bool,
250
  progress=gr.Progress(track_tqdm=True),
251
  ):
252
+ current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
253
  try:
254
  torch.cuda.reset_peak_memory_stats()
 
255
 
256
+ if input_video is None:
257
+ raise ValueError("Please upload a source video.")
258
 
259
+ video_path = str(input_video)
260
+
261
+ # Frame count must satisfy (n-1) % 8 == 0
262
  num_frames = int(duration * frame_rate) + 1
263
  num_frames = ((num_frames - 1 + 7) // 8) * 8 + 1
264
 
265
+ target_w, target_h = get_target_resolution(target_aspect, high_res)
266
 
267
+ print(f"[HDR] {target_h}x{target_w}, frames={num_frames}, fps={frame_rate}, "
268
+ f"seed={current_seed}, aspect={target_aspect}, hq_hdr={high_quality_hdr}")
 
 
 
 
 
 
 
 
269
 
270
+ # Encode prompt -> (video_context, audio_context) and swap into the
271
+ # pipeline. Gemma is loaded, used, and freed inside prompt_encoder.
272
+ print(f"[HDR] Encoding prompt: {prompt!r}")
273
+ video_context, audio_context = encode_prompt_to_contexts(prompt or "")
274
+ pipeline.text_embeddings = (video_context, audio_context)
275
 
276
+ # Tiling config: smaller spatial tile on lower-VRAM targets
277
+ tiling_config = _make_tiling_config(spatial_tile=768 if not high_res else 1280)
278
 
279
+ hdr_video = pipeline(
 
280
  seed=current_seed,
281
+ height=int(target_h),
282
+ width=int(target_w),
283
  num_frames=num_frames,
284
+ frame_rate=float(frame_rate),
285
+ video_conditioning=[(video_path, 1.0)],
286
  tiling_config=tiling_config,
287
+ high_quality_hdr=high_quality_hdr,
288
  )
289
 
290
+ # hdr_video is [f, h, w, c] linear HDR float. Write EXR frames, then
291
+ # tonemap to a libx264 mp4 for in-browser preview.
292
+ work_dir = Path(tempfile.mkdtemp(prefix="ltx-hdr-"))
293
+ exr_dir = work_dir / "exr"
294
+ exr_dir.mkdir(parents=True, exist_ok=True)
295
+
296
+ for j in range(hdr_video.shape[0]):
297
+ save_exr_tensor(
298
+ hdr_video[j].detach().cpu(),
299
+ str(exr_dir / f"frame_{j:05d}.exr"),
300
+ half=False,
301
+ )
302
+ del hdr_video
303
+
304
+ preview_mp4 = work_dir / "preview.mp4"
305
+ encode_exr_sequence_to_mp4(exr_dir, preview_mp4, frame_rate=float(frame_rate))
306
 
307
+ exr_zip_path = None
308
+ if export_exr:
309
+ exr_zip_path = work_dir / "exr.zip"
310
+ with zipfile.ZipFile(exr_zip_path, "w", compression=zipfile.ZIP_STORED) as zf:
311
+ for exr in sorted(exr_dir.glob("frame_*.exr")):
312
+ zf.write(exr, arcname=exr.name)
 
 
313
 
314
+ return str(preview_mp4), (str(exr_zip_path) if exr_zip_path else None), current_seed
 
315
 
316
  except Exception as e:
317
  import traceback
318
+ print(f"Error: {e}\n{traceback.format_exc()}")
319
+ return None, None, current_seed
 
320
 
321
 
322
+ # ─────────────────────────────────────────────────────────────────────────────
323
+ # Gradio UI — LTX 2.3 HDR
324
+ # ─────────────────────────────────────────────────────────────────────────────
325
+ css = """
326
+ .generate-btn { min-height: 52px !important; font-size: 1.1em !important; }
327
+ footer { display: none !important; }
328
+ video { object-fit: contain !important; }
329
+ """
330
+
331
+ theme = gr.themes.Citrus(
332
+ primary_hue=gr.themes.colors.purple,
333
+ secondary_hue=gr.themes.colors.purple,
334
+ neutral_hue=gr.themes.colors.gray,
335
+ )
336
+
337
+ with gr.Blocks(title="LTX 2.3 HDR", css=css, theme=theme) as demo:
338
+ gr.Markdown("""
339
+ # LTX 2.3 HDR ✨
340
+ Video-to-video HDR via LTX-2.3 + [HDR IC-LoRA](https://huggingface.co/diffusers-internal-dev/LTX-HDR-LoRA).
341
+ Output is linear HDR (LogC3 inverse decoded — auto-detected from LoRA metadata). The preview mp4 is a fixed-EV sRGB tonemap; the EXR zip contains the full linear float frames for grading.
342
+ """)
343
 
344
  with gr.Row():
345
+ with gr.Column(scale=1):
346
+ input_video = gr.Video(label="Source Video")
347
+
348
  prompt = gr.Textbox(
349
  label="Prompt",
350
+ info="Describes the scene being regenerated in HDR. Encoded through Gemma on each run.",
351
+ lines=2,
352
+ placeholder="a cinematic sunset over mountains, high dynamic range, bright sky, deep shadows",
 
353
  )
 
 
 
 
 
 
354
 
355
+ with gr.Row():
356
+ target_aspect = gr.Dropdown(
357
+ label="Aspect Ratio",
358
+ choices=["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"],
359
+ value="16:9",
360
+ )
361
+ duration = gr.Slider(
362
+ label="Duration (s)", minimum=1.0, maximum=6.7, value=3.0, step=0.1,
363
+ )
364
+ frame_rate = gr.Number(label="FPS", value=24.0, precision=2)
365
+
366
+ generate_btn = gr.Button(
367
+ "Generate HDR", variant="primary", size="lg", elem_classes=["generate-btn"],
368
+ )
369
 
370
  with gr.Accordion("Advanced Settings", open=False):
371
+ high_res = gr.Checkbox(label="High Resolution (2×)", value=False)
372
+ high_quality_hdr = gr.Checkbox(
373
+ label="High-Quality HDR",
374
+ value=False,
375
+ info="Generates at 2× internal frame count, keeps every other frame. ~2× slower, smoother temporally.",
376
+ )
377
+ export_exr = gr.Checkbox(
378
+ label="Export EXR frames (zip)",
379
+ value=True,
380
+ info="Per-frame linear OpenEXR (float32). Uncheck if you only want the mp4 preview.",
381
+ )
382
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, value=10, step=1)
383
  randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
+ with gr.Column(scale=1):
386
+ output_video = gr.Video(label="Preview (sRGB tonemap)", autoplay=True, height=480)
387
+ output_exr = gr.File(label="EXR frames (zip)")
388
+
389
+ input_video.change(
390
+ fn=on_video_upload,
391
+ inputs=[input_video],
392
+ outputs=[duration, target_aspect, frame_rate],
393
  )
394
 
395
  generate_btn.click(
396
  fn=generate_video,
397
  inputs=[
398
+ input_video, prompt, duration, frame_rate, target_aspect, high_res,
399
+ seed, randomize_seed, high_quality_hdr, export_exr,
400
  ],
401
+ outputs=[output_video, output_exr, seed],
402
  )
403
 
404
 
 
 
 
 
 
405
  if __name__ == "__main__":
406
+ demo.launch()
 
ltx-2-internal/LICENSE ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ LTX-2 Community License Agreement
2
+ License date: January 5, 2026
3
+
4
+
5
+ By using or distributing any portion or element of LTX-2, you agree
6
+ to be bound by this Agreement.
7
+
8
+ 1. Definitions.
9
+
10
+ "Agreement" means the terms and conditions for the license, use,
11
+ reproduction, and distribution of LTX-2 and the Complementary
12
+ Materials, as specified in this document.
13
+
14
+ "Control" means the direct or indirect ownership of more than
15
+ fifty percent (50%) of the voting securities or other ownership
16
+ interests, or the power to direct the management and policies of
17
+ such Entity through voting rights, contract, or otherwise.
18
+
19
+ "Data" means a collection of information and/or content extracted
20
+ from the dataset used with LTX-2, including to train, pretrain,
21
+ or otherwise evaluate LTX-2. The Data is not licensed under this
22
+ Agreement.
23
+
24
+ "Derivatives of LTX-2" means all modifications to LTX-2, works
25
+ based on LTX-2, or any other model which is created or initialized
26
+ by transfer of patterns of the weights, parameters, activations or
27
+ output of LTX-2, to the other model, in order to cause the other
28
+ model to perform similarly to LTX-2, including – but not limited
29
+ to - distillation methods entailing the use of intermediate data
30
+ representations or methods based on the generation of synthetic
31
+ data by LTX-2 for training the other model. For clarity, Derivatives
32
+ of LTX-2 include: (i) any fine-tuned or adapted weights, parameters,
33
+ or checkpoints derived from LTX-2; (ii) derivative model architectures
34
+ that incorporate or are based upon LTX-2's architecture; and
35
+ (iii) any modified or extended versions of the Complementary
36
+ Materials. All intellectual property rights in Derivatives of LTX-2
37
+ shall be subject to the terms of this Agreement, and you may not
38
+ claim exclusive ownership rights in any Derivatives of LTX-2 that
39
+ would restrict the rights granted herein.
40
+
41
+ "Entity" means any individual, corporation, partnership, limited
42
+ liability company, or other legal entity. For purposes of this
43
+ Agreement, an Entity shall be deemed to include, on an aggregative
44
+ basis, all subsidiaries, affiliates, and other companies under
45
+ common Control with such Entity. When determining whether an Entity
46
+ meets any threshold under this Agreement (including revenue
47
+ thresholds), all subsidiaries, affiliates, and companies under
48
+ common Control shall be considered collectively.
49
+
50
+ "Harm" includes but is not limited to physical, mental,
51
+ psychological, financial and reputational damage, pain, or loss.
52
+
53
+ "Licensor" or "Lightricks" means the owner that is granting the
54
+ license under this Agreement. For the purposes of this Agreement,
55
+ the Licensor is Lightricks Ltd.
56
+
57
+ "LTX-2" means the large language models, text/image/video/audio/3D
58
+ generation models, and multimodal large language models and their
59
+ software and algorithms, including trained model weights, parameters
60
+ (including optimizer states), machine-learning model code,
61
+ inference-enabling code, training-enabling code, fine-tuning
62
+ enabling code, accompanying source code, scripts, documentation,
63
+ tutorials, examples, and all other elements of the foregoing
64
+ distributed and made publicly available by Lightricks (including,
65
+ for example, at https://github.com/Lightricks/LTX-2) for the LTX-2
66
+ model released on January 5, 2026. This license is applicable to
67
+ all LTX-2 versions released since January 5, 2026, and all future
68
+ releases of LTX-2 under this license.
69
+
70
+ "Output" means the results of operating LTX-2 as embodied in
71
+ informational content resulting therefrom.
72
+
73
+ "you" (or "your") means an individual or legal Entity licensing
74
+ LTX-2 in accordance with this Agreement and/or making use of LTX-2
75
+ for whichever purpose and in any field of use, including usage of
76
+ LTX-2 in an end-use application - e.g. chatbot, translator, image
77
+ generator.
78
+
79
+ 2. Grant of License. Subject to the terms and conditions of this
80
+ Agreement, you are granted a non-exclusive, worldwide,
81
+ non-transferable and royalty-free limited license under Licensor's
82
+ intellectual property or other rights owned by Licensor embodied
83
+ in LTX-2 to use, reproduce, prepare, distribute, publicly display,
84
+ publicly perform, sublicense, copy, create derivative works of,
85
+ and make modifications to LTX-2, for any purpose, subject to the
86
+ restrictions set forth in Attachment A; provided however, that
87
+ Entities with annual revenues of at least $10,000,000 (the
88
+ "Commercial Entities") are required to obtain a paid commercial
89
+ use license in order to use LTX-2 and Derivatives of LTX-2,
90
+ subject to the terms and provisions of a different license (the
91
+ "Commercial Use Agreement"), as will be provided by the Licensor.
92
+ Commercial Entities interested in such a commercial license are
93
+ required to [contact Licensor](https://ltx.io/model/licensing).
94
+ Any commercial use of LTX-2 or Derivatives of LTX-2 by the
95
+ Commercial Entities not in accordance with this Agreement and/or
96
+ the Commercial Use Agreement is strictly prohibited and shall be
97
+ deemed a material breach of this Agreement. Such material breach
98
+ will be subject, in addition to any license fees owed to Licensor
99
+ for the period such Commercial Entity used LTX-2 (as will be
100
+ determined by Licensor), to liquidated damages, which will be paid
101
+ to Licensor immediately upon demand, in an amount equal to double
102
+ the amount that would otherwise have been paid by you for the
103
+ relevant period of time. Such amount reflects a reasonable estimation
104
+ of the losses and administrative costs incurred due to such breach.
105
+ You agree and understand that this remedy does not limit the Licensor's
106
+ right to pursue other remedies available at law or equity.
107
+
108
+ 3. Distribution and Redistribution. You may host for third parties
109
+ remote access purposes (e.g. software-as-a-service), reproduce
110
+ and distribute copies of LTX-2 or Derivatives of LTX-2 thereof in
111
+ any medium, with or without modifications, provided that you meet
112
+ the following conditions:
113
+
114
+ (a) Use-based restrictions as referenced in paragraph 4 and all
115
+ provisions of Attachment A MUST be included as an enforceable
116
+ provision by you in any type of legal agreement (e.g. a
117
+ license) governing the use and/or distribution of LTX-2 or
118
+ Derivatives of LTX-2, and you shall give notice to subsequent
119
+ users you distribute to, that LTX-2 or Derivatives of LTX-2
120
+ are subject to paragraph 4 and Attachment A in their entirety,
121
+ including all use restrictions and acceptable use policies;
122
+
123
+ (b) You must provide any third party recipients of LTX-2 or
124
+ Derivatives of LTX-2 a copy of this Agreement, including all
125
+ attachments and use policies. Any Derivative of LTX-2 (as
126
+ defined in Section 1, including but not limited to fine-tuned
127
+ weights, modified training code, models trained on Outputs, or
128
+ any other derivative) must be distributed exclusively under
129
+ the terms of this Agreement with a complete copy of this
130
+ license included;
131
+
132
+ (c) You must cause any modified files to carry prominent notices
133
+ stating that you changed the files;
134
+
135
+ (d) You must retain all copyright, patent, trademark, and
136
+ attribution notices excluding those notices that do not
137
+ pertain to any part of LTX-2, Derivatives of LTX-2.
138
+
139
+ You may add your own copyright statement to your modifications and
140
+ may provide additional or different license terms and conditions -
141
+ respecting paragraph 3(a) - for use, reproduction, or distribution
142
+ of your modifications, or for any such Derivatives of LTX-2 as a
143
+ whole, provided your use, reproduction, and distribution of LTX-2
144
+ otherwise complies with the conditions stated in this Agreement,
145
+ and you provide a complete copy of this Agreement with any such
146
+ use, reproduction and distribution of LTX-2 and any Derivatives
147
+ thereof.
148
+
149
+ 4. Use-based restrictions. The restrictions set forth in Attachment A
150
+ are considered Use-based restrictions. Therefore, you cannot use
151
+ LTX-2 and the Derivatives of LTX-2 in violation of the specified
152
+ restricted uses. You may use LTX-2 subject to this Agreement,
153
+ including only for lawful purposes and in accordance with the
154
+ Agreement. "Use" may include creating any content with, fine-tuning,
155
+ updating, running, training, evaluating and/or re-parametrizing
156
+ LTX-2. You shall require all of your users who use LTX-2 or a
157
+ Derivative of LTX-2 to comply with the terms of this paragraph 4.
158
+
159
+ 5. The Output You Generate. Except as set forth herein, Licensor
160
+ claims no rights in the Output you generate using LTX-2. You are
161
+ accountable for input you insert into LTX-2, the Output you
162
+ generate and its subsequent uses. No use of the Output can
163
+ contravene any provision as stated in the Agreement.
164
+
165
+ 6. Updates and Runtime Restrictions. To the maximum extent permitted
166
+ by law, Licensor reserves the right to restrict (remotely or
167
+ otherwise) usage of LTX-2 in violation of this Agreement, update
168
+ LTX-2 through electronic means, or modify the Output of LTX-2
169
+ based on updates. You shall undertake reasonable efforts to use
170
+ the latest version of LTX-2. Any use of the non-current version
171
+ of LTX-2 is done solely at your risk.
172
+
173
+ 7. Export Controls and Sanctions Compliance. You acknowledge that
174
+ LTX-2, Derivatives of LTX-2 may be subject to export control laws
175
+ and regulations, including but not limited to the U.S. Export
176
+ Administration Regulations and sanctions programs administered by
177
+ the Office of Foreign Assets Control (OFAC). You represent and
178
+ warrant that you and any users of LTX-2 are not (i) located in,
179
+ organized under the laws of, or ordinarily resident in any country
180
+ or territory subject to comprehensive sanctions; (ii) identified
181
+ on any U.S. government restricted party list, including the
182
+ Specially Designated Nationals and Blocked Persons List; or
183
+ (iii) otherwise prohibited from receiving LTX-2 under applicable
184
+ law. You shall not export, re-export, or transfer LTX-2, directly
185
+ or indirectly, in violation of any applicable export control or
186
+ sanctions laws or regulations. You agree to comply with all
187
+ applicable trade control laws and shall indemnify and hold
188
+ Licensor harmless from any claims arising from your failure to
189
+ comply with such laws.
190
+
191
+ 8. Trademarks and related. Nothing in this Agreement permits you to
192
+ make use of Licensor's trademarks, trade names, logos or to
193
+ otherwise suggest endorsement or misrepresent the relationship
194
+ between the parties; and any rights not expressly granted herein
195
+ are reserved by the Licensor.
196
+
197
+ 9. Disclaimer of Warranty. Unless required by applicable law or
198
+ agreed to in writing, Licensor provides LTX-2 on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
200
+ implied, including, without limitation, any warranties or
201
+ conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS
202
+ FOR A PARTICULAR PURPOSE. You are solely responsible for
203
+ determining the appropriateness of using or redistributing LTX-2
204
+ and Derivatives of LTX-2 and assume any risks associated with
205
+ your exercise of permissions under this Agreement.
206
+
207
+ 10. Limitation of Liability. In no event and under no legal theory,
208
+ whether in tort (including negligence), contract, or otherwise,
209
+ unless required by applicable law (such as deliberate and grossly
210
+ negligent acts) or agreed to in writing, shall Licensor be liable
211
+ to you for damages, including any direct, indirect, special,
212
+ incidental, or consequential damages of any character arising as
213
+ a result of this Agreement or out of the use or inability to use
214
+ LTX-2 (including but not limited to damages for loss of goodwill,
215
+ work stoppage, computer failure or malfunction, or any and all
216
+ other commercial damages or losses), even if Licensor has been
217
+ advised of the possibility of such damages.
218
+
219
+ 11. Accepting Warranty or Additional Liability. While redistributing
220
+ LTX-2 and Derivatives of LTX-2, you may, provided you do not
221
+ violate the terms of this Agreement, choose to offer and charge
222
+ a fee for, acceptance of support, warranty, indemnity, or other
223
+ liability obligations. However, in accepting such obligations,
224
+ you may act only on your own behalf and on your sole
225
+ responsibility, not on behalf of Licensor, and only if you agree
226
+ to indemnify, defend, and hold Licensor harmless for any liability
227
+ incurred by, or claims asserted against Licensor, by reason of
228
+ your accepting any such warranty or additional liability.
229
+
230
+ 12. Governing Law. This Agreement and all relations, disputes, claims
231
+ and other matters arising hereunder (including non-contractual
232
+ disputes or claims) will be governed exclusively by, and construed
233
+ exclusively in accordance with, the laws of the State of New York.
234
+ To the extent permitted by law, choice of laws rules and the
235
+ United Nations Convention on Contracts for the International Sale
236
+ of Goods will not apply. For the purposes of adjudicating any
237
+ action or proceeding to enforce the terms of this Agreement, you
238
+ hereby irrevocably consent to the exclusive jurisdiction of, and
239
+ venue in, the federal and state courts located in the County of
240
+ New York within the State of New York. The prevailing party in
241
+ any claim or dispute between the parties under this Agreement
242
+ will be entitled to reimbursement of its reasonable attorneys'
243
+ fees and costs. You hereby waive the right to a trial by jury,
244
+ to participate in a class or representative action (including in
245
+ arbitration), or to combine individual proceedings in court or
246
+ in arbitration without the consent of all parties.
247
+
248
+ 13. Term and Termination. This Agreement is effective upon your
249
+ acceptance and continues until terminated. Licensor may terminate
250
+ this Agreement immediately upon written notice to you if you
251
+ breach any provision of this Agreement, including but not limited
252
+ to violations of the use restrictions in Attachment A or
253
+ unauthorized commercial use. Upon termination: (a) all rights
254
+ granted to you under this Agreement will immediately cease;
255
+ (b) you must immediately cease all use of LTX-2 and Derivatives
256
+ of LTX-2; (c) you must delete or destroy all copies of LTX-2
257
+ and Derivatives of LTX-2 in your possession or control; and
258
+ (d) you must notify any third parties to whom you distributed
259
+ LTX-2 or Derivatives of LTX-2 of the termination. Sections 8-13,
260
+ and Section 15 shall survive termination of this Agreement.
261
+ Termination does not relieve you of any obligations incurred
262
+ prior to termination, including payment obligations under
263
+ Section 2. In addition, if You commence a lawsuit or other
264
+ proceedings (including a cross-claim or counterclaim in a lawsuit)
265
+ against Licensor or any person or entity alleging that LTX-2 or
266
+ any Output, or any portion of any of the foregoing, infringe any
267
+ intellectual property or other right owned or licensable by you,
268
+ then all licenses granted to you under this Agreement shall
269
+ terminate as of the date such lawsuit or other proceeding is filed.
270
+
271
+ 14. Disputes and Arbitration. All disputes arising in connection with
272
+ this Agreement shall be finally settled by arbitration under the
273
+ Rules of Arbitration of the International Chamber of Commerce
274
+ ("ICC Rules"), by one (1) arbitrator appointed in accordance with
275
+ the ICC Rules. The seat of arbitration shall be New York, NY, USA,
276
+ and the proceedings shall be conducted in English. The arbitrator
277
+ shall be empowered to grant any relief that a court could grant.
278
+ Judgment on the arbitration award may be entered by any court
279
+ having jurisdiction thereof. Each party waives its right to a
280
+ trial by jury and to participate in any class or representative
281
+ action.
282
+
283
+ 15. If any provision of this Agreement is held to be
284
+ invalid, illegal
285
+ or unenforceable, the remaining provisions shall be unaffected
286
+ thereby and remain valid as if such provision had not been set
287
+ forth herein.
288
+
289
+ END OF TERMS AND CONDITIONS
290
+
291
+ ATTACHMENT A: Use Restrictions
292
+
293
+ When using the Outputs, LTX-2 and any Derivatives thereof, you
294
+ will comply with the Acceptable Use Policy. In addition, you
295
+ agree not to use the Outputs, LTX-2 or its Derivatives in any
296
+ of the following ways:
297
+
298
+ 1. In any way that violates any applicable national, federal,
299
+ state, local or international law or regulation;
300
+
301
+ 2. For the purpose of exploiting, Harming or attempting to
302
+ exploit or Harm minors in any way;
303
+
304
+ 3. To generate or disseminate false information and/or content
305
+ with the purpose of Harming others;
306
+
307
+ 4. To generate or disseminate personal identifiable information
308
+ that can be used to Harm an individual;
309
+
310
+ 5. To generate or disseminate information and/or content (e.g.
311
+ images, code, posts, articles), and place the information
312
+ and/or content in any context (e.g. bot generating tweets)
313
+ without expressly and intelligibly disclaiming that the
314
+ information and/or content is machine generated;
315
+
316
+ 6. To defame, disparage or otherwise harass others;
317
+
318
+ 7. To impersonate or attempt to impersonate (e.g. deepfakes)
319
+ others without their consent;
320
+
321
+ 8. For fully automated decision making that adversely impacts an
322
+ individual's legal rights or otherwise creates or modifies a
323
+ binding, enforceable obligation;
324
+
325
+ 9. For any use intended to or which has the effect of
326
+ discriminating against or Harming individuals or groups based
327
+ on online or offline social behavior or known or predicted
328
+ personal or personality characteristics;
329
+
330
+ 10. To exploit any of the vulnerabilities of a specific group of
331
+ persons based on their age, social, physical or mental
332
+ characteristics, in order to materially distort the behavior
333
+ of a person pertaining to that group in a manner that causes
334
+ or is likely to cause that person or another person physical
335
+ or psychological Harm;
336
+
337
+ 11. For any use intended to or which has the effect of
338
+ discriminating against individuals or groups based on legally
339
+ protected characteristics or categories;
340
+
341
+ 12. To provide medical advice and medical results interpretation;
342
+
343
+ 13. To generate or disseminate information for the purpose to be
344
+ used for administration of justice, law enforcement,
345
+ immigration or asylum processes, such as predicting an
346
+ individual will commit fraud/crime commitment (e.g. by text
347
+ profiling, drawing causal relationships between assertions
348
+ made in documents, indiscriminate and arbitrarily-targeted use);
349
+
350
+ 14. To generate and/or disseminate malware (including – but not
351
+ limited to – ransomware) or any other content to be used for
352
+ the purpose of harming electronic systems;
353
+
354
+ 15. To engage in, promote, incite, or facilitate discrimination
355
+ or other unlawful or harmful conduct in the provision of
356
+ employment, employment benefits, credit, housing, or other
357
+ essential goods and services;
358
+
359
+ 16. To engage in, promote, incite, or facilitate the harassment,
360
+ abuse, threatening, or bullying of individuals or groups of
361
+ individuals;
362
+
363
+ 17. For military, warfare, nuclear industries or applications,
364
+ weapons development, or any use in connection with activities
365
+ that may cause death, personal injury, or severe physical or
366
+ environmental damage;
367
+
368
+ 18. For commercial use only: To train, improve, or fine-tune any
369
+ other machine learning model, artificial intelligence system,
370
+ or competing model, except for Derivatives of LTX-2 as
371
+ expressly permitted under this Agreement;
372
+
373
+ 19. To circumvent, disable, or interfere with any technical
374
+ limitations, safety features, content filters, or use
375
+ restrictions implemented in LTX-2 by Licensor;
376
+
377
+ 20. To use LTX-2 or Derivatives of LTX-2 in any product, service,
378
+ or application that directly competes with Licensor's
379
+ commercial products or services, or is designed to replace or
380
+ substitute Licensor's offerings in the market, without
381
+ obtaining a separate commercial license from Licensor.
ltx-2-internal/README.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LTX-2
2
+
3
+
4
+ https://github.com/user-attachments/assets/1922e7f0-cc5d-430c-9e1a-93c589e85fb4
5
+
6
+
7
+ ## Setup
8
+
9
+ To set up ltx-2 repository for development:
10
+ * `uv sync` creates and installs the virtual environment. To activate the environment, run `source .venv/bin/activate`.
11
+ * Set up pre-commit:
12
+ ```bash
13
+ $ PIP_NO_INPUT=1 pre-commit install-hooks --config .pre-commit-config.yaml
14
+ $ pre-commit install
15
+ ```
16
+ * `pytest` runs all tests in the repository. `pytest --pyargs packages/ltx-core` runs only tests specific to `ltx-core`.
17
+ * `uv build --package ltx-core` builds only the ltx-core package.
18
+ * Dependencies declared in the main `pyproject.toml` file apply to the entire repository. Always prefer specifying dependencies for specific packages.
19
+
20
+ ## Folder structure
21
+
22
+ ```
23
+ ├── README.md
24
+ ├── packages
25
+ │ └── ltx-core
26
+ │ ├── README.md
27
+ │ ├── pyproject.toml
28
+ │ ├── src
29
+ │ │ └── ltx_core
30
+ │ │ ├── __init__.py
31
+ │ │ ├── # package-specific modules go here
32
+ │ └── tests
33
+ │ └── ltx_core
34
+ │ ├── # test folder structure mirrors src of the package.
35
+ │ ├── conftest.py
36
+ │ └── unit
37
+ │ └── test_placeholder.py
38
+ ├── pyproject.toml
39
+ ├── pytest.toml
40
+ └── uv.lock
41
+ ```
42
+
43
+
44
+ ## Comments mentioning ComfyUI or internal company code
45
+
46
+ Comments relating to ComfyUI, its LTX repo or any other internal
47
+ Lightricks resource should be wrapped in LT_INTERNAL tags. Tagged
48
+ sections should be placed inside comments or docstrings. The
49
+ following examples demonstrate proper usage:
50
+ ```python
51
+ # LT_INTERNAL: check PR to understand better
52
+
53
+ # conditioning is applied before the step LT_INTERNAL: http://...
54
+
55
+ # condition is applied after the step to integrate LT_INTERNAL_BEGIN http://... LT_INTERNAL_END
56
+
57
+ """
58
+ Consult with the paper, https://arxiv.org
59
+ LT_INTERNAL_BEGIN
60
+ Most important PRs to check
61
+ http://...
62
+ http://...
63
+ LT_INTERNAL_END
64
+ """
65
+
66
+ ```
ltx-2-internal/README.public.md ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LTX-2
2
+
3
+ [![Website](https://img.shields.io/badge/Website-LTX-181717?logo=google-chrome)](https://ltx.io)
4
+ [![Model](https://img.shields.io/badge/HuggingFace-Model-orange?logo=huggingface)](https://huggingface.co/Lightricks/LTX-2.3)
5
+ [![Demo](https://img.shields.io/badge/Demo-Try%20Now-brightgreen?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAFKADAAQAAAABAAAAFAAAAACy3fD9AAACmElEQVQ4Ea1VP2haYRA/fRo0mESRIIqb2IwxuNUl0CGFQBC6OAWcikMottCpqYtDQIgdQsBFhAjZqiQhbhmySJBOgmNU0EGCg9r61Bivd0ffoykE0iQH37/77n7f3e/uqQFIPB7P/N3d3QeDwfAFEedZ91ghnyH5JM1m87dWq6UavF6vdTKZfDcajW/p4rE49+wIFMj33Gq1vlNo+kxg758KpiETqP/29vaXweVyqaS0aBfPXEfGFwTjWCwM+KBQoWA4HAJx/KDNvxcmTTGbzYAH8SljOp2C2+2GjY0NqNfrcHFxAXNzc2LDfCuKIq78KBdFOwsgGzidTnA4HHBzcwO9Xg8sFgtsbm7C3t4eVCoVaDQa0O12YXl5GUwmk5z5cZ/PB6PRCNrttgADFQUXFhbw8PAQVVXF3d1dJAeMx+P0zn0Jh8OYz+eRADCRSGAqlcLxeIz7+/u4tLSEjKUDZrNZ8U4mk0jR4fr6Op6enoru+voa0+k0rq2tYTAYxE6ng9QiSLRgrVZDv9+PFLkA6kUhT+GEC8C8XF5ewtHRkejICShiaDabwPvj42NJm3k7ODiQdDl9Fr0ocqJpdXUVIpEIdz7Y7XZRr6ysQDQahXK5LORvbW1p5rC9vQ2UifAooBqHuVxO0vt72tnZwWq1qqtisRgWCgU5ZzIZPDk50fdUUEmZvxTmAgKBgAxunT/fJpRKJWmhUCgEVDi4uroSG46kWCzC4uKitNVgMICzszOhSgA5fiJZhp4Lbbh1KARpbF65D/lx3vMdP05Vlkf5zKIDyukFJi7N6AVwNAhVsdlsM+LsjaZ56sq8kyQUqs4P6rsAKV49B4x4Padf7Y9Kv9+fEmiBQH8S4Gsa5v8EHpL9VwL7xH8BvwEcd4ccVf02KQAAAABJRU5ErkJggg==)](https://console.ltx.video/playground)
6
+ [![Paper](https://img.shields.io/badge/Paper-PDF-EC1C24?logo=adobeacrobatreader&logoColor=white)](https://arxiv.org/abs/2601.03233)
7
+ [![Discord](https://img.shields.io/badge/Join-Discord-5865F2?logo=discord)](https://discord.gg/ltxplatform)
8
+
9
+ **LTX-2** is the first DiT-based audio-video foundation model that contains all core capabilities of modern video generation in one model: synchronized audio and video, high fidelity, multiple performance modes, production-ready outputs, API access, and open access.
10
+
11
+ <div align="center">
12
+ <video src="https://github.com/user-attachments/assets/4414adc0-086c-43de-b367-9362eeb20228" width="70%" poster=""> </video>
13
+ </div>
14
+
15
+ ## 🚀 Quick Start
16
+
17
+ ```bash
18
+ # Clone the repository
19
+ git clone https://github.com/Lightricks/LTX-2.git
20
+ cd LTX-2
21
+
22
+ # Set up the environment
23
+ uv sync --frozen
24
+ source .venv/bin/activate
25
+ ```
26
+
27
+ ### Required Models
28
+
29
+ Download the following models from the [LTX-2.3 HuggingFace repository](https://huggingface.co/Lightricks/LTX-2.3):
30
+
31
+ **LTX-2.3 Model Checkpoint** (choose and download one of the following)
32
+ * [`ltx-2.3-22b-dev.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-22b-dev.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-dev.safetensors)
33
+ * [`ltx-2.3-22b-distilled-1.1.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-22b-distilled-1.1.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-1.1.safetensors)
34
+
35
+ **Spatial Upscaler** - Required for current two-stage pipeline implementations in this repository
36
+ * [`ltx-2.3-spatial-upscaler-x2-1.1.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors)
37
+ * [`ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors)
38
+
39
+ **Temporal Upscaler** - Supported by the model and will be required for future pipeline implementations
40
+ * [`ltx-2.3-temporal-upscaler-x2-1.0.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-temporal-upscaler-x2-1.0.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-temporal-upscaler-x2-1.0.safetensors)
41
+
42
+ **Distilled LoRA** - Required for current two-stage pipeline implementations in this repository (except DistilledPipeline and ICLoraPipeline)
43
+ * [`ltx-2.3-22b-distilled-lora-384-1.1.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-22b-distilled-lora-384-1.1.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-lora-384-1.1.safetensors)
44
+
45
+ **Gemma Text Encoder** (download all assets from the repository)
46
+ * [`Gemma 3`](https://huggingface.co/google/gemma-3-12b-it-qat-q4_0-unquantized/tree/main)
47
+
48
+ **LoRAs**
49
+ * [`LTX-2.3-22b-IC-LoRA-Union-Control`](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control) - [Download](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control/resolve/main/ltx-2.3-22b-ic-lora-union-control-ref0.5.safetensors)
50
+ * [`LTX-2.3-22b-IC-LoRA-Motion-Track-Control`](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control) - [Download](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control/resolve/main/ltx-2.3-22b-ic-lora-motion-track-control-ref0.5.safetensors)
51
+ * [`LTX-2-19b-IC-LoRA-Detailer`](https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer/resolve/main/ltx-2-19b-ic-lora-detailer.safetensors)
52
+ * [`LTX-2-19b-IC-LoRA-Pose-Control`](https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control/resolve/main/ltx-2-19b-ic-lora-pose-control.safetensors)
53
+ * [`LTX-2-19b-LoRA-Camera-Control-Dolly-In`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In/resolve/main/ltx-2-19b-lora-camera-control-dolly-in.safetensors)
54
+ * [`LTX-2-19b-LoRA-Camera-Control-Dolly-Left`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left/resolve/main/ltx-2-19b-lora-camera-control-dolly-left.safetensors)
55
+ * [`LTX-2-19b-LoRA-Camera-Control-Dolly-Out`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out/resolve/main/ltx-2-19b-lora-camera-control-dolly-out.safetensors)
56
+ * [`LTX-2-19b-LoRA-Camera-Control-Dolly-Right`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right/resolve/main/ltx-2-19b-lora-camera-control-dolly-right.safetensors)
57
+ * [`LTX-2-19b-LoRA-Camera-Control-Jib-Down`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down/resolve/main/ltx-2-19b-lora-camera-control-jib-down.safetensors)
58
+ * [`LTX-2-19b-LoRA-Camera-Control-Jib-Up`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up/resolve/main/ltx-2-19b-lora-camera-control-jib-up.safetensors)
59
+ * [`LTX-2-19b-LoRA-Camera-Control-Static`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static/resolve/main/ltx-2-19b-lora-camera-control-static.safetensors)
60
+ * [`LTX-2.3-22b-IC-LoRA-HDR`](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-HDR) - HDR IC-LoRA and pre-computed text embeddings for `HDRICLoraPipeline`
61
+
62
+ ### Available Pipelines
63
+
64
+ * **[TI2VidTwoStagesPipeline](packages/ltx-pipelines/src/ltx_pipelines/ti2vid_two_stages.py)** - Production-quality text/image-to-video with 2x upsampling (recommended)
65
+ * **[TI2VidTwoStagesHQPipeline](packages/ltx-pipelines/src/ltx_pipelines/ti2vid_two_stages_hq.py)** - Same two-stage flow as above but uses the res_2s second-order sampler (fewer steps, better quality)
66
+ * **[TI2VidOneStagePipeline](packages/ltx-pipelines/src/ltx_pipelines/ti2vid_one_stage.py)** - Single-stage generation for quick prototyping
67
+ * **[DistilledPipeline](packages/ltx-pipelines/src/ltx_pipelines/distilled.py)** - Fastest inference with 8 predefined sigmas
68
+ * **[ICLoraPipeline](packages/ltx-pipelines/src/ltx_pipelines/ic_lora.py)** - Video-to-video and image-to-video transformations (uses distilled model.)
69
+ * **[KeyframeInterpolationPipeline](packages/ltx-pipelines/src/ltx_pipelines/keyframe_interpolation.py)** - Interpolate between keyframe images
70
+ * **[A2VidPipelineTwoStage](packages/ltx-pipelines/src/ltx_pipelines/a2vid_two_stage.py)** - Audio-to-video generation conditioned on an input audio file
71
+ * **[RetakePipeline](packages/ltx-pipelines/src/ltx_pipelines/retake.py)** - Regenerate a specific time region of an existing video
72
+ * **[HDRICLoraPipeline](packages/ltx-pipelines/src/ltx_pipelines/hdr_ic_lora.py)** - Video-to-video with HDR output (linear float frames via LogC3 inverse decode, suitable for EXR export and tonemapping)
73
+
74
+ ### ⚡ Optimization Tips
75
+
76
+ * **Use DistilledPipeline** - Fastest inference with only 8 predefined sigmas (8 steps stage 1, 4 steps stage 2)
77
+ * **Enable FP8 quantization** - Enables lower memory footprint: `--quantization fp8-cast` (CLI) or `quantization=QuantizationPolicy.fp8_cast()` (Python). Fp8-cast should be used with bf16 checkpoints, it shall downcast them on the fly. For Hopper GPUs with TensorRT-LLM, use `--quantization fp8-scaled-mm` for FP8 scaled matrix multiplication. Fp8-scaled-mm should be used with fp8 checkpoints.
78
+ * **Install attention optimizations** - Use xFormers (`uv sync --extra xformers`) or [Flash Attention 3](https://github.com/Dao-AILab/flash-attention) for Hopper GPUs
79
+ * **Use gradient estimation** - Reduce inference steps from 40 to 20-30 while maintaining quality (see [pipeline documentation](packages/ltx-pipelines/README.md#denoising-loop-optimization))
80
+ * **Skip memory cleanup** - If you have sufficient VRAM, disable automatic memory cleanup between stages for faster processing
81
+ * **Choose single-stage pipeline** - Use `TI2VidOneStagePipeline` for faster generation when high resolution isn't required
82
+
83
+ ## ✍️ Prompting for LTX-2
84
+
85
+ When writing prompts, focus on detailed, chronological descriptions of actions and scenes. Include specific movements, appearances, camera angles, and environmental details - all in a single flowing paragraph. Start directly with the action, and keep descriptions literal and precise. Think like a cinematographer describing a shot list. Keep within 200 words. For best results, build your prompts using this structure:
86
+
87
+ - Start with main action in a single sentence
88
+ - Add specific details about movements and gestures
89
+ - Describe character/object appearances precisely
90
+ - Include background and environment details
91
+ - Specify camera angles and movements
92
+ - Describe lighting and colors
93
+ - Note any changes or sudden events
94
+
95
+ For additional guidance on writing a prompt please refer to <https://ltx.video/blog/how-to-prompt-for-ltx-2>
96
+
97
+ ### Automatic Prompt Enhancement
98
+
99
+ LTX-2 pipelines support automatic prompt enhancement via an `enhance_prompt` parameter.
100
+
101
+ ## 🔌 ComfyUI Integration
102
+
103
+ To use our model with ComfyUI, please follow the instructions at <https://github.com/Lightricks/ComfyUI-LTXVideo/>.
104
+
105
+ ## 📦 Packages
106
+
107
+ This repository is organized as a monorepo with three main packages:
108
+
109
+ * **[ltx-core](packages/ltx-core/)** - Core model implementation, inference stack, and utilities
110
+ * **[ltx-pipelines](packages/ltx-pipelines/)** - High-level pipeline implementations for text-to-video, image-to-video, and other generation modes
111
+ * **[ltx-trainer](packages/ltx-trainer/)** - Training and fine-tuning tools for LoRA, full fine-tuning, and IC-LoRA
112
+
113
+ Each package has its own README and documentation. See the [Documentation](#-documentation) section below.
114
+
115
+ ## 📚 Documentation
116
+
117
+ Each package includes comprehensive documentation:
118
+
119
+ * **[LTX-Core README](packages/ltx-core/README.md)** - Core model implementation, inference stack, and utilities
120
+ * **[LTX-Pipelines README](packages/ltx-pipelines/README.md)** - High-level pipeline implementations and usage guides
121
+ * **[LTX-Trainer README](packages/ltx-trainer/README.md)** - Training and fine-tuning documentation with detailed guides
ltx-2-internal/conftest.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import gc
4
+ import os
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Callable
7
+
8
+ import av
9
+ import pytest
10
+ import torch
11
+ import torch.nn.functional as F
12
+ from torch._prims_common import DeviceLikeType
13
+
14
+ if TYPE_CHECKING:
15
+ from ltx_core.guidance.perturbations import BatchedPerturbationConfig
16
+ from ltx_core.model.transformer import Modality
17
+
18
+ torch.use_deterministic_algorithms(True)
19
+ os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
20
+
21
+ MODELS_PATH = Path(os.getenv("MODELS_PATH", "/models"))
22
+ CHECKPOINTS_DIR = MODELS_PATH / "comfyui_models" / "checkpoints"
23
+ LORAS_DIR = MODELS_PATH / "comfyui_models" / "loras"
24
+
25
+ GEMMA_ROOT = MODELS_PATH / "comfyui_models" / "text_encoders" / "gemma-3-12b-it-qat-q4_0-unquantized_readout_proj"
26
+ GEMMA_FLATTENED_ROOT = MODELS_PATH / "comfyui_models" / "text_encoders" / "gemma-3-12b-it-qat-q4_0-unquantized"
27
+
28
+ LTX_2_0_CHECKPOINT_PATH = CHECKPOINTS_DIR / "ltx-2-19b-dev.safetensors"
29
+ LTX_2_0_CHECKPOINT_FP8_PATH = CHECKPOINTS_DIR / "ltx-2-19b-dev-fp8.safetensors"
30
+ LTX_2_3_CHECKPOINT_PATH = CHECKPOINTS_DIR / "ltx-2.3-22b-dev.safetensors"
31
+ LTX_2_0_DISTILLED_CHECKPOINT_PATH = CHECKPOINTS_DIR / "ltx-2-19b-distilled.safetensors"
32
+ LTX_2_3_DISTILLED_CHECKPOINT_PATH = CHECKPOINTS_DIR / "ltx-2.3-22b-distilled.safetensors"
33
+
34
+ LTX_2_0_SPATIAL_UPSAMPLER_PATH = CHECKPOINTS_DIR / "ltx2-spatial-upscaler-x2-1.0.bf16.safetensors"
35
+ LTX_2_3_SPATIAL_UPSAMPLER_PATH = (
36
+ MODELS_PATH / "comfyui_models" / "latent_upscale_models" / "ltx-2.3-spatial-upscaler-x2-1.0.safetensors"
37
+ )
38
+
39
+ LTX_2_0_DISTILLED_LORA_PATH = LORAS_DIR / "ltxv" / "ltx2" / "ltx-av-distilled-from-42500-lora-384_comfy.safetensors"
40
+ LTX_2_3_DISTILLED_LORA_PATH = LORAS_DIR / "ltxv" / "ltx2" / "ltx-2.3-22b-distilled-lora-384.safetensors"
41
+ LTX_2_3_IC_LORA_PATH = LORAS_DIR / "ltxv" / "ltx2" / "ltx-2.3-22b-ic-lora-union-control-ref0.5.safetensors"
42
+ LTX_2_3_HDR_IC_LORA_PATH = LORAS_DIR / "ltxv" / "ltx2" / "ltx-2.3-22b-ic-lora-hdr.safetensors"
43
+ LTX_2_3_HDR_TEXT_EMBEDDINGS_PATH = LORAS_DIR / "ltxv" / "ltx2" / "ltx-2.3-22b-ic-lora-hdr-scene-emb.pt"
44
+
45
+
46
+ def _psnr(pred: torch.Tensor, target: torch.Tensor, max_val: float = 1.0, eps: float = 1e-8) -> torch.Tensor:
47
+ """
48
+ Compute Peak Signal-to-Noise Ratio (PSNR) between two images (or batches of images).
49
+
50
+ Args:
51
+ pred: Predicted image tensor, shape (..., H, W) or (..., C, H, W)
52
+ target: Ground truth image tensor, same shape as `pred`
53
+ max_val: Maximum possible pixel value of the images.
54
+ For images in [0, 1] use 1.0, for [0, 255] use 255.0, etc.
55
+ eps: Small value to avoid log of zero.
56
+
57
+ Returns:
58
+ psnr: PSNR value (in dB).
59
+ """
60
+ if pred.shape != target.shape:
61
+ raise ValueError(f"Shape mismatch: pred {pred.shape}, target {target.shape}")
62
+
63
+ pred = pred.float()
64
+ target = target.float()
65
+
66
+ if pred.dim() > 1:
67
+ mse = F.mse_loss(pred, target, reduction="none")
68
+ dims = list(range(mse.dim()))
69
+ mse = mse.mean(dim=dims)
70
+ else:
71
+ mse = F.mse_loss(pred, target, reduction="mean")
72
+
73
+ psnr_val = 10.0 * torch.log10((max_val**2) / (mse + eps))
74
+ return psnr_val
75
+
76
+
77
+ def _psnr_per_frame(pred: torch.Tensor, target: torch.Tensor, max_val: float = 1.0, eps: float = 1e-8) -> torch.Tensor:
78
+ """
79
+ Compute per-frame PSNR between two video tensors.
80
+
81
+ Args:
82
+ pred: Predicted video tensor, shape (T, C, H, W) or higher
83
+ target: Ground truth video tensor, same shape as `pred`
84
+ max_val: Maximum possible pixel value.
85
+ eps: Small value to avoid log of zero.
86
+
87
+ Returns:
88
+ psnr: Per-frame PSNR values (in dB).
89
+ """
90
+ if pred.shape != target.shape:
91
+ raise ValueError(f"Shape mismatch: pred {pred.shape}, target {target.shape}")
92
+
93
+ pred = pred.float()
94
+ target = target.float()
95
+
96
+ if pred.dim() < 4:
97
+ raise ValueError("Expected at least 4D tensor (T, C, H, W) for per-frame PSNR.")
98
+
99
+ mse = F.mse_loss(pred, target, reduction="none")
100
+ mse = mse.mean(dim=(-3, -2, -1))
101
+ psnr_val = 10.0 * torch.log10((max_val**2) / (mse + eps))
102
+ return psnr_val
103
+
104
+
105
+ def _decode_video_from_file(path: str, device: DeviceLikeType) -> tuple[torch.Tensor, torch.Tensor | None]:
106
+ container = av.open(path)
107
+ try:
108
+ video_stream = next(s for s in container.streams if s.type == "video")
109
+ audio_stream = next((s for s in container.streams if s.type == "audio"), None)
110
+
111
+ frames = []
112
+ audio = [] if audio_stream else None
113
+
114
+ streams_to_decode = [video_stream]
115
+ if audio_stream:
116
+ streams_to_decode.append(audio_stream)
117
+
118
+ for frame in container.decode(*streams_to_decode):
119
+ if isinstance(frame, av.VideoFrame):
120
+ tensor = torch.tensor(frame.to_rgb().to_ndarray(), dtype=torch.uint8, device=device).unsqueeze(0)
121
+ frames.append(tensor)
122
+ elif isinstance(frame, av.AudioFrame):
123
+ audio.append(torch.tensor(frame.to_ndarray(), dtype=torch.float32, device=device).unsqueeze(0))
124
+
125
+ if audio:
126
+ audio = torch.cat(audio)
127
+ finally:
128
+ container.close()
129
+
130
+ return torch.cat(frames), audio
131
+
132
+
133
+ class MockTransformer:
134
+ """Mock transformer that tracks calls and returns the input latents.
135
+
136
+ Records ``call_count`` and ``batch_sizes`` for assertions. Output is
137
+ simply ``video.latent`` / ``audio.latent`` (identity), which is
138
+ deterministic and split-safe for adapter equivalence tests.
139
+ """
140
+
141
+ def __init__(self) -> None:
142
+ self.call_count = 0
143
+ self.batch_sizes: list[int] = []
144
+
145
+ @staticmethod
146
+ def _validate_modality(modality: Modality) -> None:
147
+ """Check that all tensor fields share the same batch dimension.
148
+
149
+ The real transformer's ``_prepare_timestep`` does
150
+ ``view(batch_size, ...)`` on sigma embeddings, which crashes if
151
+ sigma's batch dim doesn't match latent's. This check catches
152
+ such mismatches without needing real model weights.
153
+ """
154
+ batch_size = modality.latent.shape[0]
155
+ for name, tensor in [
156
+ ("sigma", modality.sigma),
157
+ ("timesteps", modality.timesteps),
158
+ ("positions", modality.positions),
159
+ ("context", modality.context),
160
+ ]:
161
+ if tensor.shape[0] != batch_size:
162
+ msg = f"Modality.{name} batch dim {tensor.shape[0]} != latent batch dim {batch_size}"
163
+ raise ValueError(msg)
164
+
165
+ def __call__(
166
+ self,
167
+ video: Modality | None = None,
168
+ audio: Modality | None = None,
169
+ perturbations: BatchedPerturbationConfig | None = None, # noqa: ARG002
170
+ ) -> tuple[torch.Tensor | None, torch.Tensor | None]:
171
+ self.call_count += 1
172
+ if video is not None:
173
+ self._validate_modality(video)
174
+ if audio is not None:
175
+ self._validate_modality(audio)
176
+ b = video.latent.shape[0] if video is not None else audio.latent.shape[0]
177
+ self.batch_sizes.append(b)
178
+ return (
179
+ video.latent if video is not None else None,
180
+ audio.latent if audio is not None else None,
181
+ )
182
+
183
+
184
+ @pytest.fixture
185
+ def mock_transformer() -> MockTransformer:
186
+ """Return a fresh MockTransformer instance."""
187
+ return MockTransformer()
188
+
189
+
190
+ @pytest.fixture(autouse=True)
191
+ def _cleanup_cuda_memory() -> None:
192
+ """Free GPU memory before and after each test to prevent OOM across test modules."""
193
+ gc.collect()
194
+ torch.cuda.empty_cache()
195
+
196
+ yield
197
+
198
+ gc.collect()
199
+ torch.cuda.empty_cache()
200
+
201
+
202
+ @pytest.fixture
203
+ def psnr() -> Callable[[torch.Tensor, torch.Tensor, float, float], torch.Tensor]:
204
+ """Fixture that returns the PSNR function."""
205
+ return _psnr
206
+
207
+
208
+ @pytest.fixture
209
+ def psnr_per_frame() -> Callable[[torch.Tensor, torch.Tensor, float, float], torch.Tensor]:
210
+ """Fixture that returns the per-frame PSNR function."""
211
+ return _psnr_per_frame
212
+
213
+
214
+ @pytest.fixture
215
+ def decode_video_from_file() -> Callable[[str, DeviceLikeType], tuple[torch.Tensor, torch.Tensor | None]]:
216
+ """Fixture that returns the function to decode a video from a file."""
217
+ return _decode_video_from_file
ltx-2-internal/internal/ltx-kernels/MANIFEST.in ADDED
@@ -0,0 +1 @@
 
 
1
+ recursive-include csrc *.h *.cuh *.hpp *.cpp *.cu
ltx-2-internal/internal/ltx-kernels/README.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ltx-kernels
2
+
3
+ Custom CUDA/C++ All2All communication kernels for multi-GPU tensor parallelism. Used by the sequence parallel inference path in `ltx-core`.
4
+
5
+ ## Requirements
6
+
7
+ - CUDA toolkit (nvcc) matching your GPU architecture
8
+ - PyTorch with CUDA support
9
+ - Linux
10
+
11
+ ## Building
12
+
13
+ From the repository root:
14
+
15
+ ```bash
16
+ uv pip install -e internal/ltx-kernels --no-build-isolation
17
+ ```
18
+
19
+ Set `TORCH_CUDA_ARCH_LIST` to target specific architectures (speeds up compilation):
20
+
21
+ ```bash
22
+ # H100 only
23
+ TORCH_CUDA_ARCH_LIST="9.0" uv pip install -e internal/ltx-kernels --no-build-isolation
24
+
25
+ # Multiple architectures
26
+ TORCH_CUDA_ARCH_LIST="9.0 9.0a 10.0 12.0" uv pip install -e internal/ltx-kernels --no-build-isolation
27
+ ```
28
+
29
+ ## Testing
30
+
31
+ Tests require a CUDA GPU:
32
+
33
+ ```bash
34
+ uv run pytest internal/ltx-kernels/tests/ -v
35
+ ```
36
+
37
+ ## Operations
38
+
39
+ - **send_recv_heads** -- Redistributes attention heads across GPUs (All2All)
40
+ - **gather_heads** -- Inverse of send_recv_heads
41
+ - **allgather** -- Gathers sequence tokens from all ranks
42
+
43
+ All operations support BFloat16 and Float8 (e4m3fn) data types.
ltx-2-internal/internal/ltx-kernels/csrc/all2all/all2all.cpp ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @file all2all.cpp
3
+ * @brief Implementation of All2All communication primitives for multi-GPU tensor parallelism.
4
+ *
5
+ * This file implements the All2All class which provides efficient inter-GPU communication
6
+ * using CUDA IPC (Inter-Process Communication). The implementation supports:
7
+ * - Head redistribution for tensor-parallel attention (send_recv_heads, gather_heads)
8
+ * - Sequence gathering for cross-rank aggregation (allgather)
9
+ *
10
+ * All operations use a barrier-based synchronization protocol where each GPU writes
11
+ * directly to remote GPU memory via IPC, then signals completion through atomic
12
+ * operations on barrier counters.
13
+ */
14
+
15
+ #include <ATen/cuda/CUDAContext.h>
16
+ #include <ATen/cuda/CUDADataType.h>
17
+ #include <c10/cuda/CUDAGuard.h>
18
+
19
+ #include <chrono>
20
+ #include <cuda_runtime.h>
21
+ #include <memory>
22
+ #include <pybind11/functional.h>
23
+ #include <torch/python.h>
24
+
25
+ #include "all2all.hpp"
26
+ #include "cuda/api.cuh"
27
+ #include "cuda/configs.cuh"
28
+
29
+ namespace ltx_kernels {
30
+ namespace all2all {
31
+
32
+ /**
33
+ * Constructs the All2All communication manager.
34
+ *
35
+ * Memory Allocation Strategy:
36
+ * The constructor allocates a single contiguous GPU memory block that contains:
37
+ * 1. Data buffer (tensor_bytes): Space for tensor data exchange
38
+ * 2. Barrier signals (MAX_NUM_PEERS * sizeof(int)): Per-rank completion counters
39
+ * 3. Buffer pointers (MAX_NUM_PEERS * sizeof(void*)): GPU-accessible pointer array
40
+ * 4. Barrier pointer array (MAX_NUM_PEERS * sizeof(int*)): GPU-accessible signal pointers
41
+ *
42
+ * This layout minimizes memory allocations and allows the entire region to be
43
+ * shared via a single IPC handle.
44
+ */
45
+ All2All::All2All(int rank, int world_size, int num_tokens, int hidden_dim, int num_sms, at::ScalarType tensor_dtype)
46
+ : rank(rank), world_size(world_size), num_sms(num_sms), max_tokens(num_tokens), num_elems(0), tensor_bytes(0),
47
+ tensor_dtype(tensor_dtype) {
48
+ num_elems = int64_t(num_tokens) * int64_t(hidden_dim);
49
+ tensor_bytes = num_elems * elementSize(tensor_dtype);
50
+ // Calculate sizes for each region of the shared memory block
51
+ int64_t ptrs_bytes = MAX_NUM_PEERS * sizeof(void *);
52
+ int64_t barrier_signal_bytes = MAX_NUM_PEERS * sizeof(int);
53
+ int64_t barrier_signal_ptrs_bytes = MAX_NUM_PEERS * sizeof(int *);
54
+
55
+ // Allocate GPU memory for token count arrays (used by kernels)
56
+ CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&rank_tokens_gpu), sizeof(int) * MAX_NUM_PEERS));
57
+ CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&prefix_rank_tokens_gpu), sizeof(int) * MAX_NUM_PEERS));
58
+
59
+ // Allocate the main shared memory block and create IPC handle
60
+ // Layout: [data_buffer | barrier_signals | buffer_ptrs | barrier_signal_ptrs]
61
+ CUDA_CHECK(
62
+ cudaMalloc(&buffer_ptrs[rank], tensor_bytes + barrier_signal_bytes + ptrs_bytes + barrier_signal_ptrs_bytes));
63
+ CUDA_CHECK(cudaIpcGetMemHandle(&ipc_handlers[rank], buffer_ptrs[rank]));
64
+
65
+ // Set up pointers to each region within the allocated block
66
+ buffer_ptrs_gpu =
67
+ reinterpret_cast<void **>(static_cast<uint8_t *>(buffer_ptrs[rank]) + tensor_bytes + barrier_signal_bytes);
68
+ barrier_signal_ptrs[rank] = reinterpret_cast<int *>(static_cast<uint8_t *>(buffer_ptrs[rank]) + tensor_bytes);
69
+ barrier_signal_ptrs_gpu = reinterpret_cast<int **>(static_cast<uint8_t *>(buffer_ptrs[rank]) + tensor_bytes +
70
+ barrier_signal_bytes + ptrs_bytes);
71
+
72
+ // Initialize barrier signals to zero
73
+ CUDA_CHECK(cudaMemset(barrier_signal_ptrs[rank], 0, barrier_signal_bytes));
74
+ }
75
+
76
+ All2All::~All2All() noexcept(false) {
77
+ if (!destroyed) {
78
+ printf("WARNING: destroy() was not called, which can leak resources.\n");
79
+ fflush(stdout);
80
+ destroy();
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Releases all allocated resources.
86
+ *
87
+ * This must be called explicitly before destruction to ensure proper cleanup of:
88
+ * - IPC memory mappings to remote GPUs
89
+ * - Local GPU memory allocations
90
+ *
91
+ * The method synchronizes the device to ensure all pending operations complete
92
+ * before releasing resources.
93
+ */
94
+ void All2All::destroy() {
95
+ if (destroyed) {
96
+ return;
97
+ }
98
+ CUDA_CHECK(cudaDeviceSynchronize());
99
+
100
+ // Close IPC mappings to remote GPU memory (skip our own rank)
101
+ // Only close handles that were actually opened via sync()
102
+ for (int i = 0; i < world_size; i++) {
103
+ if (i != rank && buffer_ptrs[i] != nullptr) {
104
+ CUDA_CHECK(cudaIpcCloseMemHandle(buffer_ptrs[i]));
105
+ }
106
+ }
107
+
108
+ // Free local GPU memory allocations
109
+ CUDA_CHECK(cudaFree(buffer_ptrs[rank]));
110
+ CUDA_CHECK(cudaFree(rank_tokens_gpu));
111
+ CUDA_CHECK(cudaFree(prefix_rank_tokens_gpu));
112
+ destroyed = true;
113
+ }
114
+
115
+ /**
116
+ * Opens IPC memory mappings to all peer GPUs.
117
+ *
118
+ * This method processes IPC handles gathered from all ranks and opens memory
119
+ * mappings to enable direct GPU-to-GPU memory access. After calling this method,
120
+ * each GPU can read/write directly to any other GPU's buffer via buffer_ptrs.
121
+ *
122
+ * The barrier_signal_ptrs are also set up to point to the correct offset within
123
+ * each peer's shared memory block.
124
+ */
125
+ void All2All::sync(const std::vector<std::optional<pybind11::bytearray>> &all_gathered_handles) {
126
+ for (int i = 0; i < world_size; i++) {
127
+ auto handle_str = std::string(all_gathered_handles[i].value());
128
+ EP_HOST_ASSERT(handle_str.size() == CUDA_IPC_HANDLE_SIZE);
129
+
130
+ if (i != rank) {
131
+ // Open IPC mapping to remote GPU's memory
132
+ std::memcpy(ipc_handlers[i].reserved, handle_str.c_str(), CUDA_IPC_HANDLE_SIZE);
133
+ CUDA_CHECK(cudaIpcOpenMemHandle(&buffer_ptrs[i], ipc_handlers[i], cudaIpcMemLazyEnablePeerAccess));
134
+ // Calculate offset to barrier signals in remote buffer
135
+ barrier_signal_ptrs[i] = reinterpret_cast<int *>(static_cast<uint8_t *>(buffer_ptrs[i]) + tensor_bytes);
136
+ } else {
137
+ // Verify our own handle matches what we sent
138
+ EP_HOST_ASSERT(std::memcmp(ipc_handlers[i].reserved, handle_str.c_str(), CUDA_IPC_HANDLE_SIZE) == 0);
139
+ }
140
+ }
141
+
142
+ // Copy pointer arrays to GPU for kernel access
143
+ CUDA_CHECK(cudaMemcpy(buffer_ptrs_gpu, buffer_ptrs, sizeof(void *) * world_size, cudaMemcpyHostToDevice));
144
+ CUDA_CHECK(
145
+ cudaMemcpy(barrier_signal_ptrs_gpu, barrier_signal_ptrs, sizeof(int *) * world_size, cudaMemcpyHostToDevice));
146
+ CUDA_CHECK(cudaDeviceSynchronize());
147
+ }
148
+
149
+ pybind11::bytearray All2All::get_local_ipc_handle() const {
150
+ return {ipc_handlers[rank].reserved, CUDA_IPC_HANDLE_SIZE};
151
+ }
152
+
153
+ /**
154
+ * Configures token distribution across ranks for the current batch.
155
+ *
156
+ * This method computes prefix sums needed by the kernels to calculate source
157
+ * and destination offsets. It must be called before any communication operation
158
+ * when the token distribution changes between batches.
159
+ *
160
+ * Example: For rank_num_tokens = {128, 96, 128, 64}
161
+ * - rank_tokens = {128, 96, 128, 64}
162
+ * - prefix_rank_tokens = {0, 128, 224, 352}
163
+ * - total_tokens = 416
164
+ */
165
+ void All2All::set_rank_tokens(const std::vector<int> &rank_num_tokens) {
166
+ EP_HOST_ASSERT(static_cast<int>(rank_num_tokens.size()) == world_size);
167
+
168
+ // Initialize prefix sums to zero
169
+ for (int i = 0; i < world_size; i++) {
170
+ prefix_rank_tokens[i] = 0;
171
+ }
172
+
173
+ // Compute prefix sums (exclusive scan)
174
+ for (int i = 0; i < world_size; i++) {
175
+ rank_tokens[i] = rank_num_tokens[i];
176
+ if (i > 0) {
177
+ prefix_rank_tokens[i] = prefix_rank_tokens[i - 1] + rank_tokens[i - 1];
178
+ }
179
+ }
180
+
181
+ // Total tokens is the sum of all rank tokens
182
+ total_tokens = prefix_rank_tokens[world_size - 1] + rank_tokens[world_size - 1];
183
+
184
+ // Copy to GPU for kernel access
185
+ CUDA_CHECK(cudaMemcpy(rank_tokens_gpu, rank_tokens, sizeof(int) * MAX_NUM_PEERS, cudaMemcpyHostToDevice));
186
+ CUDA_CHECK(
187
+ cudaMemcpy(prefix_rank_tokens_gpu, prefix_rank_tokens, sizeof(int) * MAX_NUM_PEERS, cudaMemcpyHostToDevice));
188
+ CUDA_CHECK(cudaDeviceSynchronize());
189
+ }
190
+
191
+ /**
192
+ * Creates a tensor from the local IPC buffer.
193
+ *
194
+ * This helper method returns either a zero-copy view of the IPC buffer or
195
+ * a newly allocated tensor with the data copied. The zero-copy mode is more
196
+ * efficient but the tensor lifetime is tied to the All2All instance.
197
+ *
198
+ * @note The buffer pointer is cast to the template type T for proper interpretation.
199
+ */
200
+ at::Tensor All2All::get_local_buffer_tensor(at::Tensor &x, int batch_size, int out_tokens, int out_heads, int head_size,
201
+ bool should_copy, cudaStream_t stream) {
202
+ auto ptr = buffer_ptrs[rank];
203
+ if (should_copy) {
204
+ // Allocate new tensor and copy data from IPC buffer
205
+ auto out_tensor = torch::empty({batch_size, out_tokens, out_heads, head_size}, x.options());
206
+ CUDA_CHECK(cudaMemcpyAsync(out_tensor.data_ptr(), ptr,
207
+ int64_t(batch_size) * int64_t(out_tokens) * int64_t(out_heads) * int64_t(head_size) *
208
+ int64_t(elementSize(x.scalar_type())),
209
+ cudaMemcpyDeviceToDevice, stream));
210
+ return out_tensor;
211
+ } else {
212
+ // Return a view directly into the IPC buffer (zero-copy)
213
+ auto out_tensor = torch::from_blob(ptr, {batch_size, out_tokens, out_heads, head_size}, x.options());
214
+ return out_tensor;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * All2All communication to redistribute attention heads across GPUs.
220
+ *
221
+ * This operation is used in tensor-parallel transformers to exchange attention heads:
222
+ * - Before: Each GPU has all tokens but only a subset of heads
223
+ * - After: Each GPU has all tokens with heads redistributed
224
+ *
225
+ * Tensor Layout Transformation:
226
+ * Input: [batch, local_tokens, all_heads, head_size] per GPU
227
+ * Output: [batch, all_tokens, heads_per_rank, head_size] per GPU
228
+ *
229
+ * The operation partitions heads evenly: heads_per_rank = all_heads / world_size
230
+ * GPU i receives heads [i*heads_per_rank : (i+1)*heads_per_rank] from all GPUs.
231
+ */
232
+ at::Tensor All2All::send_recv_heads(at::Tensor &x, bool copy_output) {
233
+ // Validate input tensor properties
234
+ EP_HOST_ASSERT(x.dim() == 4 and x.is_contiguous());
235
+ EP_HOST_ASSERT(x.dtype() == tensor_dtype);
236
+ EP_HOST_ASSERT(x.device().is_cuda());
237
+ EP_HOST_ASSERT(x.device().index() == rank);
238
+
239
+ int batch_size = x.size(0);
240
+ int num_tokens = x.size(1);
241
+ int num_heads = x.size(2);
242
+ int head_size = x.size(3);
243
+
244
+ // Output dimensions after redistribution
245
+ int out_tokens = total_tokens; // All tokens from all ranks
246
+ int out_heads = num_heads / world_size; // Each rank gets 1/world_size of heads
247
+
248
+ EP_HOST_ASSERT(int64_t(batch_size) * int64_t(out_tokens) * int64_t(out_heads) * int64_t(head_size) *
249
+ int64_t(elementSize(x.scalar_type())) <=
250
+ tensor_bytes);
251
+
252
+ at::cuda::CUDAGuard device_guard{x.device()};
253
+ auto stream = at::cuda::getCurrentCUDAStream().stream();
254
+
255
+ // Launch the All2All kernel
256
+ all2all_cuda::all2all_head_launch(buffer_ptrs_gpu, barrier_signal_ptrs_gpu, x.data_ptr(), prefix_rank_tokens_gpu,
257
+ rank, world_size, batch_size, total_tokens, num_tokens, num_heads, head_size,
258
+ stream, num_sms, tensor_dtype);
259
+
260
+ return get_local_buffer_tensor(x, batch_size, out_tokens, out_heads, head_size, copy_output, stream);
261
+ }
262
+
263
+ /**
264
+ * Inverse All2All to gather heads back to original distribution.
265
+ *
266
+ * This is the inverse operation of send_recv_heads(). It redistributes data
267
+ * so each GPU gets back its original tokens with all attention heads.
268
+ *
269
+ * Tensor Layout Transformation:
270
+ * Input: [batch, all_tokens, heads_per_rank, head_size] per GPU
271
+ * Output: [batch, local_tokens, all_heads, head_size] per GPU
272
+ *
273
+ * Each GPU sends its portion of tokens to the originating rank, reconstructing
274
+ * the original head distribution.
275
+ */
276
+ at::Tensor All2All::gather_heads(at::Tensor &x, bool copy_output) {
277
+ // Validate input tensor properties
278
+ EP_HOST_ASSERT(x.dim() == 4 and x.is_contiguous());
279
+ EP_HOST_ASSERT(x.dtype() == tensor_dtype);
280
+ EP_HOST_ASSERT(x.device().is_cuda());
281
+ EP_HOST_ASSERT(x.device().index() == rank);
282
+
283
+ at::cuda::CUDAGuard device_guard{x.device()};
284
+ auto stream = at::cuda::getCurrentCUDAStream().stream();
285
+
286
+ int batch_size = x.size(0);
287
+ int num_heads = x.size(2) * world_size; // Reconstruct total head count
288
+ int head_size = x.size(3);
289
+
290
+ // Output dimensions: this rank's tokens with all heads
291
+ int out_tokens = rank_tokens[rank];
292
+ int out_heads = num_heads;
293
+
294
+ EP_HOST_ASSERT(int64_t(batch_size) * int64_t(out_tokens) * int64_t(out_heads) * int64_t(head_size) *
295
+ int64_t(elementSize(x.scalar_type())) <=
296
+ tensor_bytes);
297
+
298
+ // Launch the gather kernel
299
+ all2all_cuda::all2all_head_gather_launch(buffer_ptrs_gpu, barrier_signal_ptrs_gpu, x.data_ptr(), rank_tokens_gpu,
300
+ prefix_rank_tokens_gpu, rank, world_size, batch_size, total_tokens,
301
+ num_heads, head_size, stream, num_sms, tensor_dtype);
302
+
303
+ return get_local_buffer_tensor(x, batch_size, out_tokens, out_heads, head_size, copy_output, stream);
304
+ }
305
+
306
+ /**
307
+ * AllGather operation to collect sequence tokens from all ranks.
308
+ *
309
+ * Each GPU contributes its local sequence tokens, which are gathered into
310
+ * a complete sequence replicated on all GPUs. This is typically used after
311
+ * tensor-parallel operations to reconstruct the full sequence.
312
+ *
313
+ * Tensor Layout Transformation:
314
+ * Input: [batch, local_seqlen, heads, head_size] per GPU
315
+ * Output: [batch, total_seqlen, heads, head_size] per GPU (identical on all GPUs)
316
+ *
317
+ * Each GPU's tokens are placed at offset prefix_rank_tokens[rank] in the output.
318
+ */
319
+ at::Tensor All2All::allgather(at::Tensor &x, bool copy_output) {
320
+ // Validate input tensor properties
321
+ EP_HOST_ASSERT(x.dim() == 4 and x.is_contiguous());
322
+ EP_HOST_ASSERT(x.dtype() == tensor_dtype);
323
+ EP_HOST_ASSERT(x.device().is_cuda());
324
+ EP_HOST_ASSERT(x.device().index() == rank);
325
+
326
+ at::cuda::CUDAGuard device_guard{x.device()};
327
+ auto stream = at::cuda::getCurrentCUDAStream().stream();
328
+
329
+ int batch_size = x.size(0);
330
+ int seqlen = x.size(1);
331
+ int num_heads = x.size(2);
332
+ int head_size = x.size(3);
333
+
334
+ // Output contains all tokens from all ranks
335
+ int out_tokens = total_tokens;
336
+ int out_heads = num_heads;
337
+ int hidden_dim = num_heads * head_size;
338
+
339
+ EP_HOST_ASSERT(int64_t(batch_size) * int64_t(out_tokens) * int64_t(out_heads) * int64_t(head_size) *
340
+ int64_t(elementSize(x.scalar_type())) <=
341
+ tensor_bytes);
342
+
343
+ // Launch the allgather kernel
344
+ all2all_cuda::allgather_launch(buffer_ptrs_gpu, barrier_signal_ptrs_gpu, x.data_ptr(), prefix_rank_tokens_gpu, rank,
345
+ world_size, batch_size, seqlen, hidden_dim, total_tokens, stream, num_sms,
346
+ tensor_dtype);
347
+
348
+ return get_local_buffer_tensor(x, batch_size, out_tokens, out_heads, head_size, copy_output, stream);
349
+ }
350
+
351
+ } // namespace all2all
352
+ } // namespace ltx_kernels
353
+
354
+ /**
355
+ * Python bindings for the All2All communication library.
356
+ *
357
+ * Usage from Python:
358
+ * import all2all_cpp
359
+ *
360
+ * # Create instance (one per GPU)
361
+ * comm = all2all_cpp.All2All(rank, world_size, max_tokens, hidden_dim, num_sms, dtype)
362
+ *
363
+ * # Exchange IPC handles and synchronize
364
+ * handle = comm.get_local_ipc_handle()
365
+ * # ... gather handles via NCCL ...
366
+ * comm.sync(all_handles)
367
+ *
368
+ * # Set token distribution
369
+ * comm.set_rank_tokens([128, 128, 128, 128])
370
+ *
371
+ * # Perform operations
372
+ * output = comm.send_recv_heads(input_tensor, copy_output=False)
373
+ *
374
+ * # Cleanup
375
+ * comm.destroy()
376
+ */
377
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
378
+ m.doc() = "High-performance All2All communication library for multi-GPU tensor parallelism.\n\n"
379
+ "This library provides IPC-based All2All operations optimized for transformer models.\n"
380
+ "Supported operations:\n"
381
+ " - send_recv_heads: Redistribute attention heads across GPUs\n"
382
+ " - gather_heads: Inverse of send_recv_heads\n"
383
+ " - allgather: Gather sequence tokens from all ranks\n";
384
+
385
+ pybind11::class_<ltx_kernels::all2all::All2All>(
386
+ m, "All2All",
387
+ "Manages All2All communication state for multi-GPU operations.\n\n"
388
+ "Args:\n"
389
+ " rank: This GPU's rank (0 to world_size-1)\n"
390
+ " world_size: Total number of GPUs\n"
391
+ " num_tokens: Maximum tokens per rank\n"
392
+ " hidden_dim: Hidden dimension (heads * head_size)\n"
393
+ " num_sms: Number of SMs for kernel launches\n"
394
+ " tensor_dtype: Tensor data type (torch.bfloat16 or torch.float8_e4m3fn)")
395
+ .def(pybind11::init<int, int, int, int, int, at::ScalarType>())
396
+ .def("get_local_ipc_handle", &ltx_kernels::all2all::All2All::get_local_ipc_handle,
397
+ "Returns the IPC handle for this rank's buffer.")
398
+ .def("sync", &ltx_kernels::all2all::All2All::sync, "Opens IPC mappings to all peer GPUs using gathered handles.")
399
+ .def("destroy", &ltx_kernels::all2all::All2All::destroy,
400
+ "Releases all GPU resources. Must be called before destruction.")
401
+ .def("send_recv_heads", &ltx_kernels::all2all::All2All::send_recv_heads,
402
+ "All2All operation to redistribute attention heads.")
403
+ .def("gather_heads", &ltx_kernels::all2all::All2All::gather_heads,
404
+ "Inverse All2All to gather heads back to original distribution.")
405
+ .def("allgather", &ltx_kernels::all2all::All2All::allgather, "Gathers sequence tokens from all ranks.")
406
+ .def("set_rank_tokens", &ltx_kernels::all2all::All2All::set_rank_tokens,
407
+ "Sets token counts per rank for the current batch.");
408
+ }
ltx-2-internal/internal/ltx-kernels/csrc/all2all/all2all.hpp ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @file all2all.hpp
3
+ * @brief High-performance All2All communication primitives for multi-GPU tensor parallelism.
4
+ *
5
+ * This library provides efficient All2All communication operations optimized for transformer
6
+ * models using tensor parallelism. It uses CUDA IPC (Inter-Process Communication) for
7
+ * zero-copy data transfer between GPUs in the same node.
8
+ *
9
+ * ## Architecture Overview
10
+ *
11
+ * The All2All class manages shared memory buffers accessible by all GPUs via IPC handles.
12
+ * Each GPU allocates a contiguous memory region containing:
13
+ * - Data buffer: Stores tensor data for exchange
14
+ * - Barrier signals: Synchronization counters for coordination
15
+ * - GPU pointer arrays: Device-accessible pointers to all peer buffers
16
+ *
17
+ * Memory Layout (per GPU):
18
+ * ```
19
+ * |<---- tensor_bytes ---->|<-- barrier signals -->|<-- buffer_ptrs_gpu -->|<-- barrier_signal_ptrs_gpu -->|
20
+ * | Data Buffer | MAX_PEERS * int | MAX_PEERS * void* | MAX_PEERS * int* |
21
+ * ```
22
+ *
23
+ * ## Supported Operations
24
+ *
25
+ * 1. **send_recv_heads**: Redistributes attention heads across GPUs (All2All)
26
+ * - Input: [batch, tokens, heads, head_size] on each GPU
27
+ * - Output: [batch, total_tokens, heads/world_size, head_size] on each GPU
28
+ *
29
+ * 2. **gather_heads**: Inverse of send_recv_heads
30
+ * - Gathers distributed heads back to original distribution
31
+ *
32
+ * 3. **allgather**: Gathers sequence data from all ranks
33
+ * - Each GPU contributes its local tokens to form the complete sequence
34
+ *
35
+ * ## Thread Safety
36
+ *
37
+ * - The class is NOT thread-safe. Each thread/process should have its own instance.
38
+ * - Multiple CUDA streams may use the same instance sequentially.
39
+ * - The `destroy()` method MUST be called before destruction to properly release IPC handles.
40
+ *
41
+ * ## Usage Example
42
+ *
43
+ * ```cpp
44
+ * // Initialize on each GPU
45
+ * auto comm = All2All(rank, world_size, max_tokens, hidden_dim, num_sms, dtype);
46
+ *
47
+ * // Exchange IPC handles (via NCCL or other collective)
48
+ * auto my_handle = comm.get_local_ipc_handle();
49
+ * // ... gather all handles ...
50
+ * comm.sync(all_handles);
51
+ *
52
+ * // Set token distribution for current batch
53
+ * comm.set_rank_tokens({128, 128, 128, 128}); // tokens per rank
54
+ *
55
+ * // Perform All2All on attention heads
56
+ * auto result = comm.send_recv_heads(input_tensor, copy_output=false);
57
+ *
58
+ * // Clean up
59
+ * comm.destroy();
60
+ * ```
61
+ */
62
+
63
+ #pragma once
64
+
65
+ #include "cuda/configs.cuh"
66
+ #include "event.hpp"
67
+ #include <pybind11/pybind11.h>
68
+ #include <pybind11/pytypes.h>
69
+ #include <torch/types.h>
70
+ #include <tuple>
71
+ #include <vector>
72
+
73
+ namespace ltx_kernels {
74
+ namespace all2all {
75
+
76
+ /**
77
+ * @class All2All
78
+ * @brief Manages All2All communication state and operations for multi-GPU tensor parallelism.
79
+ *
80
+ * This class encapsulates the IPC-based communication infrastructure needed for
81
+ * efficient All2All operations. It maintains shared memory buffers, barrier signals,
82
+ * and provides methods for head-parallel tensor redistribution.
83
+ */
84
+ struct All2All {
85
+ private:
86
+ int rank; ///< This GPU's rank (0 to world_size-1)
87
+ int world_size; ///< Total number of GPUs in the communication group
88
+ int num_sms; ///< Number of SMs to use for kernel launches
89
+ int max_tokens; ///< Maximum number of tokens the buffer was allocated for
90
+ int64_t num_elems; ///< Number of elements in the data buffer (tokens * hidden_dim)
91
+ int64_t tensor_bytes; ///< Size of the data buffer in bytes
92
+
93
+ /// Host array of pointers to each rank's data buffer (GPU memory)
94
+ void *buffer_ptrs[MAX_NUM_PEERS] = {nullptr};
95
+ /// Device-accessible array of buffer pointers (copied to GPU)
96
+ void **buffer_ptrs_gpu = nullptr;
97
+
98
+ /// Host array of pointers to each rank's barrier signal buffer
99
+ int *barrier_signal_ptrs[MAX_NUM_PEERS] = {nullptr};
100
+ /// Device-accessible array of barrier signal pointers
101
+ int **barrier_signal_ptrs_gpu = nullptr;
102
+
103
+ /// IPC handles for sharing memory between processes
104
+ cudaIpcMemHandle_t ipc_handlers[MAX_NUM_PEERS];
105
+
106
+ at::ScalarType tensor_dtype; ///< Data type of tensors (BFloat16 or Float8_e4m3fn)
107
+ bool destroyed = false; ///< Flag to track if resources have been released
108
+
109
+ int total_tokens; ///< Sum of tokens across all ranks for current batch
110
+ int rank_tokens[MAX_NUM_PEERS]; ///< Number of tokens on each rank
111
+ int prefix_rank_tokens[MAX_NUM_PEERS]; ///< Cumulative sum of tokens (for offset calculation)
112
+ int *rank_tokens_gpu = nullptr; ///< Device copy of rank_tokens
113
+ int *prefix_rank_tokens_gpu = nullptr; ///< Device copy of prefix_rank_tokens
114
+
115
+ public:
116
+ /**
117
+ * @brief Constructs an All2All communication manager.
118
+ *
119
+ * Allocates GPU memory for the local data buffer, barrier signals, and pointer arrays.
120
+ * The IPC handle for the local buffer is created and can be retrieved via get_local_ipc_handle().
121
+ *
122
+ * @param rank This GPU's rank in the communication group (0-indexed)
123
+ * @param world_size Total number of GPUs/ranks
124
+ * @param num_tokens Maximum number of tokens this rank will handle
125
+ * @param hidden_dim Hidden dimension size (heads * head_size)
126
+ * @param num_sms Number of CUDA SMs to use for kernel execution
127
+ * @param tensor_dtype Data type for tensors (BFloat16 or Float8_e4m3fn)
128
+ */
129
+ All2All(int rank, int world_size, int num_tokens, int hidden_dim, int num_sms, at::ScalarType tensor_dtype);
130
+
131
+ /**
132
+ * @brief Destructor - warns if destroy() was not called.
133
+ *
134
+ * @warning Always call destroy() explicitly before the destructor to properly
135
+ * release IPC handles. Failing to do so may leak resources.
136
+ */
137
+ ~All2All() noexcept(false);
138
+
139
+ /**
140
+ * @brief Synchronizes IPC handles from all ranks and opens remote memory mappings.
141
+ *
142
+ * This method must be called after all ranks have created their All2All instances
143
+ * and exchanged IPC handles via an external collective (e.g., NCCL allgather).
144
+ *
145
+ * @param all_gathered_handles Vector of IPC handles from all ranks (indexed by rank)
146
+ */
147
+ void sync(const std::vector<std::optional<pybind11::bytearray>> &all_gathered_handles);
148
+
149
+ /**
150
+ * @brief Returns the IPC handle for this rank's shared buffer.
151
+ *
152
+ * The returned handle should be gathered across all ranks and passed to sync().
153
+ *
154
+ * @return pybind11::bytearray containing the CUDA IPC handle (CUDA_IPC_HANDLE_SIZE bytes)
155
+ */
156
+ pybind11::bytearray get_local_ipc_handle() const;
157
+
158
+ /**
159
+ * @brief Creates a tensor view or copy of the local output buffer.
160
+ *
161
+ * @param x Reference tensor for options (dtype, device)
162
+ * @param batch_size Batch dimension size
163
+ * @param out_tokens Output token dimension size
164
+ * @param out_heads Output heads dimension size
165
+ * @param head_size Head dimension size
166
+ * @param should_copy If true, copies data to a new tensor; if false, returns a view
167
+ * @param stream CUDA stream for async copy
168
+ * @return Tensor with shape [batch_size, out_tokens, out_heads, head_size]
169
+ */
170
+ at::Tensor get_local_buffer_tensor(at::Tensor &x, int batch_size, int out_tokens, int out_heads, int head_size,
171
+ bool should_copy, cudaStream_t stream);
172
+
173
+ /**
174
+ * @brief Releases all GPU resources and closes IPC handles.
175
+ *
176
+ * This method MUST be called before the object is destroyed. It synchronizes
177
+ * the device, closes remote IPC mappings, and frees local GPU memory.
178
+ */
179
+ void destroy();
180
+
181
+ /**
182
+ * @brief Performs All2All communication to redistribute attention heads.
183
+ *
184
+ * Redistributes tensor from [batch, local_tokens, all_heads, head_size] to
185
+ * [batch, all_tokens, local_heads, head_size]. Each rank sends its portion
186
+ * of heads to the corresponding target rank.
187
+ *
188
+ * @param x Input tensor with shape [batch, num_tokens, num_heads, head_size]
189
+ * @param copy_output If true, returns a copy; if false, returns a view of the IPC buffer
190
+ * @return Tensor with shape [batch, total_tokens, num_heads/world_size, head_size]
191
+ */
192
+ at::Tensor send_recv_heads(at::Tensor &x, bool copy_output);
193
+
194
+ /**
195
+ * @brief Performs inverse All2All to gather heads back to original distribution.
196
+ *
197
+ * Inverse of send_recv_heads(). Redistributes from [batch, all_tokens, local_heads, head_size]
198
+ * back to [batch, local_tokens, all_heads, head_size].
199
+ *
200
+ * @param x Input tensor with shape [batch, total_tokens, heads_per_rank, head_size]
201
+ * @param copy_output If true, returns a copy; if false, returns a view of the IPC buffer
202
+ * @return Tensor with shape [batch, rank_tokens[rank], num_heads, head_size]
203
+ */
204
+ at::Tensor gather_heads(at::Tensor &x, bool copy_output);
205
+
206
+ /**
207
+ * @brief Gathers sequence tokens from all ranks.
208
+ *
209
+ * Each rank contributes its local sequence tokens, which are gathered into
210
+ * a complete sequence on all ranks.
211
+ *
212
+ * @param x Input tensor with shape [batch, seqlen, num_heads, head_size]
213
+ * @param copy_output If true, returns a copy; if false, returns a view of the IPC buffer
214
+ * @return Tensor with shape [batch, total_tokens, num_heads, head_size]
215
+ */
216
+ at::Tensor allgather(at::Tensor &x, bool copy_output);
217
+
218
+ /**
219
+ * @brief Sets the token count for each rank in the current batch.
220
+ *
221
+ * Must be called before send_recv_heads(), gather_heads(), or allgather()
222
+ * to configure the token distribution. This allows variable-length sequences
223
+ * across ranks.
224
+ *
225
+ * @param rank_num_tokens Vector of token counts, one per rank (must have world_size elements)
226
+ */
227
+ void set_rank_tokens(const std::vector<int> &rank_num_tokens);
228
+ };
229
+
230
+ } // namespace all2all
231
+ } // namespace ltx_kernels
ltx-2-internal/internal/ltx-kernels/csrc/all2all/cuda/all2all_heads.cu ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @file all2all_heads.cu
3
+ * @brief CUDA kernels for All2All attention head redistribution.
4
+ *
5
+ * This file implements the GPU kernels for redistributing attention heads across
6
+ * multiple GPUs using IPC-based direct memory access. The kernels are designed
7
+ * for tensor-parallel transformer models where attention heads need to be
8
+ * exchanged between GPUs.
9
+ *
10
+ * ## Algorithm Overview
11
+ *
12
+ * The kernels use a direct-write approach where each GPU writes its data directly
13
+ * to the target GPU's memory buffer via IPC. This avoids intermediate copies and
14
+ * achieves near-peak memory bandwidth utilization.
15
+ *
16
+ * ## SM Work Distribution (Round-Robin)
17
+ *
18
+ * SMs are distributed round-robin among target ranks to handle non-divisible SM counts:
19
+ * - SM i writes to rank (i % world_size)
20
+ * - With 132 SMs and 8 GPUs: ranks 0-3 get 17 SMs, ranks 4-7 get 16 SMs
21
+ * - Each SM group processes all tokens for its assigned target rank
22
+ * - Within each group, SMs cooperate to cover all tokens in strided fashion
23
+ *
24
+ * ## Synchronization Protocol
25
+ *
26
+ * After data transfer, a barrier synchronization ensures all ranks have completed:
27
+ * 1. Each SM atomically increments the target rank's barrier counter for this rank
28
+ * 2. SM 0 waits until it has received signals from all ranks
29
+ * 3. Barrier counters are reset for the next operation
30
+ */
31
+
32
+ #include "cuda/configs.cuh"
33
+ #include "cuda/exceptions.cuh"
34
+ #include "cuda/utils.cuh"
35
+ #include <ATen/cuda/CUDADataType.h>
36
+
37
+ namespace ltx_kernels {
38
+ namespace all2all {
39
+ namespace all2all_cuda {
40
+
41
+ /**
42
+ * @brief All2All kernel for redistributing attention heads across GPUs.
43
+ *
44
+ * This kernel performs the "send" phase of All2All: each GPU writes its assigned
45
+ * subset of attention heads to all other GPUs. The data layout transformation is:
46
+ *
47
+ * Source: [batch, num_tokens, num_heads, head_size]
48
+ * Dest: [batch, total_tokens, heads_per_rank, head_size]
49
+ *
50
+ * Each GPU writes heads [target_rank * heads_per_rank : (target_rank+1) * heads_per_rank]
51
+ * to target_rank's buffer at token offset prefix_rank_tokens[rank].
52
+ *
53
+ * ## Memory Layout
54
+ *
55
+ * Input tensor x (row-major, contiguous):
56
+ * - Batch dimension: outermost
57
+ * - Token dimension: batch_stride = num_tokens * num_heads * head_size
58
+ * - Head dimension: token_stride = num_heads * head_size
59
+ * - Head element: head_stride = head_size
60
+ *
61
+ * Output buffer (per target rank):
62
+ * - Similar layout but with heads_per_rank instead of num_heads
63
+ * - Tokens from this rank placed at offset prefix_rank_tokens[rank]
64
+ *
65
+ * ## Thread Block Organization
66
+ *
67
+ * Each thread block handles multiple tokens cooperatively:
68
+ * - Threads are organized in a 2D logical grid (rows=tokens, cols=elements)
69
+ * - Each thread copies 16 bytes (int4) per iteration
70
+ * - num_threads_per_token = (heads_per_rank * head_size) / elements_per_thread
71
+ * - num_tokens_per_copy = num_threads / num_threads_per_token
72
+ *
73
+ * @tparam ELEM_T Element type (at::BFloat16 or at::Float8_e4m3fn)
74
+ * @param buffer_ptrs Device array of pointers to each rank's data buffer
75
+ * @param barrier_signal_ptrs Device array of pointers to each rank's barrier signals
76
+ * @param x Source tensor data pointer
77
+ * @param rank This GPU's rank
78
+ * @param world_size Total number of GPUs
79
+ * @param batch_size Number of batches
80
+ * @param num_tokens Number of tokens on this rank
81
+ * @param num_heads Total number of attention heads
82
+ * @param head_size Size of each attention head
83
+ * @param total_tokens Sum of tokens across all ranks
84
+ * @param prefix_rank_tokens Cumulative token counts for offset calculation
85
+ */
86
+ template <typename ELEM_T>
87
+ __global__ void send_recv_all2all(void **buffer_ptrs, int **barrier_signal_ptrs, void *x, int rank, int world_size,
88
+ int batch_size, int num_tokens, int num_heads, int head_size, int total_tokens,
89
+ int *prefix_rank_tokens) {
90
+ // Grid dimensions
91
+ int num_sms = gridDim.x;
92
+ int sm_id = blockIdx.x;
93
+ int num_threads = blockDim.x;
94
+
95
+ // === SM Work Distribution (Round-Robin) ===
96
+ // Use modular assignment to handle num_sms not divisible by world_size.
97
+ // This ensures all SMs are utilized: some ranks get ceil(num_sms/world_size)
98
+ // SMs, others get floor(num_sms/world_size) SMs.
99
+ int64_t target_rank = get_target_rank(sm_id, world_size);
100
+ int64_t rank_local_sm_id = get_rank_local_sm_id(sm_id, world_size);
101
+ int64_t num_sms_for_this_rank = get_num_sms_for_rank(target_rank, num_sms, world_size);
102
+
103
+ // === Head Assignment ===
104
+ // Heads are partitioned evenly: rank i gets heads [i*hpr : (i+1)*hpr]
105
+ int64_t heads_per_rank = num_heads / world_size;
106
+ int64_t head_id = target_rank * heads_per_rank; // Starting head for target rank
107
+
108
+ // === Thread Mapping ===
109
+ // Each thread copies an int4 (16 bytes) per memory operation
110
+ // Threads form a 2D grid: (tokens_per_copy, threads_per_token)
111
+ int64_t num_elems_per_thread = sizeof(int4) / sizeof(ELEM_T);
112
+ int64_t num_threads_per_token = heads_per_rank * head_size / num_elems_per_thread;
113
+ int64_t num_tokens_per_copy = num_threads / num_threads_per_token;
114
+
115
+ // 2D thread coordinates within the logical grid
116
+ int64_t copy_thr_col_idx = threadIdx.x % num_threads_per_token; // Element offset
117
+ int64_t copy_thr_row_idx = threadIdx.x / num_threads_per_token; // Token offset
118
+
119
+ // Get target rank's buffer pointer
120
+ auto ptr = reinterpret_cast<void *>(static_cast<int8_t *>(buffer_ptrs[target_rank]));
121
+
122
+ // Use 64-bit arithmetic to avoid overflow for large tensors
123
+ int64_t num_tokens_64b = int64_t(num_tokens);
124
+ int64_t num_heads_64b = int64_t(num_heads);
125
+ int64_t head_size_64b = int64_t(head_size);
126
+
127
+ // === Main Copy Loop ===
128
+ // Iterate over batches and tokens, with SMs in the same group
129
+ // working on different token ranges in strided fashion
130
+ for (int64_t batch_ind = 0; batch_ind < batch_size; batch_ind++) {
131
+ // Strided token iteration: each SM in the group handles different token ranges
132
+ for (int64_t token_idx = rank_local_sm_id * num_tokens_per_copy; token_idx < num_tokens;
133
+ token_idx += num_tokens_per_copy * num_sms_for_this_rank) {
134
+ int64_t copy_token_idx = token_idx + copy_thr_row_idx;
135
+ // Destination token index accounts for this rank's offset in the global sequence
136
+ int64_t dst_token_idx = prefix_rank_tokens[rank] + copy_token_idx;
137
+
138
+ if (copy_token_idx >= num_tokens)
139
+ break;
140
+
141
+ // === Pointer Arithmetic ===
142
+ // Source: Read from this rank's input tensor at [batch, token, head_id:head_id+hpr, :]
143
+ // Note: We read a contiguous chunk of heads starting at head_id
144
+ int4 *shuffled_x_ptr =
145
+ reinterpret_cast<int4 *>(reinterpret_cast<uint8_t *>(x) +
146
+ batch_ind * num_tokens_64b * num_heads_64b * head_size_64b * sizeof(ELEM_T) +
147
+ copy_token_idx * num_heads_64b * head_size_64b * sizeof(ELEM_T) +
148
+ head_id * head_size_64b * sizeof(ELEM_T)) +
149
+ copy_thr_col_idx;
150
+
151
+ // Destination: Write to target rank's buffer at [batch, dst_token, :, :]
152
+ // The buffer has layout [batch, total_tokens, heads_per_rank, head_size]
153
+ int4 *shuffled_buffer_ptr =
154
+ reinterpret_cast<int4 *>(reinterpret_cast<uint8_t *>(ptr) +
155
+ batch_ind * total_tokens * heads_per_rank * head_size_64b * sizeof(ELEM_T) +
156
+ dst_token_idx * heads_per_rank * head_size_64b * sizeof(ELEM_T)) +
157
+ copy_thr_col_idx;
158
+
159
+ // Non-allocating store to avoid polluting L1 cache
160
+ st_na_global(shuffled_buffer_ptr, __ldg(shuffled_x_ptr));
161
+ }
162
+ }
163
+
164
+ // === Barrier Synchronization ===
165
+ // Signal completion to target rank and wait for all ranks to finish
166
+ barrier_wait_and_reset_roundrobin(barrier_signal_ptrs, target_rank, rank, world_size, num_sms, sm_id, threadIdx.x,
167
+ NUM_TIMEOUT_CYCLES);
168
+ }
169
+
170
+ /**
171
+ * @brief All2All kernel for gathering attention heads back to original distribution.
172
+ *
173
+ * This kernel performs the inverse of send_recv_all2all: it gathers heads from
174
+ * all ranks back to reconstruct the original tensor layout. Each GPU reads from
175
+ * its local buffer and writes its portion of heads to all target ranks.
176
+ *
177
+ * Data layout transformation:
178
+ * Source: [batch, total_tokens, heads_per_rank, head_size] (per GPU)
179
+ * Dest: [batch, rank_tokens[target], num_heads, head_size] (per target GPU)
180
+ *
181
+ * ## Memory Layout
182
+ *
183
+ * Input tensor x (this rank's portion after send_recv_all2all):
184
+ * - Contains all tokens but only heads_per_rank heads
185
+ * - Layout: [batch, total_tokens, heads_per_rank, head_size]
186
+ *
187
+ * Output buffer (per target rank):
188
+ * - Contains only that rank's tokens but all heads
189
+ * - Layout: [batch, rank_tokens[target], num_heads, head_size]
190
+ * - This rank writes heads [rank * heads_per_rank : (rank+1) * heads_per_rank]
191
+ *
192
+ * @tparam ELEM_T Element type (at::BFloat16 or at::Float8_e4m3fn)
193
+ * @param buffer_ptrs Device array of pointers to each rank's data buffer
194
+ * @param barrier_signal_ptrs Device array of pointers to barrier signals
195
+ * @param x Source tensor data (this rank's buffer after send_recv)
196
+ * @param rank This GPU's rank
197
+ * @param world_size Total number of GPUs
198
+ * @param batch_size Number of batches
199
+ * @param num_heads Total number of heads (reconstructed)
200
+ * @param head_size Size of each attention head
201
+ * @param rank_tokens Number of tokens for each rank
202
+ * @param total_tokens Sum of tokens across all ranks
203
+ * @param prefix_rank_tokens Cumulative token counts for offset calculation
204
+ */
205
+ template <typename ELEM_T>
206
+ __global__ void gather_heads(void **buffer_ptrs, int **barrier_signal_ptrs, void *x, int rank, int world_size,
207
+ int batch_size, int num_heads, int head_size, const int *__restrict__ rank_tokens,
208
+ int total_tokens, int *prefix_rank_tokens) {
209
+ // Grid dimensions
210
+ int num_sms = gridDim.x;
211
+ int sm_id = blockIdx.x;
212
+ int num_threads = blockDim.x;
213
+
214
+ // === SM Work Distribution (Round-Robin) ===
215
+ // Same partitioning as send_recv_all2all
216
+ int64_t target_rank = get_target_rank(sm_id, world_size);
217
+ int64_t rank_local_sm_id = get_rank_local_sm_id(sm_id, world_size);
218
+ int64_t num_sms_for_this_rank = get_num_sms_for_rank(target_rank, num_sms, world_size);
219
+ int64_t heads_per_rank = num_heads / world_size;
220
+
221
+ // === Thread Mapping ===
222
+ int64_t num_elems_per_thread = sizeof(int4) / sizeof(ELEM_T);
223
+ int64_t num_threads_per_token = heads_per_rank * head_size / num_elems_per_thread;
224
+ int64_t num_tokens_per_copy = num_threads / num_threads_per_token;
225
+
226
+ int64_t copy_thr_col_idx = threadIdx.x % num_threads_per_token;
227
+ int64_t copy_thr_row_idx = threadIdx.x / num_threads_per_token;
228
+
229
+ // Number of tokens owned by target rank
230
+ const int64_t tgt_tokens = int64_t(rank_tokens[target_rank]);
231
+
232
+ // This rank writes its heads at offset [rank * heads_per_rank] in the output
233
+ int64_t head_idx = rank * heads_per_rank;
234
+ int64_t num_heads_64b = int64_t(num_heads);
235
+ int64_t head_size_64b = int64_t(head_size);
236
+ int64_t total_tokens_64b = int64_t(total_tokens);
237
+
238
+ // Get target rank's buffer pointer
239
+ auto ptr = reinterpret_cast<void *>(static_cast<int8_t *>(buffer_ptrs[target_rank]));
240
+
241
+ // === Main Copy Loop ===
242
+ // Process target rank's tokens: read from global position, write to local position
243
+ for (int64_t batch_idx = 0; batch_idx < batch_size; batch_idx++) {
244
+ for (int64_t token_idx = rank_local_sm_id * num_tokens_per_copy; token_idx < tgt_tokens;
245
+ token_idx += num_tokens_per_copy * num_sms_for_this_rank) {
246
+ int64_t copy_token = token_idx + copy_thr_row_idx;
247
+ if (copy_token >= tgt_tokens)
248
+ break;
249
+
250
+ // Source: Read from global token position (target rank's tokens in our buffer)
251
+ int64_t src_token_idx = prefix_rank_tokens[target_rank] + copy_token;
252
+ // Destination: Write to local token position in target's buffer
253
+ int64_t dst_token_idx = copy_token;
254
+
255
+ // Source pointer: our input tensor at [batch, src_token, :, :]
256
+ int4 *shuffled_x_ptr =
257
+ reinterpret_cast<int4 *>(reinterpret_cast<uint8_t *>(x) +
258
+ batch_idx * total_tokens_64b * heads_per_rank * head_size_64b * sizeof(ELEM_T) +
259
+ src_token_idx * heads_per_rank * head_size_64b * sizeof(ELEM_T)) +
260
+ copy_thr_col_idx;
261
+
262
+ // Destination pointer: target's buffer at [batch, dst_token, head_idx:head_idx+hpr, :]
263
+ int4 *shuffled_buffer_ptr =
264
+ reinterpret_cast<int4 *>(reinterpret_cast<uint8_t *>(ptr) +
265
+ batch_idx * tgt_tokens * num_heads_64b * head_size_64b * sizeof(ELEM_T) +
266
+ dst_token_idx * num_heads_64b * head_size_64b * sizeof(ELEM_T) +
267
+ head_idx * head_size_64b * sizeof(ELEM_T)) +
268
+ copy_thr_col_idx;
269
+
270
+ st_na_global(shuffled_buffer_ptr, __ldg(shuffled_x_ptr));
271
+ }
272
+ }
273
+
274
+ // === Barrier Synchronization ===
275
+ barrier_wait_and_reset_roundrobin(barrier_signal_ptrs, target_rank, rank, world_size, num_sms, sm_id, threadIdx.x,
276
+ NUM_TIMEOUT_CYCLES);
277
+ }
278
+
279
+ /**
280
+ * @brief Host function to launch the gather_heads kernel.
281
+ *
282
+ * Selects the appropriate template instantiation based on tensor data type
283
+ * and launches the kernel with the specified number of SMs.
284
+ *
285
+ * @param buffer_ptrs Device array of buffer pointers
286
+ * @param barrier_signal_ptrs Device array of barrier signal pointers
287
+ * @param x Input tensor data pointer
288
+ * @param rank_tokens Token count per rank (device memory)
289
+ * @param prefix_rank_tokens Cumulative token counts (device memory)
290
+ * @param rank This GPU's rank
291
+ * @param world_size Total number of GPUs
292
+ * @param batch_size Number of batches
293
+ * @param total_tokens Sum of tokens across all ranks
294
+ * @param num_heads Total number of attention heads
295
+ * @param head_size Size of each attention head
296
+ * @param stream CUDA stream for async execution
297
+ * @param num_sms Number of SMs to launch
298
+ * @param tensor_dtype Data type (BFloat16 or Float8_e4m3fn)
299
+ */
300
+ void all2all_head_gather_launch(void **buffer_ptrs, int **barrier_signal_ptrs, void *x, const int *rank_tokens,
301
+ int *prefix_rank_tokens, int rank, int world_size, int batch_size, int total_tokens,
302
+ int num_heads, int head_size, cudaStream_t stream, int num_sms,
303
+ at::ScalarType tensor_dtype) {
304
+ do {
305
+ if (tensor_dtype == at::ScalarType::BFloat16) {
306
+ gather_heads<at::BFloat16><<<num_sms, DEFAULT_KERNEL_THREADS, 0, stream>>>(
307
+ buffer_ptrs, barrier_signal_ptrs, x, rank, world_size, batch_size, num_heads, head_size, rank_tokens,
308
+ total_tokens, prefix_rank_tokens);
309
+ } else if (tensor_dtype == at::ScalarType::Float8_e4m3fn) {
310
+ gather_heads<at::Float8_e4m3fn><<<num_sms, DEFAULT_KERNEL_THREADS, 0, stream>>>(
311
+ buffer_ptrs, barrier_signal_ptrs, x, rank, world_size, batch_size, num_heads, head_size, rank_tokens,
312
+ total_tokens, prefix_rank_tokens);
313
+ }
314
+
315
+ // Check for kernel launch errors
316
+ cudaError_t e = cudaGetLastError();
317
+ if (e != cudaSuccess) {
318
+ EPException cuda_exception("CUDA", __FILE__, __LINE__, cudaGetErrorString(e));
319
+ fprintf(stderr, "%s\n", cuda_exception.what());
320
+ throw cuda_exception;
321
+ }
322
+ } while (0);
323
+ }
324
+
325
+ /**
326
+ * @brief Host function to launch the send_recv_all2all kernel.
327
+ *
328
+ * Selects the appropriate template instantiation based on tensor data type
329
+ * and launches the kernel with the specified number of SMs.
330
+ *
331
+ * @param buffer_ptrs Device array of buffer pointers
332
+ * @param barrier_signal_ptrs Device array of barrier signal pointers
333
+ * @param x Input tensor data pointer
334
+ * @param prefix_rank_tokens Cumulative token counts (device memory)
335
+ * @param rank This GPU's rank
336
+ * @param world_size Total number of GPUs
337
+ * @param batch_size Number of batches
338
+ * @param total_tokens Sum of tokens across all ranks
339
+ * @param num_tokens Number of tokens on this rank
340
+ * @param num_heads Total number of attention heads
341
+ * @param head_size Size of each attention head
342
+ * @param stream CUDA stream for async execution
343
+ * @param num_sms Number of SMs to launch
344
+ * @param tensor_dtype Data type (BFloat16 or Float8_e4m3fn)
345
+ */
346
+ void all2all_head_launch(void **buffer_ptrs, int **barrier_signal_ptrs, void *x, int *prefix_rank_tokens, int rank,
347
+ int world_size, int batch_size, int total_tokens, int num_tokens, int num_heads, int head_size,
348
+ cudaStream_t stream, int num_sms, at::ScalarType tensor_dtype) {
349
+ do {
350
+ if (tensor_dtype == at::ScalarType::BFloat16) {
351
+ send_recv_all2all<at::BFloat16><<<num_sms, DEFAULT_KERNEL_THREADS, 0, stream>>>(
352
+ buffer_ptrs, barrier_signal_ptrs, x, rank, world_size, batch_size, num_tokens, num_heads, head_size,
353
+ total_tokens, prefix_rank_tokens);
354
+ } else if (tensor_dtype == at::ScalarType::Float8_e4m3fn) {
355
+ send_recv_all2all<at::Float8_e4m3fn><<<num_sms, DEFAULT_KERNEL_THREADS, 0, stream>>>(
356
+ buffer_ptrs, barrier_signal_ptrs, x, rank, world_size, batch_size, num_tokens, num_heads, head_size,
357
+ total_tokens, prefix_rank_tokens);
358
+ }
359
+
360
+ // Check for kernel launch errors
361
+ cudaError_t e = cudaGetLastError();
362
+ if (e != cudaSuccess) {
363
+ EPException cuda_exception("CUDA", __FILE__, __LINE__, cudaGetErrorString(e));
364
+ fprintf(stderr, "%s\n", cuda_exception.what());
365
+ throw cuda_exception;
366
+ }
367
+ } while (0);
368
+ }
369
+
370
+ } // namespace all2all_cuda
371
+ } // namespace all2all
372
+ } // namespace ltx_kernels
ltx-2-internal/internal/ltx-kernels/csrc/all2all/cuda/allgather.cu ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @file allgather.cu
3
+ * @brief CUDA kernel for AllGather operation using IPC-based direct memory access.
4
+ *
5
+ * This file implements the GPU kernel for gathering sequence tokens from all GPUs
6
+ * into a complete sequence on each GPU. Unlike the head redistribution kernels,
7
+ * this kernel preserves the head dimension and only gathers across the token
8
+ * (sequence) dimension.
9
+ *
10
+ * ## Algorithm Overview
11
+ *
12
+ * Each GPU broadcasts its local tokens to all other GPUs' buffers:
13
+ * - GPU i writes its tokens to position [prefix_rank_tokens[i]] in each buffer
14
+ * - After completion, all buffers contain the full sequence [0:total_tokens]
15
+ *
16
+ * ## Use Case
17
+ *
18
+ * This is typically used after tensor-parallel computation to reconstruct the
19
+ * full sequence for operations that require global context (e.g., output projection).
20
+ */
21
+
22
+ #include "cuda/configs.cuh"
23
+ #include "cuda/exceptions.cuh"
24
+ #include "cuda/utils.cuh"
25
+ #include <ATen/cuda/CUDADataType.h>
26
+
27
+ namespace ltx_kernels {
28
+ namespace all2all {
29
+ namespace all2all_cuda {
30
+
31
+ /**
32
+ * @brief AllGather kernel to collect sequence tokens from all ranks.
33
+ *
34
+ * Each GPU writes its local sequence tokens to all other GPUs' buffers at the
35
+ * appropriate offset. After synchronization, all GPUs have the complete sequence.
36
+ *
37
+ * Data layout transformation:
38
+ * Input per GPU: [batch, seqlen, hidden_dim]
39
+ * Output per GPU: [batch, total_tokens, hidden_dim] (identical on all GPUs)
40
+ *
41
+ * ## Memory Layout
42
+ *
43
+ * Input tensor x (contiguous):
44
+ * - Shape: [batch, seqlen, hidden_dim]
45
+ * - hidden_dim = num_heads * head_size (flattened)
46
+ *
47
+ * Output buffer (per target rank, after gather):
48
+ * - Shape: [batch, total_tokens, hidden_dim]
49
+ * - This rank's tokens placed at offset rank_tokens_prefix[rank]
50
+ *
51
+ * ## Thread Mapping
52
+ *
53
+ * Similar to all2all_heads, threads cooperate to copy tokens:
54
+ * - Each thread copies 16 bytes (int4)
55
+ * - Threads per token = hidden_dim * sizeof(ELEM_T) / sizeof(int4)
56
+ * - Multiple tokens processed per thread block
57
+ *
58
+ * @tparam ELEM_T Element type (__nv_bfloat16 or at::Float8_e4m3fn)
59
+ * @param x Source tensor data pointer (this rank's tokens)
60
+ * @param buffer_ptrs Device array of pointers to each rank's data buffer
61
+ * @param barrier_signal_ptrs Device array of pointers to barrier signals
62
+ * @param batch_size Number of batches
63
+ * @param seqlen Number of tokens on this rank
64
+ * @param hidden_dim Hidden dimension size (num_heads * head_size)
65
+ * @param world_size Total number of GPUs
66
+ * @param rank This GPU's rank
67
+ * @param total_tokens Sum of tokens across all ranks
68
+ * @param rank_tokens_prefix Cumulative token counts (device memory)
69
+ */
70
+ template <typename ELEM_T>
71
+ __global__ void allgather(void *x, void **buffer_ptrs, int **barrier_signal_ptrs, int batch_size, int seqlen,
72
+ int hidden_dim, int world_size, int rank, int total_tokens, int *rank_tokens_prefix) {
73
+
74
+ // Grid dimensions
75
+ int num_sms = gridDim.x;
76
+ int sm_id = blockIdx.x;
77
+ int num_threads = blockDim.x;
78
+
79
+ // === SM Work Distribution (Round-Robin) ===
80
+ // Use modular assignment to handle num_sms not divisible by world_size.
81
+ // This ensures all SMs are utilized: some ranks get ceil(num_sms/world_size)
82
+ // SMs, others get floor(num_sms/world_size) SMs.
83
+ int tgt_rank = get_target_rank(sm_id, world_size);
84
+ int rank_local_sm_id = get_rank_local_sm_id(sm_id, world_size);
85
+ int num_sms_for_this_rank = get_num_sms_for_rank(tgt_rank, num_sms, world_size);
86
+
87
+ // Get target rank's buffer pointer
88
+ auto ptr = reinterpret_cast<void *>(static_cast<int8_t *>(buffer_ptrs[tgt_rank]));
89
+
90
+ // === Thread Mapping ===
91
+ // Each thread copies one int4 (16 bytes)
92
+ int64_t num_elems_per_thread = sizeof(int4) / sizeof(ELEM_T);
93
+ int64_t num_threads_per_token = hidden_dim / num_elems_per_thread;
94
+ int64_t num_tokens_per_copy = num_threads / num_threads_per_token;
95
+
96
+ // 2D thread coordinates
97
+ int64_t copy_thr_col_idx = threadIdx.x % num_threads_per_token; // Element offset
98
+ int64_t copy_thr_row_idx = threadIdx.x / num_threads_per_token; // Token offset
99
+
100
+ // Use 64-bit arithmetic to avoid overflow
101
+ int64_t hidden_dim_64b = int64_t(hidden_dim);
102
+ int64_t total_tokens_64b = int64_t(total_tokens);
103
+ int64_t seqlen_64b = int64_t(seqlen);
104
+
105
+ // === Main Copy Loop ===
106
+ // Broadcast this rank's tokens to all target ranks' buffers
107
+ for (int64_t batch_idx = 0; batch_idx < batch_size; batch_idx++) {
108
+ // Strided token iteration within SM group for this target rank
109
+ for (int64_t token_idx = rank_local_sm_id * num_tokens_per_copy; token_idx < seqlen;
110
+ token_idx += num_tokens_per_copy * num_sms_for_this_rank) {
111
+ int64_t copy_token = token_idx + copy_thr_row_idx;
112
+ if (copy_token >= seqlen)
113
+ break;
114
+
115
+ // Source: local token index in input tensor
116
+ int64_t src_token_idx = copy_token;
117
+ // Destination: global token index in output buffer
118
+ // This rank's tokens start at prefix_rank_tokens[rank]
119
+ int64_t dst_token_idx = copy_token + rank_tokens_prefix[rank];
120
+
121
+ // Source pointer: input tensor at [batch, src_token, :]
122
+ int4 *shuffled_x_ptr = reinterpret_cast<int4 *>(reinterpret_cast<uint8_t *>(x) +
123
+ batch_idx * seqlen_64b * hidden_dim_64b * sizeof(ELEM_T) +
124
+ src_token_idx * hidden_dim_64b * sizeof(ELEM_T)) +
125
+ copy_thr_col_idx;
126
+
127
+ // Destination pointer: target buffer at [batch, dst_token, :]
128
+ int4 *shuffled_buffer_ptr =
129
+ reinterpret_cast<int4 *>(reinterpret_cast<uint8_t *>(ptr) +
130
+ batch_idx * total_tokens_64b * hidden_dim_64b * sizeof(ELEM_T) +
131
+ dst_token_idx * hidden_dim_64b * sizeof(ELEM_T)) +
132
+ copy_thr_col_idx;
133
+
134
+ // Non-allocating store for better cache behavior
135
+ st_na_global(shuffled_buffer_ptr, __ldg(shuffled_x_ptr));
136
+ }
137
+ }
138
+
139
+ // === Barrier Synchronization ===
140
+ // Signal completion to target rank and wait for all ranks
141
+ // Use round-robin variant since SM counts per rank may differ
142
+ barrier_wait_and_reset_roundrobin(barrier_signal_ptrs, tgt_rank, rank, world_size, num_sms, sm_id, threadIdx.x,
143
+ NUM_TIMEOUT_CYCLES);
144
+ }
145
+
146
+ /**
147
+ * @brief Host function to launch the allgather kernel.
148
+ *
149
+ * Launches the AllGather kernel with the specified configuration.
150
+ * Uses ALLGATHER_KERNEL_THREADS (1024) threads per block for higher
151
+ * occupancy than the All2All kernels.
152
+ *
153
+ * @param buffer_ptrs Device array of buffer pointers
154
+ * @param barrier_signal_ptrs Device array of barrier signal pointers
155
+ * @param x Input tensor data pointer
156
+ * @param prefix_rank_tokens Cumulative token counts (device memory)
157
+ * @param rank This GPU's rank
158
+ * @param world_size Total number of GPUs
159
+ * @param batch_size Number of batches
160
+ * @param seqlen Number of tokens on this rank
161
+ * @param hidden_dim Hidden dimension size
162
+ * @param total_tokens Sum of tokens across all ranks
163
+ * @param stream CUDA stream for async execution
164
+ * @param num_sms Number of SMs to launch
165
+ * @param tensor_dtype Data type (BFloat16 or Float8_e4m3fn)
166
+ */
167
+ void allgather_launch(void **buffer_ptrs, int **barrier_signal_ptrs, void *x, int *prefix_rank_tokens, int rank,
168
+ int world_size, int batch_size, int seqlen, int hidden_dim, int total_tokens, cudaStream_t stream,
169
+ int num_sms, at::ScalarType tensor_dtype) {
170
+ do {
171
+ if (tensor_dtype == at::ScalarType::BFloat16) {
172
+ allgather<at::BFloat16><<<num_sms, ALLGATHER_KERNEL_THREADS, 0, stream>>>(
173
+ x, buffer_ptrs, barrier_signal_ptrs, batch_size, seqlen, hidden_dim, world_size, rank, total_tokens,
174
+ prefix_rank_tokens);
175
+ } else if (tensor_dtype == at::ScalarType::Float8_e4m3fn) {
176
+ allgather<at::Float8_e4m3fn><<<num_sms, ALLGATHER_KERNEL_THREADS, 0, stream>>>(
177
+ x, buffer_ptrs, barrier_signal_ptrs, batch_size, seqlen, hidden_dim, world_size, rank, total_tokens,
178
+ prefix_rank_tokens);
179
+ } else {
180
+ EPException dtype_exception("allgather_launch", __FILE__, __LINE__, "Unsupported dtype");
181
+ fprintf(stderr, "%s\n", dtype_exception.what());
182
+ throw dtype_exception;
183
+ }
184
+
185
+ // Check for kernel launch errors
186
+ cudaError_t e = cudaGetLastError();
187
+ if (e != cudaSuccess) {
188
+ EPException cuda_exception("CUDA", __FILE__, __LINE__, cudaGetErrorString(e));
189
+ fprintf(stderr, "%s\n", cuda_exception.what());
190
+ throw cuda_exception;
191
+ }
192
+ } while (0);
193
+ }
194
+
195
+ } // namespace all2all_cuda
196
+ } // namespace all2all
197
+ } // namespace ltx_kernels
ltx-2-internal/internal/ltx-kernels/csrc/all2all/cuda/api.cuh ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @file api.cuh
3
+ * @brief CUDA kernel launch function declarations for All2All operations.
4
+ *
5
+ * This header provides the host-callable interface for launching the All2All
6
+ * CUDA kernels. These functions handle template instantiation and kernel
7
+ * configuration based on the tensor data type.
8
+ */
9
+
10
+ #pragma once
11
+
12
+ #include <ATen/cuda/CUDADataType.h>
13
+ #include <vector>
14
+
15
+ namespace ltx_kernels {
16
+ namespace all2all {
17
+ namespace all2all_cuda {
18
+
19
+ /**
20
+ * @brief Launches the All2All head redistribution kernel.
21
+ *
22
+ * Redistributes attention heads across GPUs:
23
+ * Input: [batch, num_tokens, num_heads, head_size] per GPU
24
+ * Output: [batch, total_tokens, num_heads/world_size, head_size] per GPU
25
+ *
26
+ * @param buffer_ptrs Device array of pointers to each rank's data buffer
27
+ * @param barrier_signal_ptrs Device array of pointers to barrier signals
28
+ * @param x Source tensor data pointer
29
+ * @param prefix_rank_tokens Cumulative token counts per rank (device memory)
30
+ * @param rank This GPU's rank (0 to world_size-1)
31
+ * @param world_size Total number of GPUs
32
+ * @param batch_size Batch dimension size
33
+ * @param total_tokens Sum of tokens across all ranks
34
+ * @param num_tokens Number of tokens on this rank
35
+ * @param num_heads Total number of attention heads
36
+ * @param head_size Size of each attention head
37
+ * @param stream CUDA stream for async execution
38
+ * @param num_sms Number of SMs to use for the kernel
39
+ * @param tensor_dtype Data type (BFloat16 or Float8_e4m3fn)
40
+ */
41
+ void all2all_head_launch(void **buffer_ptrs, int **barrier_signal_ptrs, void *x, int *prefix_rank_tokens, int rank,
42
+ int world_size, int batch_size, int total_tokens, int num_tokens, int num_heads, int head_size,
43
+ cudaStream_t stream, int num_sms, at::ScalarType tensor_dtype);
44
+
45
+ /**
46
+ * @brief Launches the gather heads kernel (inverse of all2all_head_launch).
47
+ *
48
+ * Redistributes tokens back to original head distribution:
49
+ * Input: [batch, total_tokens, heads_per_rank, head_size] per GPU
50
+ * Output: [batch, rank_tokens[rank], num_heads, head_size] per GPU
51
+ *
52
+ * @param buffer_ptrs Device array of pointers to each rank's data buffer
53
+ * @param barrier_signal_ptrs Device array of pointers to barrier signals
54
+ * @param x Source tensor data pointer
55
+ * @param rank_tokens Token count for each rank (device memory)
56
+ * @param prefix_rank_tokens Cumulative token counts (device memory)
57
+ * @param rank This GPU's rank
58
+ * @param world_size Total number of GPUs
59
+ * @param batch_size Batch dimension size
60
+ * @param total_tokens Sum of tokens across all ranks
61
+ * @param num_heads Total number of attention heads (reconstructed)
62
+ * @param head_size Size of each attention head
63
+ * @param stream CUDA stream for async execution
64
+ * @param num_sms Number of SMs to use for the kernel
65
+ * @param tensor_dtype Data type (BFloat16 or Float8_e4m3fn)
66
+ */
67
+ void all2all_head_gather_launch(void **buffer_ptrs, int **barrier_signal_ptrs, void *x, const int *rank_tokens,
68
+ int *prefix_rank_tokens, int rank, int world_size, int batch_size, int total_tokens,
69
+ int num_heads, int head_size, cudaStream_t stream, int num_sms,
70
+ at::ScalarType tensor_dtype);
71
+
72
+ /**
73
+ * @brief Launches the AllGather kernel for sequence tokens.
74
+ *
75
+ * Gathers sequence tokens from all ranks:
76
+ * Input: [batch, seqlen, hidden_dim] per GPU
77
+ * Output: [batch, total_tokens, hidden_dim] per GPU (identical on all)
78
+ *
79
+ * @param buffer_ptrs Device array of pointers to each rank's data buffer
80
+ * @param barrier_signal_ptrs Device array of pointers to barrier signals
81
+ * @param x Source tensor data pointer
82
+ * @param prefix_rank_tokens Cumulative token counts (device memory)
83
+ * @param rank This GPU's rank
84
+ * @param world_size Total number of GPUs
85
+ * @param batch_size Batch dimension size
86
+ * @param seqlen Number of tokens on this rank
87
+ * @param hidden_dim Hidden dimension size (num_heads * head_size)
88
+ * @param total_tokens Sum of tokens across all ranks
89
+ * @param stream CUDA stream for async execution
90
+ * @param num_sms Number of SMs to use for the kernel
91
+ * @param tensor_dtype Data type (BFloat16 or Float8_e4m3fn)
92
+ */
93
+ void allgather_launch(void **buffer_ptrs, int **barrier_signal_ptrs, void *x, int *prefix_rank_tokens, int rank,
94
+ int world_size, int batch_size, int seqlen, int hidden_dim, int total_tokens, cudaStream_t stream,
95
+ int num_sms, at::ScalarType tensor_dtype);
96
+
97
+ } // namespace all2all_cuda
98
+ } // namespace all2all
99
+ } // namespace ltx_kernels
ltx-2-internal/internal/ltx-kernels/csrc/include/cuda/configs.cuh ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @file configs.cuh
3
+ * @brief Configuration constants and compile-time settings for ltx-kernels.
4
+ *
5
+ * This header defines the tunable parameters and constants used throughout
6
+ * the ltx-kernels communication library. These values are chosen to balance
7
+ * performance across different GPU architectures.
8
+ */
9
+
10
+ #pragma once
11
+
12
+ #include <cstdint>
13
+ #include <cuda_bf16.h>
14
+ #include <cuda_runtime.h>
15
+
16
+ namespace ltx_kernels {
17
+ // =============================================================================
18
+ // Synchronization Configuration
19
+ // =============================================================================
20
+
21
+ /**
22
+ * @brief Timeout for barrier synchronization in GPU clock cycles.
23
+ *
24
+ * If a barrier wait exceeds this timeout, the kernel will trap to indicate
25
+ * a deadlock or communication failure. The value ~25 billion cycles corresponds
26
+ * to approximately 10-100 seconds depending on GPU clock speed.
27
+ *
28
+ * Example: At 2.5 GHz boost clock, 25B cycles ≈ 10 seconds.
29
+ */
30
+ constexpr uint64_t NUM_TIMEOUT_CYCLES = 25000000000ull;
31
+
32
+ // =============================================================================
33
+ // Hardware Limits
34
+ // =============================================================================
35
+
36
+ /**
37
+ * @brief Maximum number of peer GPUs supported for IPC communication.
38
+ *
39
+ * This limits the size of static arrays for buffer pointers and barrier signals.
40
+ * Set to 8 to support up to 8-way tensor parallelism (common for DGX systems).
41
+ */
42
+ constexpr int MAX_NUM_PEERS = 8;
43
+
44
+ // =============================================================================
45
+ // Kernel Configuration
46
+ // =============================================================================
47
+
48
+ /**
49
+ * @brief Default number of threads per block for All2All kernels.
50
+ *
51
+ * Used by send_recv_all2all and gather_heads kernels. The value 512 provides
52
+ * good occupancy while leaving registers for complex pointer arithmetic.
53
+ */
54
+ constexpr int DEFAULT_KERNEL_THREADS = 512;
55
+
56
+ /**
57
+ * @brief Number of threads per block for the AllGather kernel.
58
+ *
59
+ * AllGather uses more threads (1024) because its memory access pattern
60
+ * is simpler (no head selection), allowing higher thread-level parallelism.
61
+ */
62
+ constexpr int ALLGATHER_KERNEL_THREADS = 1024;
63
+
64
+ } // namespace ltx_kernels
65
+
66
+ // =============================================================================
67
+ // Torch/CUDA Compatibility Fixes
68
+ // =============================================================================
69
+
70
+ /*
71
+ * PyTorch sometimes disables CUDA half/bfloat16 operators and conversions
72
+ * to avoid ambiguity in template resolution. We re-enable them here since
73
+ * our kernels explicitly handle these types.
74
+ */
75
+
76
+ #ifdef __CUDA_NO_HALF_CONVERSIONS__
77
+ #undef __CUDA_NO_HALF_CONVERSIONS__
78
+ #endif
79
+ #ifdef __CUDA_NO_HALF_OPERATORS__
80
+ #undef __CUDA_NO_HALF_OPERATORS__
81
+ #endif
82
+ #ifdef __CUDA_NO_HALF2_OPERATORS__
83
+ #undef __CUDA_NO_HALF2_OPERATORS__
84
+ #endif
85
+ #ifdef __CUDA_NO_BFLOAT16_CONVERSIONS__
86
+ #undef __CUDA_NO_BFLOAT16_CONVERSIONS__
87
+ #endif
88
+ #ifdef __CUDA_NO_BFLOAT162_OPERATORS__
89
+ #undef __CUDA_NO_BFLOAT162_OPERATORS__
90
+ #endif
ltx-2-internal/internal/ltx-kernels/csrc/include/cuda/exceptions.cuh ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @file exceptions.cuh
3
+ * @brief Exception handling and assertion macros for CUDA/C++ code.
4
+ *
5
+ * This header provides a unified exception type and assertion macros for
6
+ * both host and device code. The macros capture file and line information
7
+ * for easier debugging of errors.
8
+ *
9
+ * ## Usage Examples
10
+ *
11
+ * ```cpp
12
+ * // Check CUDA API call
13
+ * CUDA_CHECK(cudaMalloc(&ptr, size));
14
+ *
15
+ * // Host-side assertion
16
+ * EP_HOST_ASSERT(tensor.is_contiguous());
17
+ *
18
+ * // Device-side assertion (inside kernel)
19
+ * EP_DEVICE_ASSERT(threadIdx.x < MAX_THREADS);
20
+ *
21
+ * // Compile-time assertion
22
+ * EP_STATIC_ASSERT(sizeof(int4) == 16, "int4 must be 16 bytes");
23
+ * ```
24
+ */
25
+
26
+ #pragma once
27
+
28
+ #include <exception>
29
+ #include <string>
30
+
31
+ #include "configs.cuh"
32
+
33
+ // =============================================================================
34
+ // Static Assertions
35
+ // =============================================================================
36
+
37
+ /**
38
+ * @brief Compile-time assertion macro.
39
+ *
40
+ * @param cond Condition that must be true at compile time
41
+ * @param reason Human-readable error message if condition fails
42
+ */
43
+ #ifndef EP_STATIC_ASSERT
44
+ #define EP_STATIC_ASSERT(cond, reason) static_assert(cond, reason)
45
+ #endif
46
+
47
+ // =============================================================================
48
+ // Exception Type
49
+ // =============================================================================
50
+
51
+ /**
52
+ * @class EPException
53
+ * @brief Custom exception type with file/line information.
54
+ *
55
+ * EPException captures the location (file, line) and context (name, error)
56
+ * of the error for debugging. It inherits from std::exception for
57
+ * compatibility with standard C++ exception handling.
58
+ *
59
+ * ## Message Format
60
+ *
61
+ * The what() message has the format:
62
+ * "Failed: <name> error <file>:<line> '<error message>'"
63
+ */
64
+ class EPException : public std::exception {
65
+ private:
66
+ std::string message = {}; ///< Formatted error message
67
+
68
+ public:
69
+ /**
70
+ * @brief Constructs an EPException with location and error information.
71
+ *
72
+ * @param name Category of error (e.g., "CUDA", "Assertion")
73
+ * @param file Source file where error occurred (__FILE__)
74
+ * @param line Line number where error occurred (__LINE__)
75
+ * @param error Description of the error
76
+ */
77
+ explicit EPException(const char *name, const char *file, const int line, const std::string &error) {
78
+ message = std::string("Failed: ") + name + " error " + file + ":" + std::to_string(line) + " '" + error + "'";
79
+ }
80
+
81
+ /**
82
+ * @brief Returns the formatted error message.
83
+ * @return C-string containing the error message
84
+ */
85
+ const char *what() const noexcept override { return message.c_str(); }
86
+ };
87
+
88
+ // =============================================================================
89
+ // Runtime Assertion Macros
90
+ // =============================================================================
91
+
92
+ /**
93
+ * @brief Checks CUDA API return value and throws on error.
94
+ *
95
+ * Use this macro to wrap all CUDA runtime API calls. If the call fails,
96
+ * an EPException is thrown with the CUDA error string.
97
+ *
98
+ * @param cmd CUDA API call expression
99
+ * @throws EPException if the CUDA call returns an error
100
+ *
101
+ * Example:
102
+ * ```cpp
103
+ * CUDA_CHECK(cudaMalloc(&ptr, size));
104
+ * CUDA_CHECK(cudaMemcpy(dst, src, size, cudaMemcpyDeviceToDevice));
105
+ * ```
106
+ */
107
+ #ifndef CUDA_CHECK
108
+ #define CUDA_CHECK(cmd) \
109
+ do { \
110
+ cudaError_t e = (cmd); \
111
+ if (e != cudaSuccess) { \
112
+ throw EPException("CUDA", __FILE__, __LINE__, cudaGetErrorString(e)); \
113
+ } \
114
+ } while (0)
115
+ #endif
116
+
117
+ /**
118
+ * @brief Host-side assertion that throws on failure.
119
+ *
120
+ * Use this for runtime checks in host code. If the condition is false,
121
+ * an EPException is thrown with the condition as the error message.
122
+ *
123
+ * @param cond Condition to check (must be true)
124
+ * @throws EPException if condition is false
125
+ *
126
+ * Example:
127
+ * ```cpp
128
+ * EP_HOST_ASSERT(tensor.dim() == 4);
129
+ * EP_HOST_ASSERT(rank >= 0 && rank < world_size);
130
+ * ```
131
+ */
132
+ #ifndef EP_HOST_ASSERT
133
+ #define EP_HOST_ASSERT(cond) \
134
+ do { \
135
+ if (not(cond)) { \
136
+ throw EPException("Assertion", __FILE__, __LINE__, #cond); \
137
+ } \
138
+ } while (0)
139
+ #endif
140
+
141
+ /**
142
+ * @brief Device-side assertion that traps on failure.
143
+ *
144
+ * Use this for runtime checks inside CUDA kernels. If the condition is
145
+ * false, prints an error message and executes a trap instruction to
146
+ * halt the GPU.
147
+ *
148
+ * @warning This causes the entire kernel to abort. Use sparingly and
149
+ * consider removing from release builds for performance.
150
+ *
151
+ * @param cond Condition to check (must be true)
152
+ *
153
+ * Example:
154
+ * ```cpp
155
+ * __global__ void my_kernel(int* data, int size) {
156
+ * int idx = threadIdx.x + blockIdx.x * blockDim.x;
157
+ * EP_DEVICE_ASSERT(idx < size);
158
+ * data[idx] = 42;
159
+ * }
160
+ * ```
161
+ */
162
+ #ifndef EP_DEVICE_ASSERT
163
+ #define EP_DEVICE_ASSERT(cond) \
164
+ do { \
165
+ if (not(cond)) { \
166
+ printf("Assertion failed: %s:%d, condition: %s\n", __FILE__, __LINE__, #cond); \
167
+ asm("trap;"); \
168
+ } \
169
+ } while (0)
170
+ #endif
ltx-2-internal/internal/ltx-kernels/csrc/include/cuda/utils.cuh ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @file utils.cuh
3
+ * @brief Low-level CUDA utility functions for memory operations and synchronization.
4
+ *
5
+ * This header provides optimized PTX assembly wrappers for memory operations
6
+ * that bypass cache hierarchy or use specific memory ordering semantics.
7
+ * These are critical for achieving peak bandwidth in multi-GPU communication.
8
+ *
9
+ * ## Memory Operation Types
10
+ *
11
+ * - **Non-allocating stores (st_na)**: Bypass L1 cache to avoid polluting it
12
+ * with data that won't be reused locally
13
+ * - **Non-caching loads (ld_nc)**: Bypass L1 cache for streaming reads
14
+ * - **Acquire/Release**: Memory ordering for synchronization
15
+ * - **System scope (sys)**: Visibility across all GPUs, not just this one
16
+ *
17
+ * ## Cache Hints
18
+ *
19
+ * - L1::no_allocate: Don't allocate in L1 on miss (streaming pattern)
20
+ * - L2::256B: Use 256-byte L2 cache lines
21
+ * - volatile: Bypass all caches, always go to memory
22
+ */
23
+
24
+ #pragma once
25
+ #include <stdint.h>
26
+
27
+ // =============================================================================
28
+ // PTX Instruction Selection
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Store instruction macro. When DISABLE_AGGRESSIVE_PTX_INSTRS is not defined,
33
+ * uses non-allocating stores to avoid polluting L1 cache with write-only data.
34
+ */
35
+ #ifndef DISABLE_AGGRESSIVE_PTX_INSTRS
36
+ #define ST_NA_FUNC "st.global.L1::no_allocate"
37
+ #else
38
+ #define ST_NA_FUNC "st.global"
39
+ #endif
40
+
41
+ /**
42
+ * Load instruction macro. When DISABLE_AGGRESSIVE_PTX_INSTRS is not defined,
43
+ * uses non-caching loads optimized for streaming access patterns.
44
+ */
45
+ #ifndef DISABLE_AGGRESSIVE_PTX_INSTRS
46
+ #define LD_NC_FUNC "ld.global.nc.L1::no_allocate.L2::256B"
47
+ #else
48
+ #define LD_NC_FUNC "ld.volatile.global.L2::256B"
49
+ #endif
50
+
51
+ namespace ltx_kernels {
52
+
53
+ // =============================================================================
54
+ // Round-Robin SM Distribution Helpers
55
+ // =============================================================================
56
+
57
+ /**
58
+ * @brief Compute target rank for a given SM using round-robin distribution.
59
+ *
60
+ * Round-robin assignment ensures all SMs are utilized even when num_sms
61
+ * is not evenly divisible by world_size.
62
+ *
63
+ * @param sm_id The SM/block ID (blockIdx.x)
64
+ * @param world_size Total number of ranks
65
+ * @return Target rank for this SM
66
+ */
67
+ __device__ __forceinline__ int get_target_rank(int sm_id, int world_size) { return sm_id % world_size; }
68
+
69
+ /**
70
+ * @brief Compute local SM index within a rank's SM group.
71
+ *
72
+ * With round-robin, SM i is the (i / world_size)-th SM assigned to its rank.
73
+ *
74
+ * @param sm_id The SM/block ID (blockIdx.x)
75
+ * @param world_size Total number of ranks
76
+ * @return Local index of this SM within its assigned rank's group
77
+ */
78
+ __device__ __forceinline__ int get_rank_local_sm_id(int sm_id, int world_size) { return sm_id / world_size; }
79
+
80
+ /**
81
+ * @brief Compute number of SMs assigned to a specific rank.
82
+ *
83
+ * With round-robin distribution:
84
+ * - Ranks [0, extra) get (base + 1) SMs each
85
+ * - Ranks [extra, world_size) get base SMs each
86
+ * where base = num_sms / world_size, extra = num_sms % world_size
87
+ *
88
+ * @param target_rank The rank to query
89
+ * @param num_sms Total number of SMs launched
90
+ * @param world_size Total number of ranks
91
+ * @return Number of SMs assigned to target_rank
92
+ */
93
+ __device__ __forceinline__ int get_num_sms_for_rank(int target_rank, int num_sms, int world_size) {
94
+ int base_sms = num_sms / world_size;
95
+ int extra_sms = num_sms % world_size;
96
+ return base_sms + (target_rank < extra_sms ? 1 : 0);
97
+ }
98
+
99
+ // =============================================================================
100
+ // Control Flow
101
+ // =============================================================================
102
+
103
+ /**
104
+ * @brief Triggers a GPU trap (fatal error).
105
+ *
106
+ * Used for unrecoverable errors like synchronization timeout.
107
+ * Causes the kernel to abort and report an error to the host.
108
+ */
109
+ __device__ __forceinline__ void trap() { asm("trap;"); }
110
+
111
+ // =============================================================================
112
+ // Memory Ordering Operations (for synchronization)
113
+ // =============================================================================
114
+
115
+ /**
116
+ * @brief System-scope store with release ordering.
117
+ *
118
+ * Ensures all prior memory operations are visible before this store.
119
+ * System scope means visibility across all GPUs (for IPC communication).
120
+ *
121
+ * @param ptr Pointer to global memory
122
+ * @param val Value to store
123
+ */
124
+ __device__ __forceinline__ void st_release_sys_global(const int *ptr, int val) {
125
+ asm volatile("st.release.sys.global.s32 [%0], %1;" ::"l"(ptr), "r"(val) : "memory");
126
+ }
127
+
128
+ /**
129
+ * @brief System-scope store with relaxed ordering.
130
+ *
131
+ * No ordering guarantees - fastest store but requires external synchronization.
132
+ *
133
+ * @param ptr Pointer to global memory
134
+ * @param val Value to store
135
+ */
136
+ __device__ __forceinline__ void st_relaxed_sys_global(const int *ptr, int val) {
137
+ asm volatile("st.relaxed.sys.global.s32 [%0], %1;" ::"l"(ptr), "r"(val) : "memory");
138
+ }
139
+
140
+ /**
141
+ * @brief CTA-scope store with release ordering.
142
+ *
143
+ * Ensures visibility within the thread block (CTA = Cooperative Thread Array).
144
+ *
145
+ * @param ptr Pointer to global memory
146
+ * @param val Value to store
147
+ */
148
+ __device__ __forceinline__ void st_release_cta(const int *ptr, int val) {
149
+ asm volatile("st.release.cta.s32 [%0], %1;" ::"l"(ptr), "r"(val) : "memory");
150
+ }
151
+
152
+ /**
153
+ * @brief System-scope load with acquire ordering (32-bit).
154
+ *
155
+ * Ensures subsequent memory operations are ordered after this load.
156
+ * System scope for IPC visibility across GPUs.
157
+ *
158
+ * @param ptr Pointer to global memory
159
+ * @return Loaded value
160
+ */
161
+ __device__ __forceinline__ int ld_acquire_sys_global(const int *ptr) {
162
+ int ret;
163
+ asm volatile("ld.acquire.sys.global.s32 %0, [%1];" : "=r"(ret) : "l"(ptr));
164
+ return ret;
165
+ }
166
+
167
+ /**
168
+ * @brief System-scope load with acquire ordering (64-bit).
169
+ *
170
+ * @param ptr Pointer to global memory
171
+ * @return Loaded value
172
+ */
173
+ __device__ __forceinline__ uint64_t ld_acquire_sys_global(const uint64_t *ptr) {
174
+ uint64_t ret;
175
+ asm volatile("ld.acquire.sys.global.u64 %0, [%1];" : "=l"(ret) : "l"(ptr));
176
+ return ret;
177
+ }
178
+
179
+ /**
180
+ * @brief GPU-scope load with acquire ordering.
181
+ *
182
+ * Visibility limited to this GPU (not for IPC).
183
+ *
184
+ * @param ptr Pointer to global memory
185
+ * @return Loaded value
186
+ */
187
+ __device__ __forceinline__ int ld_acquire_global(const int *ptr) {
188
+ int ret;
189
+ asm volatile("ld.acquire.gpu.global.s32 %0, [%1];" : "=r"(ret) : "l"(ptr));
190
+ return ret;
191
+ }
192
+
193
+ /**
194
+ * @brief Volatile load bypassing all caches.
195
+ *
196
+ * Always reads from memory, never from cache. Used for polling
197
+ * synchronization variables that may be updated by other GPUs.
198
+ *
199
+ * @param ptr Pointer to global memory
200
+ * @return Loaded value
201
+ */
202
+ __device__ __forceinline__ int ld_volatile_global(const int *ptr) {
203
+ int ret;
204
+ asm volatile("ld.volatile.global.s32 %0, [%1];" : "=r"(ret) : "l"(ptr));
205
+ return ret;
206
+ }
207
+
208
+ // =============================================================================
209
+ // Optimized Bulk Memory Operations
210
+ // =============================================================================
211
+
212
+ /**
213
+ * @brief Non-allocating 128-bit store.
214
+ *
215
+ * Stores an int4 (128 bits / 16 bytes) without allocating in L1 cache.
216
+ * Optimal for write-streaming patterns where data won't be read locally.
217
+ *
218
+ * @param ptr Destination pointer (must be 16-byte aligned)
219
+ * @param value Data to store
220
+ */
221
+ __device__ __forceinline__ void st_na_global(const int4 *ptr, const int4 &value) {
222
+ asm volatile(ST_NA_FUNC ".v4.s32 [%0], {%1, %2, %3, %4};" ::"l"(ptr), "r"(value.x), "r"(value.y), "r"(value.z),
223
+ "r"(value.w));
224
+ }
225
+
226
+ /**
227
+ * @brief Non-caching 128-bit load.
228
+ *
229
+ * Loads an int4 bypassing L1 cache with optimized L2 caching (256B lines).
230
+ * Optimal for read-streaming patterns.
231
+ *
232
+ * @param ptr Source pointer (must be 16-byte aligned)
233
+ * @return Loaded int4 value
234
+ */
235
+ __device__ __forceinline__ int4 ld_nc_global(const int4 *ptr) {
236
+ int4 ret;
237
+ asm volatile(LD_NC_FUNC ".v4.s32 {%0, %1, %2, %3}, [%4];"
238
+ : "=r"(ret.x), "=r"(ret.y), "=r"(ret.z), "=r"(ret.w)
239
+ : "l"(ptr));
240
+ return ret;
241
+ }
242
+
243
+ /**
244
+ * @brief Barrier synchronization pattern for multi-GPU communication.
245
+ *
246
+ * This function implements a barrier synchronization protocol used in All2All
247
+ * and AllGather operations. It signals completion to target ranks and waits
248
+ * for all expected signals to arrive before resetting the barrier.
249
+ *
250
+ * Protocol:
251
+ * 1. Thread 0 of each block signals completion to the target rank
252
+ * 2. Block 0 waits for all ranks to signal (with timeout protection)
253
+ * 3. Once all signals received, reset the barrier counters
254
+ *
255
+ * @param barrier_signal_ptrs Array of pointers to barrier signal buffers for each rank
256
+ * @param target_rank The rank this block is sending data to
257
+ * @param rank This GPU's rank
258
+ * @param world_size Total number of GPUs/ranks
259
+ * @param expected_count Number of signals expected (typically num_sms_per_rank)
260
+ * @param sm_id The SM/block ID (blockIdx.x)
261
+ * @param thread_id The thread ID within the block (threadIdx.x)
262
+ * @param timeout_cycles Number of cycles to wait before timeout (use NUM_TIMEOUT_CYCLES)
263
+ */
264
+ __device__ __forceinline__ void barrier_wait_and_reset(int **barrier_signal_ptrs, int target_rank, int rank,
265
+ int world_size, int expected_count, int sm_id, int thread_id,
266
+ uint64_t timeout_cycles) {
267
+ // Ensure all threads in the block have completed their work
268
+ __syncthreads();
269
+
270
+ // Thread 0 signals completion to target rank
271
+ if (thread_id == 0) {
272
+ atomicAdd_system(barrier_signal_ptrs[target_rank] + rank, 1);
273
+ }
274
+
275
+ // Synchronize before checking signals
276
+ __syncthreads();
277
+
278
+ // Only block 0 waits for all signals and resets the barrier
279
+ if (sm_id == 0 && thread_id < world_size) {
280
+ auto start_time = clock64();
281
+ while (true) {
282
+ bool is_recv_full = ld_volatile_global(barrier_signal_ptrs[rank] + thread_id) == expected_count;
283
+ if (is_recv_full) {
284
+ break;
285
+ }
286
+ if (clock64() - start_time >= timeout_cycles) {
287
+ printf("All2All barrier timeout: rank=%d, waiting_for_source=%d, expected=%d, got=%d\n", rank, thread_id,
288
+ expected_count, ld_volatile_global(barrier_signal_ptrs[rank] + thread_id));
289
+ trap();
290
+ }
291
+ }
292
+ // Reset barrier for next use
293
+ atomicSub_system(barrier_signal_ptrs[rank] + thread_id, expected_count);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * @brief Barrier synchronization for round-robin SM distribution.
299
+ *
300
+ * Similar to barrier_wait_and_reset, but handles the case where SMs are
301
+ * distributed round-robin across ranks, resulting in different target ranks
302
+ * receiving different numbers of signals.
303
+ *
304
+ * With round-robin: target ranks [0, extra) receive (base + 1) signals from
305
+ * each source, and target ranks [extra, world_size) receive base signals
306
+ * from each source. Note that ALL sources send the same count to a given
307
+ * receiver - the count depends on the receiver's rank position.
308
+ *
309
+ * @param barrier_signal_ptrs Array of pointers to barrier signal buffers for each rank
310
+ * @param target_rank The rank this block is sending data to
311
+ * @param rank This GPU's rank
312
+ * @param world_size Total number of GPUs/ranks
313
+ * @param num_sms Total number of SMs launched (used to compute expected counts)
314
+ * @param sm_id The SM/block ID (blockIdx.x)
315
+ * @param thread_id The thread ID within the block (threadIdx.x)
316
+ * @param timeout_cycles Number of cycles to wait before timeout
317
+ */
318
+ __device__ __forceinline__ void barrier_wait_and_reset_roundrobin(int **barrier_signal_ptrs, int target_rank, int rank,
319
+ int world_size, int num_sms, int sm_id, int thread_id,
320
+ uint64_t timeout_cycles) {
321
+ // Ensure all threads in the block have completed their work
322
+ __syncthreads();
323
+
324
+ // Thread 0 signals completion to target rank
325
+ if (thread_id == 0) {
326
+ atomicAdd_system(barrier_signal_ptrs[target_rank] + rank, 1);
327
+ }
328
+
329
+ // Synchronize before checking signals
330
+ __syncthreads();
331
+
332
+ // Only block 0 waits for all signals and resets the barrier
333
+ // Each thread handles one source rank
334
+ if (sm_id == 0 && thread_id < world_size) {
335
+ // All sources send the same number of signals to THIS receiver.
336
+ // The count depends on how many SMs target this rank (the receiver).
337
+ int expected_from_each_source = get_num_sms_for_rank(rank, num_sms, world_size);
338
+
339
+ auto start_time = clock64();
340
+ while (true) {
341
+ bool is_recv_full = ld_volatile_global(barrier_signal_ptrs[rank] + thread_id) == expected_from_each_source;
342
+ if (is_recv_full) {
343
+ break;
344
+ }
345
+ if (clock64() - start_time >= timeout_cycles) {
346
+ printf("All2All barrier timeout (roundrobin): rank=%d, waiting_for_source=%d, expected=%d, got=%d\n", rank,
347
+ thread_id, expected_from_each_source, ld_volatile_global(barrier_signal_ptrs[rank] + thread_id));
348
+ trap();
349
+ }
350
+ }
351
+ // Reset barrier for next use
352
+ atomicSub_system(barrier_signal_ptrs[rank] + thread_id, expected_from_each_source);
353
+ }
354
+ }
355
+
356
+ } // namespace ltx_kernels
ltx-2-internal/internal/ltx-kernels/csrc/include/event.hpp ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @file event.hpp
3
+ * @brief CUDA stream and event synchronization utilities.
4
+ *
5
+ * This header provides wrapper types and helper functions for managing
6
+ * CUDA events and stream synchronization in PyTorch/ATen environment.
7
+ * These utilities are used to coordinate asynchronous operations across
8
+ * multiple CUDA streams.
9
+ */
10
+
11
+ #pragma once
12
+
13
+ #include <ATen/cuda/CUDAContext.h>
14
+ #include <memory>
15
+
16
+ #include "cuda/exceptions.cuh"
17
+
18
+ namespace ltx_kernels {
19
+
20
+ /**
21
+ * @struct EventHandle
22
+ * @brief RAII wrapper for a CUDA event with automatic recording.
23
+ *
24
+ * EventHandle encapsulates a torch::Event and automatically records it
25
+ * on the specified (or current) CUDA stream upon construction. This
26
+ * provides a convenient way to capture the completion point of stream
27
+ * operations for synchronization purposes.
28
+ *
29
+ * ## Usage Example
30
+ *
31
+ * ```cpp
32
+ * // Record event on current stream
33
+ * EventHandle ev1;
34
+ *
35
+ * // Record event on specific stream
36
+ * EventHandle ev2(my_stream);
37
+ *
38
+ * // Make current stream wait for the event
39
+ * ev1.current_stream_wait();
40
+ * ```
41
+ */
42
+ struct EventHandle {
43
+ /// Shared pointer to the underlying torch::Event
44
+ std::shared_ptr<torch::Event> event;
45
+
46
+ /**
47
+ * @brief Constructs an EventHandle and records on the current CUDA stream.
48
+ *
49
+ * The event captures the completion point of all operations submitted
50
+ * to the current stream before this constructor is called.
51
+ */
52
+ EventHandle() {
53
+ event = std::make_shared<torch::Event>(torch::kCUDA);
54
+ event->record(at::cuda::getCurrentCUDAStream());
55
+ }
56
+
57
+ /**
58
+ * @brief Constructs an EventHandle and records on the specified stream.
59
+ *
60
+ * @param stream The CUDA stream to record the event on
61
+ */
62
+ explicit EventHandle(const at::cuda::CUDAStream &stream) {
63
+ event = std::make_shared<torch::Event>(torch::kCUDA);
64
+ event->record(stream);
65
+ }
66
+
67
+ /// Copy constructor (shares the underlying event)
68
+ EventHandle(const EventHandle &other) = default;
69
+
70
+ /**
71
+ * @brief Makes the current CUDA stream wait for this event.
72
+ *
73
+ * After this call returns, operations submitted to the current stream
74
+ * will not execute until the event has been reached on its recording stream.
75
+ */
76
+ void current_stream_wait() const { at::cuda::getCurrentCUDAStream().unwrap().wait(*event); }
77
+ };
78
+
79
+ /**
80
+ * @brief Creates and records a CUDA event on the specified stream.
81
+ *
82
+ * @param s The CUDA stream to record on
83
+ * @return A torch::Event that has been recorded on stream s
84
+ */
85
+ inline torch::Event create_event(const at::cuda::CUDAStream &s) {
86
+ auto event = torch::Event(torch::kCUDA);
87
+ event.record(s);
88
+ return event;
89
+ }
90
+
91
+ /**
92
+ * @brief Makes stream s_0 wait for stream s_1's current position.
93
+ *
94
+ * After this call, operations on s_0 will not execute until all operations
95
+ * currently queued on s_1 have completed.
96
+ *
97
+ * @param s_0 The stream that will wait
98
+ * @param s_1 The stream to wait for
99
+ * @pre s_0 and s_1 must be different streams
100
+ */
101
+ inline void stream_wait(const at::cuda::CUDAStream &s_0, const at::cuda::CUDAStream &s_1) {
102
+ EP_HOST_ASSERT(s_0.id() != s_1.id());
103
+ s_0.unwrap().wait(create_event(s_1));
104
+ }
105
+
106
+ /**
107
+ * @brief Makes a stream wait for a previously recorded event.
108
+ *
109
+ * @param s The stream that will wait
110
+ * @param event The event to wait for
111
+ */
112
+ inline void stream_wait(const at::cuda::CUDAStream &s, const EventHandle &event) { s.unwrap().wait(*event.event); }
113
+
114
+ } // namespace ltx_kernels
ltx-2-internal/internal/ltx-kernels/pyproject.toml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=61",
4
+ "wheel",
5
+ "torch",
6
+ ]
7
+ build-backend = "setuptools.build_meta"
8
+
9
+ [project]
10
+ name = "ltx-kernels"
11
+ version = "0.1.0"
12
+ dependencies = ["torch"]
13
+
14
+ [tool.setuptools]
15
+ package-dir = {"" = "src"}
16
+
17
+ [tool.setuptools.packages.find]
18
+ where = ["src"]
ltx-2-internal/internal/ltx-kernels/setup.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import setuptools
4
+ from torch.utils.cpp_extension import BuildExtension, CUDAExtension
5
+
6
+
7
+ def _nvidia_include_dirs() -> list[str]:
8
+ """Include dirs from pip-installed nvidia packages (e.g. cusparse headers)."""
9
+ try:
10
+ import nvidia # noqa: PLC0415
11
+
12
+ return [str(p) for pkg in Path(nvidia.__path__[0]).iterdir() if (p := pkg / "include").is_dir()]
13
+ except ImportError:
14
+ return []
15
+
16
+
17
+ if __name__ == "__main__":
18
+ cxx_flags = ["-O3", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", "-Wno-attributes"]
19
+ nvcc_flags = ["-O3"]
20
+ extra_compile_args = {
21
+ "cxx": cxx_flags,
22
+ "nvcc": nvcc_flags,
23
+ }
24
+ ROOT = Path(__file__).resolve().parent
25
+ setuptools.setup(
26
+ ext_modules=[
27
+ CUDAExtension(
28
+ name="all2all_cpp",
29
+ include_dirs=[str(ROOT / "csrc/all2all"), str(ROOT / "csrc/include"), *_nvidia_include_dirs()],
30
+ sources=[
31
+ "csrc/all2all/all2all.cpp",
32
+ "csrc/all2all/cuda/all2all_heads.cu",
33
+ "csrc/all2all/cuda/allgather.cu",
34
+ ],
35
+ extra_compile_args=extra_compile_args,
36
+ )
37
+ ],
38
+ cmdclass={"build_ext": BuildExtension},
39
+ )
ltx-2-internal/internal/ltx-kernels/src/ltx_kernels/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """ltx-kernels: High-performance CUDA kernels for distributed attention operations."""
2
+
3
+ from ltx_kernels.all_to_all import All2All
4
+
5
+ __all__ = ["All2All"]
6
+ __version__ = "0.1.0"
ltx-2-internal/internal/ltx-kernels/src/ltx_kernels/all_to_all.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """All2All communication primitives for distributed attention."""
2
+
3
+ from typing import Any
4
+
5
+ import torch
6
+ import torch.distributed as dist
7
+ from all2all_cpp import All2All as All2AllCpp
8
+
9
+
10
+ class All2All:
11
+ """IPC-based All2All communication for distributed head-parallel attention.
12
+
13
+ This class manages GPU memory buffers and IPC handles to enable efficient
14
+ cross-GPU communication for attention head redistribution.
15
+
16
+ Args:
17
+ rank: Local rank of this process.
18
+ world_size: Total number of processes in the distributed group.
19
+ seqlen: Maximum sequence length to allocate buffers for.
20
+ hidden_dim: Hidden dimension size (num_heads * head_dim).
21
+ num_sms: Number of SMs to use for kernel execution.
22
+ tensor_dtype: Data type for tensors (e.g., torch.bfloat16).
23
+ group: PyTorch distributed process group.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ rank: int,
29
+ world_size: int,
30
+ seqlen: int,
31
+ hidden_dim: int,
32
+ num_sms: int,
33
+ tensor_dtype: torch.dtype,
34
+ group: torch.distributed.ProcessGroup | None = None,
35
+ ) -> None:
36
+ self.rank = rank
37
+ self.world_size = world_size
38
+ self.num_sms = num_sms
39
+ self.tensor_dtype = tensor_dtype
40
+ self.buffer_size = int(seqlen * hidden_dim * tensor_dtype.itemsize)
41
+
42
+ # Initialize the C++ runtime
43
+ self.runtime = All2AllCpp(rank, world_size, seqlen, hidden_dim, num_sms, tensor_dtype)
44
+
45
+ # Exchange IPC handles across all ranks
46
+ ipc_handles: list[Any] = [None] * world_size
47
+ local_ipc_handle = self.runtime.get_local_ipc_handle()
48
+ dist.all_gather_object(ipc_handles, local_ipc_handle, group)
49
+
50
+ self.runtime.sync(ipc_handles)
51
+
52
+ def send_recv_heads(self, x: torch.Tensor, *, copy_out: bool = False) -> torch.Tensor:
53
+ """Exchange attention heads across ranks (All2All pattern).
54
+
55
+ Args:
56
+ x: Input tensor of shape [batch, tokens, heads, head_dim].
57
+ copy_out: If True, copy result to a new tensor instead of using buffer.
58
+
59
+ Returns:
60
+ Output tensor with redistributed heads.
61
+ """
62
+ return self.runtime.send_recv_heads(x, copy_out)
63
+
64
+ def gather_heads(self, x: torch.Tensor, *, copy_out: bool = False) -> torch.Tensor:
65
+ """Gather heads back to original distribution (reverse All2All).
66
+
67
+ Args:
68
+ x: Input tensor with distributed heads.
69
+ copy_out: If True, copy result to a new tensor instead of using buffer.
70
+
71
+ Returns:
72
+ Output tensor with gathered heads.
73
+ """
74
+ return self.runtime.gather_heads(x, copy_out)
75
+
76
+ def allgather(self, x: torch.Tensor, *, copy_out: bool = False) -> torch.Tensor:
77
+ """Allgather operation across all ranks.
78
+
79
+ Args:
80
+ x: Input tensor to gather.
81
+ copy_out: If True, copy result to a new tensor instead of using buffer.
82
+
83
+ Returns:
84
+ Gathered tensor from all ranks.
85
+ """
86
+ return self.runtime.allgather(x, copy_out)
87
+
88
+ def destroy(self) -> None:
89
+ """Release IPC handles and GPU memory buffers."""
90
+ self.runtime.destroy()
ltx-2-internal/internal/test-txt2img-parity/conftest.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Session-level import stubs for txt2img JAX modules.
2
+
3
+ Why this exists
4
+ ---------------
5
+ ``txt2img.jax.models.transformers.attention_block`` (which defines
6
+ ``SelfAttention`` and ``CrossAttention``) has two top-level imports that fail
7
+ in CPU/GPU environments:
8
+
9
+ 1. ``from ltx_video.utils.skip_layer_strategy import SkipLayerStrategy``
10
+ ``ltx_video`` is a Lightricks-internal package that is not on PyPI and is
11
+ not installed in this environment. Its ``SkipLayerStrategy`` enum is only
12
+ used in ``SelfAttention.__call__`` for STG skip-layer strategies, which are
13
+ never exercised by the parity tests.
14
+
15
+ 2. ``txt2img.jax.models.transformers.attention.__init__`` unconditionally
16
+ imports ``TokamaxSplashAttention``, which depends on ``tokamax`` — a
17
+ Google-internal TPU kernel library unavailable on CPU/GPU. The splash
18
+ attention path is only reachable when ``attention_op="tokamax_splash"``;
19
+ parity tests always use ``attention_op="explicit"``.
20
+
21
+ Python executes every line of a module file before exposing any of its
22
+ contents, so even though ``SelfAttention`` itself never touches these paths,
23
+ the file fails to load and nothing inside it is importable.
24
+
25
+ The fix: inject minimal ``sys.modules`` entries *before* any test module is
26
+ imported. pytest loads ``conftest.py`` first, so the stubs are in place when
27
+ the test files run their top-level imports. The stubs are never called at
28
+ runtime — they exist solely to satisfy the module loader.
29
+ """
30
+
31
+ import sys
32
+ import types
33
+ from enum import Enum, auto
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # 1. ltx_video.utils.skip_layer_strategy
38
+ # ---------------------------------------------------------------------------
39
+ class SkipLayerStrategy(Enum):
40
+ AttentionSkip = auto()
41
+ AttentionValues = auto()
42
+ Residual = auto()
43
+ TransformerBlock = auto()
44
+
45
+
46
+ _ltx_video = types.ModuleType("ltx_video")
47
+ _ltx_video_utils = types.ModuleType("ltx_video.utils")
48
+ _ltx_video_sls = types.ModuleType("ltx_video.utils.skip_layer_strategy")
49
+ _ltx_video_sls.SkipLayerStrategy = SkipLayerStrategy # type: ignore[attr-defined]
50
+
51
+ sys.modules.setdefault("ltx_video", _ltx_video)
52
+ sys.modules.setdefault("ltx_video.utils", _ltx_video_utils)
53
+ sys.modules.setdefault("ltx_video.utils.skip_layer_strategy", _ltx_video_sls)
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # 2. tokamax — TPU-only splash/ring attention kernels
58
+ # ---------------------------------------------------------------------------
59
+ class _SplashConfig:
60
+ pass
61
+
62
+
63
+ class _SplashKernel:
64
+ SplashConfig = _SplashConfig
65
+
66
+
67
+ class _SplashMask:
68
+ Mask = None
69
+
70
+
71
+ _tpu_splash = types.ModuleType("tokamax._src.ops.experimental.tpu.splash_attention")
72
+ _tpu_splash.splash_attention_kernel = _SplashKernel() # type: ignore[attr-defined]
73
+ _tpu_splash.ring_attention_kernel = None # type: ignore[attr-defined]
74
+ _tpu_splash.splash_attention_mask = _SplashMask() # type: ignore[attr-defined]
75
+
76
+ for _name in [
77
+ "tokamax",
78
+ "tokamax._src",
79
+ "tokamax._src.ops",
80
+ "tokamax._src.ops.experimental",
81
+ "tokamax._src.ops.experimental.tpu",
82
+ ]:
83
+ sys.modules.setdefault(_name, types.ModuleType(_name))
84
+
85
+ sys.modules.setdefault("tokamax._src.ops.experimental.tpu.splash_attention", _tpu_splash)
ltx-2-internal/internal/test-txt2img-parity/pyproject.toml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "test-txt2img-parity"
3
+ version = "0.1.0"
4
+ description = "Parity tests verifying PyTorch LTX-2 matches the txt2img JAX reference implementation"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "ltx-core",
8
+ "jax",
9
+ "flax",
10
+ "txt2img",
11
+ "pydantic",
12
+ "google-cloud-storage",
13
+ "diffusers",
14
+ "pytest",
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.9.8,<0.10.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [tool.pytest.ini_options]
22
+ addopts = ["--import-mode=importlib"]
23
+ pythonpath = ["src"]
24
+
25
+ [tool.uv.sources]
26
+ ltx-core = { path = "../../packages/ltx-core", editable = true }
27
+ txt2img = { git = "https://github.com/LightricksResearch/txt2img", branch = "main" }
28
+
29
+ # txt2img pins requires-python = "==3.12.12" but works on >=3.10 — relaxed here.
30
+ # txt2img base deps include local-only editable packages (ltx-video, dataset_metadata, lttensor)
31
+ # that don't exist on PyPI. The actual ML deps (jax, flax, etc.) are in dependency groups,
32
+ # not base deps, so they're never installed automatically. We strip everything and declare
33
+ # exactly what we need explicitly.
34
+ [[tool.uv.dependency-metadata]]
35
+ name = "txt2img"
36
+ version = "0.1.0"
37
+ requires-python = ">=3.10"
38
+ requires-dist = []
ltx-2-internal/internal/test-txt2img-parity/src/test_txt2img_parity/__init__.py ADDED
File without changes
ltx-2-internal/internal/test-txt2img-parity/src/test_txt2img_parity/testing_facilities.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import time
3
+ from collections.abc import Callable, Generator
4
+ from contextlib import ExitStack, contextmanager
5
+ from copy import deepcopy
6
+ from functools import wraps
7
+ from typing import Any
8
+
9
+ import jax
10
+ import jax.numpy as jnp
11
+ import numpy as np
12
+ import numpy.typing as npt
13
+ import torch
14
+ import torch.utils._pytree as pytree
15
+ from jax import grad
16
+ from txt2img.jax.utils.torch_compat import jax_statedict_to_torch
17
+
18
+ Shape = tuple[int, ...]
19
+ ShapeDict = dict[str, Shape]
20
+ NumpyTensorDict = dict[str, npt.NDArray]
21
+ JaxTensorTree = jax.Array | dict[str, jax.Array]
22
+ TorchTensorTree = torch.Tensor | dict[str, torch.Tensor]
23
+ TorchFunction = Callable[[TorchTensorTree], torch.Tensor]
24
+ JaxFunction = Callable[[JaxTensorTree], jax.Array]
25
+
26
+
27
+ @contextmanager
28
+ def torch_default_dtype(enable_x64: bool) -> Generator[None, None, None]:
29
+ previous_dtype = torch.get_default_dtype()
30
+ new_dtype = torch.float64 if enable_x64 else torch.float32
31
+ torch.set_default_dtype(new_dtype)
32
+ try:
33
+ yield
34
+ finally:
35
+ torch.set_default_dtype(previous_dtype)
36
+
37
+
38
+ @contextmanager
39
+ def jax_64_precision() -> Generator[None, None, None]:
40
+ previous_state = jax.config.read("jax_enable_x64") # Save current state
41
+ jax.config.update("jax_enable_x64", True) # Enable x64
42
+ try:
43
+ yield
44
+ finally:
45
+ jax.config.update("jax_enable_x64", previous_state) # Restore original state
46
+
47
+
48
+ def measure_time(func: Callable[..., Any]) -> Callable[..., tuple[Any, float]]:
49
+ """Decorator that measures the execution time of a function."""
50
+
51
+ @wraps(func)
52
+ def wrapper(*args: object, **kwargs: object) -> tuple[object, float]:
53
+ start_time = time.perf_counter()
54
+ result = func(*args, **kwargs)
55
+ end_time = time.perf_counter()
56
+ execution_time = end_time - start_time
57
+ return result, execution_time
58
+
59
+ return wrapper
60
+
61
+
62
+ def ensure_tolerance(desc: str, data_torch: npt.NDArray, data_jax: npt.NDArray, atol: float, rtol: float) -> None:
63
+ """
64
+ Ensures that the outputs of the torch and jax functions are within the given tolerance.
65
+
66
+ Args:
67
+ desc (str): The tensors description
68
+ data_torch (npt.NDArray): torch predictions
69
+ data_jax (npt.NDArray): jax predictions
70
+ atol (float): absolute tolerance
71
+ rtol (float): relative tolerance
72
+ """
73
+ torch_shape = data_torch.shape
74
+ jax_shape = data_jax.shape
75
+ assert torch_shape == jax_shape, f"{desc} Shapes do not match: {torch_shape} != {jax_shape}"
76
+
77
+ if data_torch.dtype in (bool, np.bool_):
78
+ abs_err = np.bitwise_xor(data_jax, data_torch).sum() / np.prod(data_jax.shape)
79
+ rtol = 0
80
+ atol = 0
81
+ else:
82
+ abs_err = abs(data_torch - data_jax).max()
83
+
84
+ passed = np.isclose(data_torch, data_jax, atol=atol, rtol=rtol).all()
85
+ logger = logging.getLogger(__name__)
86
+ logger.info(
87
+ "%s: max_abs_err=%.6e, atol=%.6e, rtol=%.6e, passed=%s",
88
+ desc,
89
+ float(abs_err),
90
+ atol,
91
+ rtol,
92
+ passed,
93
+ )
94
+ assert passed, f"{desc} Outputs do not match, got absolute error {abs_err}"
95
+
96
+
97
+ def _compare_module(
98
+ data: npt.NDArray | NumpyTensorDict, # NOTE - some layers may define several inputs, so we use a dictionary
99
+ torch_function: TorchFunction,
100
+ jax_function: JaxFunction,
101
+ atol: float,
102
+ rtol: float,
103
+ forward_only: bool,
104
+ ) -> None:
105
+ """
106
+ Compare a torch module against its jax equivalent.
107
+
108
+ Args:
109
+ data (npt.NDArray | NumpyTensorDict): Dictionary containing the input tensors
110
+ torch_function (TorchFunction): Torch function to test
111
+ jax_function (JaxFunction): Jax function to test
112
+ atol (float): Absolute tolerance.
113
+ rtol (float): Relative tolerance.
114
+ forward_only (bool): if True, only the forward pass will be tested
115
+ """
116
+ logger = logging.getLogger(__name__)
117
+
118
+ # run torch function
119
+ inputs_torch = pytree.tree_map(torch.from_numpy, deepcopy(data))
120
+ output_torch, torch_time = measure_time(torch_function)(inputs_torch)
121
+ logger.info("torch_time: %s seconds", torch_time)
122
+
123
+ # Run jax function
124
+ inputs_jax = pytree.tree_map(jax.numpy.array, deepcopy(data))
125
+ output_jax, jax_time = measure_time(jax_function)(inputs_jax)
126
+ logger.info("jax_time: %s seconds", jax_time)
127
+
128
+ # Handle few outputs
129
+ if isinstance(output_torch, tuple):
130
+ assert isinstance(output_jax, tuple), "torch returned tuple but jax did not"
131
+ assert len(output_jax) == len(output_torch), (
132
+ f"torch and jax output counts do not match: {len(output_torch)} != {len(output_jax)}"
133
+ )
134
+ else:
135
+ output_torch, output_jax = (output_torch,), (output_jax,)
136
+
137
+ # Convert to numpy
138
+ for i, (single_output_torch, single_output_jax) in enumerate(
139
+ zip(output_torch, output_jax, strict=True),
140
+ ):
141
+ data_torch = single_output_torch.cpu().detach().numpy()
142
+
143
+ cpu_device = jax.devices("cpu")[0]
144
+ data_jax = np.array(jax.device_put(single_output_jax, cpu_device))
145
+
146
+ ensure_tolerance(f"Output {i}", data_torch, data_jax, atol, rtol)
147
+
148
+ if not forward_only:
149
+ cpu_device = jax.devices("cpu")[0]
150
+
151
+ def _to_numpy(x: jax.Array) -> npt.NDArray:
152
+ return np.array(jax.device_put(x, cpu_device))
153
+
154
+ # Backward torch
155
+ num_outputs = len(output_torch)
156
+ for i in range(num_outputs):
157
+ inputs_torch = pytree.tree_map(torch.from_numpy, deepcopy(data))
158
+ pytree.tree_map(lambda x: (x.requires_grad_(), x.retain_grad()), inputs_torch)
159
+ output_torch, torch_time = measure_time(torch_function)(inputs_torch)
160
+ if num_outputs > 1:
161
+ output_torch = output_torch[i]
162
+ output_torch.sum().backward()
163
+ grad_torch = pytree.tree_map(lambda x: x.grad.numpy() if x.grad is not None else None, inputs_torch)
164
+ logger.warning("torch_time (backward) for output %d: %s seconds", i, torch_time)
165
+
166
+ inputs_jax = pytree.tree_map(jax.numpy.array, deepcopy(data))
167
+
168
+ def _make_loss_func(idx: int, n_outputs: int, jax_fn: JaxFunction) -> Callable[[JaxTensorTree], jax.Array]:
169
+ def _loss(x: JaxTensorTree) -> jax.Array:
170
+ return jnp.sum(jax_fn(x)[idx]) if n_outputs > 1 else jnp.sum(jax_fn(x))
171
+
172
+ return _loss
173
+
174
+ loss_func = _make_loss_func(i, num_outputs, jax_function)
175
+
176
+ grad_jax, jax_time = jax.grad(measure_time(loss_func), has_aux=True)(inputs_jax)
177
+ logger.warning("jax_time (backward) for output %d: %s seconds", i, jax_time)
178
+
179
+ if isinstance(grad_torch, dict):
180
+ for key, torch_grad in grad_torch.items():
181
+ grad_jax_np = _to_numpy(grad_jax[key])
182
+ if torch_grad is not None:
183
+ ensure_tolerance(f"Grad {key} for output {i}", torch_grad, grad_jax_np, atol, rtol)
184
+ else:
185
+ assert bool((grad_jax_np == 0).all())
186
+ else:
187
+ ensure_tolerance(f"Grad for output {i}", grad_torch, _to_numpy(grad_jax), atol, rtol)
188
+
189
+
190
+ def torch_jax_comparison_test(
191
+ shapes: Shape | ShapeDict,
192
+ torch_function: TorchFunction,
193
+ jax_function: JaxFunction,
194
+ atol: float = 1e-5,
195
+ rtol: float = 1e-5,
196
+ seed: int = 42,
197
+ forward_only: bool = False,
198
+ use_x64_precision: bool = True, # Default to True to preserve current behavior
199
+ ) -> None:
200
+ """
201
+ Facilitates the comparison test between torch and jax functions.
202
+
203
+ Args:
204
+ shapes (Shape | ShapeDict): a mapping between input name and its shape that will be generated
205
+ torch_function (TorchFunction): The torch function equivalent to test
206
+ jax_function (JaxFunction): The jax function equivalent to test
207
+ atol (float, optional): Absolute tolerance of error. Defaults to 1e-5.
208
+ rtol (float, optional): Relative tolerance of error. Defaults to 1e-5.
209
+ seed (int): the seed to generate random numpy arrays with
210
+ forward_only (bool): if True, only the forward pass will be tested
211
+ use_x64_precision (bool): Flag to enable jax x64 precision mode. Defaults to True.
212
+ """
213
+ rng = np.random.default_rng(seed)
214
+ torch.manual_seed(seed)
215
+ dtype = np.float64 if use_x64_precision else np.float32
216
+ if isinstance(shapes, dict):
217
+ data = {name: rng.random(shape).astype(dtype) for name, shape in shapes.items()} # Multi input
218
+ else:
219
+ data = rng.random(shapes).astype(dtype) # Single input
220
+
221
+ with ExitStack() as stack:
222
+ if use_x64_precision:
223
+ stack.enter_context(jax_64_precision())
224
+ stack.enter_context(torch_default_dtype(use_x64_precision))
225
+ with jax.default_matmul_precision("highest"):
226
+ _compare_module(data, torch_function, jax_function, atol, rtol, forward_only)
227
+
228
+
229
+ def compare_gradients_by_parameters(
230
+ torch_module: torch.nn.Module,
231
+ jax_module: object,
232
+ shapes: ShapeDict,
233
+ loss_fn_torch: Callable[[Any], torch.Tensor],
234
+ loss_fn_jax: Callable[[Any], jax.Array],
235
+ use_x64_precision: bool,
236
+ params_jax: dict,
237
+ atol: float,
238
+ rtol: float,
239
+ ) -> None:
240
+ def _convert_masks_to_boolean(data: dict[str, Any]) -> dict[str, Any]:
241
+ if "attention_mask" in data:
242
+ data["attention_mask"] = data["attention_mask"] >= 0.5
243
+ return data
244
+
245
+ rng = np.random.default_rng(42)
246
+ torch_module.train()
247
+ torch_module.zero_grad()
248
+ dtype = np.float64 if use_x64_precision else np.float32
249
+ data = {name: rng.random(shape).astype(dtype) for name, shape in shapes.items()}
250
+ torch_input = pytree.tree_map(torch.from_numpy, deepcopy(data))
251
+ jax_input = pytree.tree_map(jax.numpy.array, deepcopy(data))
252
+
253
+ torch_input = _convert_masks_to_boolean(torch_input)
254
+ torch_input["encoder_attention_mask"] = torch.ones_like(torch_input.pop("encoder_attention_segment_ids")).to(
255
+ torch.bool
256
+ )
257
+
258
+ torch_output = torch_module(**torch_input)
259
+ torch_loss = loss_fn_torch(torch_output)
260
+ torch_loss.backward()
261
+
262
+ def jax_loss(params: dict, inputs: dict) -> jax.Array:
263
+ outputs = jax_module.apply(params, **inputs)
264
+ ret = loss_fn_jax(outputs)
265
+ return ret
266
+
267
+ jax_input = _convert_masks_to_boolean(jax_input)
268
+ jax_input["encoder_attention_segment_ids"] = jnp.ones_like(data["encoder_attention_segment_ids"], dtype=jnp.uint8)
269
+
270
+ jax_grads = grad(jax_loss)(params_jax, jax_input)
271
+ jax_grads_to_torch_statedict = jax_statedict_to_torch(jax_grads)
272
+
273
+ for name, param in torch_module.named_parameters():
274
+ torch_grad = param.grad.detach().numpy() if param.grad is not None else None
275
+ jax_grad = np.array(jax_grads_to_torch_statedict[name])
276
+
277
+ if torch_grad is not None and jax_grad is not None:
278
+ ensure_tolerance(f"PARAMETER Grad for output {name}", torch_grad, jax_grad, atol, rtol)
279
+ elif torch_grad is None and not (jax_grad == 0).all():
280
+ raise ValueError("Mismatch in gradient presence: torch gradient is None while the jax is non-zero.")
281
+ elif torch_grad is not None and (jax_grad == 0).all():
282
+ raise ValueError("Mismatch in gradient presence: torch gradient is not None while the jax is zero.")
ltx-2-internal/internal/test-txt2img-parity/uv.lock ADDED
The diff for this file is too large to render. See raw diff
 
ltx-2-internal/packages/ltx-bench/README.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ltx-bench
2
+
3
+ Profiling and benchmarking tools for LTX-2.
4
+
5
+ ## Running benchmarks
6
+
7
+ ```bash
8
+ # All benchmarks (requires H100 GPU + model checkpoints at /models/)
9
+ uv run pytest -m bench --bench
10
+
11
+ # With Nsight Systems profiling
12
+ uv run pytest -m bench --bench --nsys
13
+
14
+ # Custom output directory and iterations
15
+ uv run pytest -m bench --bench \
16
+ --bench-output=./my_results \
17
+ --bench-warmup=3 \
18
+ --bench-repeats=10
19
+ ```
20
+
21
+ ## Benchmark types
22
+
23
+ ### Components
24
+
25
+ Individual model components benchmarked in isolation:
26
+
27
+ | Benchmark | What it measures |
28
+ |-----------|-----------------|
29
+ | `transformer_forward_video_only` | X0Model forward pass (video only) |
30
+ | `transformer_forward_av` | X0Model forward pass (video + audio) |
31
+ | `video_vae_decode` | Video VAE decoder |
32
+ | `video_vae_encode` | Video VAE encoder |
33
+ | `audio_decode` | Audio VAE decoder + vocoder |
34
+ | `audio_encode` | Audio VAE encoder |
35
+ | `text_encoder_forward` | Gemma text encoder (positive + negative prompt) |
36
+ | `spatial_upsampler` | Latent spatial upsampler 2x |
37
+
38
+ ### Pipelines
39
+
40
+ End-to-end pipeline benchmarks with per-stage instrumentation:
41
+
42
+ | Benchmark | Pipeline |
43
+ |-----------|----------|
44
+ | `distilled` | 8-step distilled inference |
45
+ | `ti2vid_one_stage` | Text-to-video one-stage |
46
+ | `ti2vid_two_stages` | Text-to-video two-stage with upsampling |
47
+ | `keyframe_interpolation` | Keyframe interpolation two-stage |
48
+ | `ic_lora` | IC-LoRA with depth conditioning |
49
+ | `a2vid_two_stage` | Audio-to-video two-stage |
50
+ | `retake` | Video retake with regeneration |
51
+
52
+ ## Trend analysis
53
+
54
+ Generate trend charts from GCS historical data:
55
+
56
+ ```bash
57
+ python -m ltx_bench.trends --branch main --last 20 --output ./trend_report
58
+ ```
59
+
60
+ ## Report output
61
+
62
+ Each run produces a `REPORT.md` with:
63
+ - Per-component GPU time, peak VRAM, and peak RAM
64
+ - Per-pipeline total time with stage breakdown (text encoding, denoising, VAE decode, etc.)
65
+ - Denoising throughput in s/it
ltx-2-internal/packages/ltx-bench/benchmarks/__init__.py ADDED
File without changes
ltx-2-internal/packages/ltx-bench/benchmarks/components/__init__.py ADDED
File without changes
ltx-2-internal/packages/ltx-bench/benchmarks/components/conftest.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model builders, re-exports, and helpers for component benchmarks.
2
+
3
+ Component benchmarks build models explicitly via ``SingleGPUModelBuilder``
4
+ rather than going through ``ModelLedger`` (which is a pipeline-level abstraction).
5
+ Shared constants (paths, shape helpers) are re-exported from the parent conftest.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import torch
11
+
12
+ # Re-export from parent conftest for test files
13
+ from benchmarks.conftest import ( # noqa: F401
14
+ CHECKPOINT_PATH,
15
+ DISTILLED_LORA_PATH,
16
+ GEMMA_ROOT_PATH,
17
+ IC_LORA_PATH,
18
+ UPSAMPLER_PATH,
19
+ make_audio_latent_state,
20
+ make_noised_states,
21
+ make_video_latent_state,
22
+ )
23
+ from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder as Builder
24
+ from ltx_core.model.audio_vae import (
25
+ AUDIO_VAE_DECODER_COMFY_KEYS_FILTER,
26
+ AUDIO_VAE_ENCODER_COMFY_KEYS_FILTER,
27
+ VOCODER_COMFY_KEYS_FILTER,
28
+ AudioDecoder,
29
+ AudioDecoderConfigurator,
30
+ AudioEncoder,
31
+ AudioEncoderConfigurator,
32
+ Vocoder,
33
+ VocoderConfigurator,
34
+ )
35
+ from ltx_core.model.model_protocol import ModelType
36
+ from ltx_core.model.transformer import LTXV_MODEL_COMFY_RENAMING_MAP, LTXModelConfigurator, X0Model
37
+ from ltx_core.model.upsampler import LatentUpsampler, LatentUpsamplerConfigurator
38
+ from ltx_core.model.video_vae import (
39
+ VAE_DECODER_COMFY_KEYS_FILTER,
40
+ VAE_ENCODER_COMFY_KEYS_FILTER,
41
+ VideoDecoder,
42
+ VideoDecoderConfigurator,
43
+ VideoEncoder,
44
+ VideoEncoderConfigurator,
45
+ )
46
+ from ltx_core.text_encoders.gemma import (
47
+ GEMMA_LLM_KEY_OPS,
48
+ GemmaTextEncoderConfigurator,
49
+ module_ops_from_gemma_root,
50
+ )
51
+ from ltx_core.text_encoders.gemma.encoders.encoder_configurator import GEMMA_MODEL_OPS
52
+ from ltx_core.utils import find_matching_file
53
+
54
+ _DTYPE = torch.bfloat16
55
+ _DEVICE = "cuda"
56
+
57
+
58
+ def _build(builder: Builder[ModelType]) -> ModelType:
59
+ return builder.build(device=torch.device(_DEVICE), dtype=_DTYPE).to(_DEVICE).eval()
60
+
61
+
62
+ def build_transformer(checkpoint_path: str) -> X0Model:
63
+ builder = Builder(
64
+ model_path=checkpoint_path,
65
+ model_class_configurator=LTXModelConfigurator,
66
+ model_sd_ops=LTXV_MODEL_COMFY_RENAMING_MAP,
67
+ )
68
+ return X0Model(_build(builder)).eval()
69
+
70
+
71
+ def build_video_decoder(checkpoint_path: str) -> VideoDecoder:
72
+ builder = Builder(
73
+ model_path=checkpoint_path,
74
+ model_class_configurator=VideoDecoderConfigurator,
75
+ model_sd_ops=VAE_DECODER_COMFY_KEYS_FILTER,
76
+ )
77
+ return _build(builder)
78
+
79
+
80
+ def build_video_encoder(checkpoint_path: str) -> VideoEncoder:
81
+ builder = Builder(
82
+ model_path=checkpoint_path,
83
+ model_class_configurator=VideoEncoderConfigurator,
84
+ model_sd_ops=VAE_ENCODER_COMFY_KEYS_FILTER,
85
+ )
86
+ return _build(builder)
87
+
88
+
89
+ def build_audio_decoder(checkpoint_path: str) -> AudioDecoder:
90
+ builder = Builder(
91
+ model_path=checkpoint_path,
92
+ model_class_configurator=AudioDecoderConfigurator,
93
+ model_sd_ops=AUDIO_VAE_DECODER_COMFY_KEYS_FILTER,
94
+ )
95
+ return _build(builder)
96
+
97
+
98
+ def build_audio_encoder(checkpoint_path: str) -> AudioEncoder:
99
+ builder = Builder(
100
+ model_path=checkpoint_path,
101
+ model_class_configurator=AudioEncoderConfigurator,
102
+ model_sd_ops=AUDIO_VAE_ENCODER_COMFY_KEYS_FILTER,
103
+ )
104
+ return _build(builder)
105
+
106
+
107
+ def build_vocoder(checkpoint_path: str) -> Vocoder:
108
+ builder = Builder(
109
+ model_path=checkpoint_path,
110
+ model_class_configurator=VocoderConfigurator,
111
+ model_sd_ops=VOCODER_COMFY_KEYS_FILTER,
112
+ )
113
+ return _build(builder)
114
+
115
+
116
+ def build_text_encoder(checkpoint_path: str, gemma_root_path: str) -> torch.nn.Module:
117
+ if not gemma_root_path:
118
+ msg = "gemma_root_path is required to build text encoder"
119
+ raise ValueError(msg)
120
+ module_ops = module_ops_from_gemma_root(gemma_root_path)
121
+ model_folder = find_matching_file(gemma_root_path, "model*.safetensors").parent
122
+ weight_paths = [str(p) for p in model_folder.rglob("*.safetensors")]
123
+ builder = Builder(
124
+ model_path=(checkpoint_path, *weight_paths),
125
+ model_class_configurator=GemmaTextEncoderConfigurator,
126
+ model_sd_ops=GEMMA_LLM_KEY_OPS,
127
+ module_ops=(GEMMA_MODEL_OPS, *module_ops),
128
+ )
129
+ return _build(builder)
130
+
131
+
132
+ def build_upsampler(upsampler_path: str) -> LatentUpsampler:
133
+ if not upsampler_path:
134
+ msg = "upsampler_path is required to build upsampler"
135
+ raise ValueError(msg)
136
+ builder = Builder(
137
+ model_path=upsampler_path,
138
+ model_class_configurator=LatentUpsamplerConfigurator,
139
+ )
140
+ return _build(builder)
ltx-2-internal/packages/ltx-bench/benchmarks/components/test_audio_vae.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: audio VAE encoder, decoder, and vocoder.
2
+
3
+ Measures GPU time for the audio latent <-> spectrogram path (encoder/decoder)
4
+ and spectrogram -> waveform (vocoder). Audio shapes depend only on video
5
+ duration (num_frames / fps), not spatial resolution. Resolution column shows
6
+ the actual input tensor shape (excluding batch).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+ import torch
16
+
17
+ from benchmarks.conftest import COMPONENT_SIZES, InputSize, audio_latent_torch_shape
18
+ from ltx_bench.core import BenchmarkResult
19
+ from ltx_bench.nvtx import nvtx_range
20
+ from ltx_core.model.audio_vae import AudioDecoder, AudioEncoder, Vocoder
21
+
22
+ from .conftest import (
23
+ CHECKPOINT_PATH,
24
+ build_audio_decoder,
25
+ build_audio_encoder,
26
+ build_vocoder,
27
+ )
28
+
29
+ _MODEL_NAME = Path(CHECKPOINT_PATH).stem
30
+
31
+
32
+ def _shape_label(tensor: torch.Tensor) -> str:
33
+ """Format tensor shape (excluding batch dim) as a resolution label."""
34
+ return "x".join(str(d) for d in tensor.shape[1:])
35
+
36
+
37
+ @pytest.fixture(scope="module")
38
+ def audio_decoder() -> AudioDecoder:
39
+ return build_audio_decoder(CHECKPOINT_PATH)
40
+
41
+
42
+ @pytest.fixture(scope="module")
43
+ def audio_encoder() -> AudioEncoder:
44
+ return build_audio_encoder(CHECKPOINT_PATH)
45
+
46
+
47
+ @pytest.fixture(scope="module")
48
+ def vocoder() -> Vocoder:
49
+ return build_vocoder(CHECKPOINT_PATH)
50
+
51
+
52
+ def _make_spectrogram(
53
+ audio_decoder: AudioDecoder,
54
+ size: InputSize,
55
+ ) -> torch.Tensor:
56
+ """Decode a random audio latent to produce a realistic spectrogram."""
57
+ device = next(audio_decoder.parameters()).device
58
+ dtype = next(audio_decoder.parameters()).dtype
59
+ latents = torch.randn(*audio_latent_torch_shape(size), device=device, dtype=dtype)
60
+ with torch.inference_mode():
61
+ return audio_decoder(latents)
62
+
63
+
64
+ @pytest.mark.bench
65
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
66
+ @torch.inference_mode()
67
+ def test_audio_decoder_forward(
68
+ audio_decoder: AudioDecoder,
69
+ bench_run: Callable[..., BenchmarkResult],
70
+ size: InputSize,
71
+ ) -> None:
72
+ """Benchmark AudioDecoder forward pass (latent → spectrogram)."""
73
+ device = next(audio_decoder.parameters()).device
74
+ dtype = next(audio_decoder.parameters()).dtype
75
+ latents = torch.randn(*audio_latent_torch_shape(size), device=device, dtype=dtype)
76
+
77
+ def _decode() -> None:
78
+ with nvtx_range("audio_decoder_forward"):
79
+ audio_decoder(latents)
80
+
81
+ bench_run(_decode, bench_type="audio_decoder_forward", model_name=_MODEL_NAME, resolution=_shape_label(latents))
82
+
83
+
84
+ @pytest.mark.bench
85
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
86
+ @torch.inference_mode()
87
+ def test_audio_encoder_forward(
88
+ audio_encoder: AudioEncoder,
89
+ audio_decoder: AudioDecoder,
90
+ bench_run: Callable[..., BenchmarkResult],
91
+ size: InputSize,
92
+ ) -> None:
93
+ """Benchmark AudioEncoder forward pass (spectrogram → latent)."""
94
+ spectrogram = _make_spectrogram(audio_decoder, size)
95
+
96
+ def _encode() -> None:
97
+ with nvtx_range("audio_encoder_forward"):
98
+ audio_encoder(spectrogram)
99
+
100
+ bench_run(_encode, bench_type="audio_encoder_forward", model_name=_MODEL_NAME, resolution=_shape_label(spectrogram))
101
+
102
+
103
+ @pytest.mark.bench
104
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
105
+ @torch.inference_mode()
106
+ def test_vocoder_forward(
107
+ vocoder: Vocoder,
108
+ audio_decoder: AudioDecoder,
109
+ bench_run: Callable[..., BenchmarkResult],
110
+ size: InputSize,
111
+ ) -> None:
112
+ """Benchmark vocoder (HiFi-GAN) forward pass (spectrogram → waveform)."""
113
+ spectrogram = _make_spectrogram(audio_decoder, size)
114
+
115
+ def _forward() -> None:
116
+ with nvtx_range("vocoder_forward"):
117
+ vocoder(spectrogram)
118
+
119
+ bench_run(_forward, bench_type="vocoder_forward", model_name=_MODEL_NAME, resolution=_shape_label(spectrogram))
ltx-2-internal/packages/ltx-bench/benchmarks/components/test_component_loading.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: model loading time for each component (safetensors → GPU)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from benchmarks.conftest import warmup_io
11
+ from ltx_bench.core import BenchmarkResult
12
+
13
+ from .conftest import (
14
+ CHECKPOINT_PATH,
15
+ GEMMA_ROOT_PATH,
16
+ UPSAMPLER_PATH,
17
+ build_audio_decoder,
18
+ build_audio_encoder,
19
+ build_text_encoder,
20
+ build_transformer,
21
+ build_upsampler,
22
+ build_video_decoder,
23
+ build_video_encoder,
24
+ build_vocoder,
25
+ )
26
+
27
+ _CP = Path(CHECKPOINT_PATH).stem
28
+ _UP = Path(UPSAMPLER_PATH).stem
29
+ _GP = Path(GEMMA_ROOT_PATH).name
30
+
31
+ _LOAD_CASES: list[tuple[str, str, Callable[[], object]]] = [
32
+ ("text_encoder_load", _GP, lambda: build_text_encoder(CHECKPOINT_PATH, GEMMA_ROOT_PATH)),
33
+ ("transformer_load", _CP, lambda: build_transformer(CHECKPOINT_PATH)),
34
+ ("video_encoder_load", _CP, lambda: build_video_encoder(CHECKPOINT_PATH)),
35
+ ("video_decoder_load", _CP, lambda: build_video_decoder(CHECKPOINT_PATH)),
36
+ ("audio_encoder_load", _CP, lambda: build_audio_encoder(CHECKPOINT_PATH)),
37
+ ("audio_decoder_load", _CP, lambda: build_audio_decoder(CHECKPOINT_PATH)),
38
+ ("vocoder_load", _CP, lambda: build_vocoder(CHECKPOINT_PATH)),
39
+ ("upsampler_load", _UP, lambda: build_upsampler(UPSAMPLER_PATH)),
40
+ ]
41
+
42
+
43
+ @pytest.fixture(scope="module", autouse=True)
44
+ def _warmup_page_cache() -> None:
45
+ """Read checkpoint files into OS page cache before loading benchmarks."""
46
+ warmup_io()
47
+
48
+
49
+ @pytest.mark.bench
50
+ @pytest.mark.parametrize(
51
+ ("bench_type", "model_name", "builder"),
52
+ _LOAD_CASES,
53
+ ids=[c[0] for c in _LOAD_CASES],
54
+ )
55
+ def test_component_load(
56
+ bench_type: str,
57
+ model_name: str,
58
+ builder: Callable[[], object],
59
+ bench_run: Callable[..., BenchmarkResult],
60
+ ) -> None:
61
+ """Benchmark loading a single component from safetensors to GPU."""
62
+ bench_run(
63
+ builder,
64
+ bench_type=bench_type,
65
+ model_name=model_name,
66
+ warmup=0,
67
+ repeats=1,
68
+ )
ltx-2-internal/packages/ltx-bench/benchmarks/components/test_lora_fusion.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: static LoRA fusion via ``apply_loras()`` with different LoRA sizes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ import torch
10
+
11
+ from ltx_bench.core import BenchmarkResult
12
+ from ltx_core.loader import LTXV_LORA_COMFY_RENAMING_MAP
13
+ from ltx_core.loader.fuse_loras import apply_loras
14
+ from ltx_core.loader.primitives import LoraStateDictWithStrength, StateDict
15
+ from ltx_core.loader.sft_loader import SafetensorsModelStateDictLoader
16
+ from ltx_core.model.transformer import LTXV_MODEL_COMFY_RENAMING_MAP
17
+
18
+ from .conftest import CHECKPOINT_PATH, DISTILLED_LORA_PATH, IC_LORA_PATH
19
+
20
+ _LOADER = SafetensorsModelStateDictLoader()
21
+
22
+ _LORA_CASES = [
23
+ ("lora_fusion_distilled", DISTILLED_LORA_PATH),
24
+ ("lora_fusion_ic_lora", IC_LORA_PATH),
25
+ ]
26
+
27
+
28
+ @pytest.fixture(scope="module")
29
+ def model_sd() -> StateDict:
30
+ """Load the transformer state dict on GPU, matching production (SingleGPUModelBuilder)."""
31
+ return _LOADER.load([CHECKPOINT_PATH], sd_ops=LTXV_MODEL_COMFY_RENAMING_MAP, device=torch.device("cuda"))
32
+
33
+
34
+ @pytest.mark.bench
35
+ @pytest.mark.parametrize(
36
+ ("bench_type", "lora_path"),
37
+ _LORA_CASES,
38
+ ids=[c[0] for c in _LORA_CASES],
39
+ )
40
+ def test_lora_fusion(
41
+ bench_type: str,
42
+ lora_path: str,
43
+ model_sd: StateDict,
44
+ bench_run: Callable[..., BenchmarkResult],
45
+ ) -> None:
46
+ """Benchmark static LoRA fusion (apply_loras) with a single LoRA checkpoint."""
47
+ lora_sd = _LOADER.load([lora_path], sd_ops=LTXV_LORA_COMFY_RENAMING_MAP)
48
+ lora_pairs = [LoraStateDictWithStrength(lora_sd, 1.0)]
49
+
50
+ bench_run(
51
+ lambda: apply_loras(model_sd, lora_pairs, dtype=torch.bfloat16, destination_sd=model_sd),
52
+ bench_type=bench_type,
53
+ model_name=Path(CHECKPOINT_PATH).stem,
54
+ warmup=1,
55
+ repeats=3,
56
+ )
ltx-2-internal/packages/ltx-bench/benchmarks/components/test_text_encoder.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: Gemma 3 text encoder forward pass (positive + negative prompt)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ import torch
10
+
11
+ from ltx_bench.core import BenchmarkResult
12
+ from ltx_bench.nvtx import nvtx_range
13
+ from ltx_core.text_encoders.gemma.encoders.base_encoder import GemmaTextEncoder
14
+ from ltx_pipelines.utils.constants import DEFAULT_NEGATIVE_PROMPT
15
+
16
+ from .conftest import CHECKPOINT_PATH, GEMMA_ROOT_PATH, build_text_encoder
17
+
18
+ PROMPT = (
19
+ "A medium close-up shot of a person speaking directly to the camera with natural lighting "
20
+ "and a softly blurred background."
21
+ )
22
+
23
+
24
+ @pytest.fixture(scope="module")
25
+ def text_encoder() -> GemmaTextEncoder:
26
+ return build_text_encoder(CHECKPOINT_PATH, GEMMA_ROOT_PATH)
27
+
28
+
29
+ @pytest.mark.bench
30
+ @torch.inference_mode()
31
+ def test_text_encoder_forward(
32
+ text_encoder: GemmaTextEncoder,
33
+ bench_run: Callable[..., BenchmarkResult],
34
+ ) -> None:
35
+ """Benchmark Gemma text encoder: encode positive + negative prompt.
36
+
37
+ Uses ``encode_text()`` which is the same function the pipeline calls,
38
+ encoding both prompts sequentially.
39
+ """
40
+
41
+ def _forward() -> None:
42
+ with nvtx_range("text_encoder_forward"):
43
+ [text_encoder.encode(p) for p in [PROMPT, DEFAULT_NEGATIVE_PROMPT]]
44
+
45
+ bench_run(_forward, bench_type="text_encoder_forward", model_name=Path(GEMMA_ROOT_PATH).name)
ltx-2-internal/packages/ltx-bench/benchmarks/components/test_transformer.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: transformer (X0Model) forward pass using production Modality construction.
2
+
3
+ Measures GPU time for a single denoising step through the 48-layer
4
+ dual-stream transformer. Tests video-only and audio+video variants.
5
+ Input latent states are sized to match stage-1 half-resolution.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable, Generator
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+ import torch
15
+
16
+ from benchmarks.conftest import COMPONENT_SIZES, InputSize
17
+ from ltx_bench.core import BenchmarkResult
18
+ from ltx_bench.nvtx import nvtx_range
19
+ from ltx_core.block_streaming import DISK_CPU_SLOTS, BlockStreamingWrapper, StreamingModelBuilder
20
+ from ltx_core.components.patchifiers import AudioPatchifier, VideoLatentPatchifier, VideoLatentShape
21
+ from ltx_core.guidance.perturbations import BatchedPerturbationConfig, PerturbationConfig
22
+ from ltx_core.model.transformer import LTXV_MODEL_COMFY_RENAMING_MAP, LTXModelConfigurator
23
+ from ltx_core.model.transformer.model import X0Model
24
+ from ltx_core.tools import AudioLatentTools, VideoLatentTools
25
+ from ltx_core.types import AudioLatentShape, VideoPixelShape
26
+ from ltx_pipelines.utils.constants import VIDEO_LATENT_CHANNELS, VIDEO_SCALE_FACTORS
27
+ from ltx_pipelines.utils.denoisers import _repeat_state
28
+ from ltx_pipelines.utils.helpers import modality_from_latent_state
29
+
30
+ from .conftest import (
31
+ CHECKPOINT_PATH,
32
+ build_transformer,
33
+ make_video_latent_state,
34
+ )
35
+
36
+ _MODEL_NAME = Path(CHECKPOINT_PATH).stem
37
+ _FPS = 24.0
38
+
39
+ # Dummy context token count for benchmarking (roughly matches a typical short prompt).
40
+ _CONTEXT_TOKENS = 64
41
+
42
+
43
+ @pytest.fixture(scope="module")
44
+ def transformer() -> X0Model:
45
+ return build_transformer(CHECKPOINT_PATH)
46
+
47
+
48
+ def _make_av_modalities(
49
+ transformer: torch.nn.Module,
50
+ height: int,
51
+ width: int,
52
+ num_frames: int,
53
+ batch: int,
54
+ ) -> tuple:
55
+ """Build batched video + audio modalities and return (video, audio, perturbations, total_tokens)."""
56
+ model = transformer.velocity_model
57
+ device = next(model.parameters()).device
58
+ dtype = next(model.parameters()).dtype
59
+
60
+ pixel_shape = VideoPixelShape(batch=1, frames=num_frames, height=height, width=width, fps=_FPS)
61
+ v_shape = VideoLatentShape.from_pixel_shape(pixel_shape, VIDEO_LATENT_CHANNELS, VIDEO_SCALE_FACTORS)
62
+ a_shape = AudioLatentShape.from_video_pixel_shape(pixel_shape)
63
+
64
+ v_tools = VideoLatentTools(VideoLatentPatchifier(patch_size=1), v_shape, _FPS)
65
+ a_tools = AudioLatentTools(AudioPatchifier(patch_size=1), a_shape)
66
+
67
+ video_state = v_tools.create_initial_state(device=device, dtype=dtype)
68
+ audio_state = a_tools.create_initial_state(device=device, dtype=dtype)
69
+
70
+ v_context_dim = model.transformer_blocks[0].attn2.to_k.in_features
71
+ a_context_dim = model.transformer_blocks[0].audio_attn2.to_k.in_features
72
+
73
+ sigma = torch.full((batch,), 0.5, device=device, dtype=torch.float32)
74
+ v_context = torch.randn(batch, _CONTEXT_TOKENS, v_context_dim, device=device, dtype=dtype)
75
+ a_context = torch.randn(batch, _CONTEXT_TOKENS, a_context_dim, device=device, dtype=dtype)
76
+
77
+ video = modality_from_latent_state(_repeat_state(video_state, batch), v_context, sigma)
78
+ audio = modality_from_latent_state(_repeat_state(audio_state, batch), a_context, sigma)
79
+ perturbations = BatchedPerturbationConfig([PerturbationConfig.empty()] * batch)
80
+
81
+ total_tokens = video.latent.shape[1] + audio.latent.shape[1] # per batch item
82
+ return video, audio, perturbations, total_tokens
83
+
84
+
85
+ @pytest.mark.bench
86
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
87
+ @torch.inference_mode()
88
+ def test_transformer_forward_video_only(
89
+ transformer: X0Model,
90
+ bench_run: Callable[..., BenchmarkResult],
91
+ size: InputSize,
92
+ ) -> None:
93
+ """Benchmark X0Model forward pass with video-only input (no audio).
94
+
95
+ Uses ``modality_from_latent_state()`` to construct the Modality, matching
96
+ the production code path exactly.
97
+ """
98
+ model = transformer.velocity_model
99
+ device = next(model.parameters()).device
100
+ dtype = next(model.parameters()).dtype
101
+
102
+ video_state = make_video_latent_state(size, device=device, dtype=dtype)
103
+ context_dim = model.transformer_blocks[0].attn2.to_k.in_features
104
+ context = torch.randn(1, _CONTEXT_TOKENS, context_dim, device=device, dtype=dtype)
105
+ sigma = torch.tensor([0.5], device=device, dtype=torch.float32)
106
+ perturbations = BatchedPerturbationConfig.empty(1)
107
+
108
+ video = modality_from_latent_state(video_state, context, sigma)
109
+
110
+ def _forward() -> None:
111
+ with nvtx_range("transformer_forward_video_only"):
112
+ transformer(video, None, perturbations)
113
+
114
+ bench_run(
115
+ _forward,
116
+ bench_type="transformer_forward_video_only",
117
+ model_name=_MODEL_NAME,
118
+ resolution=size.label,
119
+ )
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Batched forward-pass benchmarks — data for throughput vs token-count graphs.
124
+ #
125
+ # Measures a single transformer forward call at varying batch sizes and
126
+ # resolutions (token counts), with and without layer streaming.
127
+ # To compare sequential (4 x B=1) vs batched (1 x B=4) step time,
128
+ # multiply the B=1 result by the number of guidance passes.
129
+ # ---------------------------------------------------------------------------
130
+
131
+ _BATCH_SIZES = [
132
+ pytest.param(1, id="B1"),
133
+ pytest.param(2, id="B2"),
134
+ pytest.param(4, id="B4"),
135
+ ]
136
+
137
+
138
+ @pytest.mark.bench
139
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
140
+ @pytest.mark.parametrize("batch", _BATCH_SIZES)
141
+ @torch.inference_mode()
142
+ def test_transformer_av_batch(
143
+ transformer: X0Model,
144
+ bench_run: Callable[..., BenchmarkResult],
145
+ size: InputSize,
146
+ batch: int,
147
+ ) -> None:
148
+ """Benchmark a single AV forward call at a given batch size (no streaming)."""
149
+ video, audio, perturbations, total_tokens = _make_av_modalities(
150
+ transformer, size.height, size.width, size.num_frames, batch=batch
151
+ )
152
+
153
+ def _forward() -> None:
154
+ with nvtx_range(f"av_b{batch}"):
155
+ transformer(video, audio, perturbations)
156
+
157
+ result = bench_run(
158
+ _forward,
159
+ warmup=1,
160
+ repeats=3,
161
+ bench_type=f"transformer_av_b{batch}",
162
+ model_name=_MODEL_NAME,
163
+ resolution=size.label,
164
+ )
165
+ result.extra["total_tokens"] = total_tokens
166
+ result.extra["batch"] = batch
167
+ result.extra["streaming"] = False
168
+
169
+
170
+ def _build_streaming_wrapper(
171
+ cpu_slots_count: int | None = None,
172
+ gpu_slots_count: int | None = None,
173
+ ) -> BlockStreamingWrapper:
174
+ """Build a streaming wrapper for the transformer checkpoint."""
175
+ builder = StreamingModelBuilder(
176
+ model_class_configurator=LTXModelConfigurator,
177
+ model_path=CHECKPOINT_PATH,
178
+ model_sd_ops=LTXV_MODEL_COMFY_RENAMING_MAP,
179
+ blocks_attr="velocity_model.transformer_blocks",
180
+ blocks_prefix="transformer_blocks",
181
+ state_dict_prefix="velocity_model.",
182
+ model_wrapper=lambda m: X0Model(m).eval(),
183
+ )
184
+ return builder.build(
185
+ target_device=torch.device("cuda"),
186
+ dtype=torch.bfloat16,
187
+ cpu_slots_count=cpu_slots_count,
188
+ gpu_slots_count=gpu_slots_count,
189
+ )
190
+
191
+
192
+ @pytest.fixture(scope="module")
193
+ def streaming_transformer() -> Generator[BlockStreamingWrapper, None, None]:
194
+ """Module-scoped RAM-streaming wrapper."""
195
+ wrapped = _build_streaming_wrapper()
196
+ yield wrapped
197
+ wrapped.teardown()
198
+
199
+
200
+ @pytest.fixture(scope="module")
201
+ def disk_streaming_transformer() -> Generator[BlockStreamingWrapper, None, None]:
202
+ """Module-scoped disk-streaming wrapper."""
203
+ wrapped = _build_streaming_wrapper(cpu_slots_count=DISK_CPU_SLOTS)
204
+ yield wrapped
205
+ wrapped.teardown()
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # AV batch — RAM streaming
210
+ # ---------------------------------------------------------------------------
211
+
212
+
213
+ @pytest.mark.bench
214
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
215
+ @pytest.mark.parametrize("batch", _BATCH_SIZES)
216
+ @torch.inference_mode()
217
+ def test_transformer_av_batch_streaming(
218
+ streaming_transformer: BlockStreamingWrapper,
219
+ bench_run: Callable[..., BenchmarkResult],
220
+ size: InputSize,
221
+ batch: int,
222
+ ) -> None:
223
+ """Benchmark a single AV forward call at a given batch size (with RAM streaming)."""
224
+
225
+ video, audio, perturbations, total_tokens = _make_av_modalities(
226
+ streaming_transformer, size.height, size.width, size.num_frames, batch=batch
227
+ )
228
+
229
+ def _forward() -> None:
230
+ with nvtx_range(f"av_b{batch}_streaming"):
231
+ streaming_transformer(video, audio, perturbations)
232
+
233
+ result = bench_run(
234
+ _forward,
235
+ warmup=1,
236
+ repeats=3,
237
+ bench_type=f"transformer_av_b{batch}_streaming",
238
+ model_name=_MODEL_NAME,
239
+ resolution=size.label,
240
+ )
241
+ result.extra["total_tokens"] = total_tokens
242
+ result.extra["batch"] = batch
243
+ result.extra["streaming"] = True
244
+
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # AV batch — disk streaming
248
+ # ---------------------------------------------------------------------------
249
+
250
+
251
+ @pytest.mark.bench
252
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
253
+ @pytest.mark.parametrize("batch", _BATCH_SIZES)
254
+ @torch.inference_mode()
255
+ def test_transformer_av_batch_disk_streaming(
256
+ disk_streaming_transformer: BlockStreamingWrapper,
257
+ bench_run: Callable[..., BenchmarkResult],
258
+ size: InputSize,
259
+ batch: int,
260
+ ) -> None:
261
+ """Benchmark a single AV forward call at a given batch size (with disk streaming)."""
262
+
263
+ video, audio, perturbations, total_tokens = _make_av_modalities(
264
+ disk_streaming_transformer, size.height, size.width, size.num_frames, batch=batch
265
+ )
266
+
267
+ def _forward() -> None:
268
+ with nvtx_range(f"av_b{batch}_disk_streaming"):
269
+ disk_streaming_transformer(video, audio, perturbations)
270
+
271
+ result = bench_run(
272
+ _forward,
273
+ warmup=1,
274
+ repeats=3,
275
+ bench_type=f"transformer_av_b{batch}_disk_streaming",
276
+ model_name=_MODEL_NAME,
277
+ resolution=size.label,
278
+ )
279
+ result.extra["total_tokens"] = total_tokens
280
+ result.extra["batch"] = batch
281
+ result.extra["streaming"] = True
282
+ result.extra["disk"] = True
ltx-2-internal/packages/ltx-bench/benchmarks/components/test_upsampler.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: spatial latent upsampler 2x using the production ``upsample_video`` wrapper.
2
+
3
+ Measures GPU time for the learned 2x spatial upsampling of video latents,
4
+ including the per-channel un-normalize / normalize steps. Input latents are
5
+ sized to match stage-1 half-resolution (output is full pipeline resolution).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+ import torch
15
+
16
+ from benchmarks.conftest import COMPONENT_SIZES, InputSize, video_latent_torch_shape
17
+ from ltx_bench.core import BenchmarkResult
18
+ from ltx_bench.nvtx import nvtx_range
19
+ from ltx_core.model.upsampler import LatentUpsampler, upsample_video
20
+ from ltx_core.model.video_vae import VideoEncoder
21
+
22
+ from .conftest import (
23
+ CHECKPOINT_PATH,
24
+ UPSAMPLER_PATH,
25
+ build_upsampler,
26
+ build_video_encoder,
27
+ )
28
+
29
+
30
+ @pytest.fixture(scope="module")
31
+ def upsampler() -> LatentUpsampler:
32
+ return build_upsampler(UPSAMPLER_PATH)
33
+
34
+
35
+ @pytest.fixture(scope="module")
36
+ def video_encoder() -> VideoEncoder:
37
+ return build_video_encoder(CHECKPOINT_PATH)
38
+
39
+
40
+ @pytest.mark.bench
41
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
42
+ @torch.inference_mode()
43
+ def test_spatial_upsampler(
44
+ upsampler: LatentUpsampler,
45
+ video_encoder: VideoEncoder,
46
+ bench_run: Callable[..., BenchmarkResult],
47
+ size: InputSize,
48
+ ) -> None:
49
+ """Benchmark spatial upsampler 2x using the production ``upsample_video`` wrapper.
50
+
51
+ Includes the per-channel un-normalize / normalize steps that the
52
+ real pipeline applies around the upsampler.
53
+ """
54
+ device = next(upsampler.parameters()).device
55
+ dtype = next(upsampler.parameters()).dtype
56
+ latents = torch.randn(*video_latent_torch_shape(size), device=device, dtype=dtype)
57
+
58
+ def _upsample() -> None:
59
+ with nvtx_range("upsampler_forward"):
60
+ upsample_video(latents, video_encoder, upsampler)
61
+
62
+ bench_run(
63
+ _upsample,
64
+ bench_type="upsampler_forward",
65
+ model_name=Path(UPSAMPLER_PATH).stem,
66
+ resolution=size.label,
67
+ )
ltx-2-internal/packages/ltx-bench/benchmarks/components/test_video_vae.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: video VAE decoder and encoder (tiled and non-tiled).
2
+
3
+ Measures GPU time for video latent → pixel (decode) and pixel → latent (encode)
4
+ paths. Input tensors are sized to match stage-1 half-resolution latents.
5
+ Non-tiled variants run at the smallest size only (larger sizes OOM).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+ import torch
15
+
16
+ from benchmarks.conftest import COMPONENT_SIZES, InputSize, pixel_torch_shape, video_latent_torch_shape
17
+ from ltx_bench.core import BenchmarkResult
18
+ from ltx_bench.nvtx import nvtx_range
19
+ from ltx_core.model.video_vae import TilingConfig, VideoDecoder, VideoEncoder
20
+ from ltx_pipelines.utils.constants import LTX_2_3_PARAMS
21
+
22
+ from .conftest import (
23
+ CHECKPOINT_PATH,
24
+ build_video_decoder,
25
+ build_video_encoder,
26
+ )
27
+
28
+ _MODEL_NAME = Path(CHECKPOINT_PATH).stem
29
+ HALF_1080P_121 = InputSize(544, 960, 121)
30
+
31
+
32
+ @pytest.fixture(scope="module")
33
+ def video_decoder() -> VideoDecoder:
34
+ return build_video_decoder(CHECKPOINT_PATH)
35
+
36
+
37
+ @pytest.fixture(scope="module")
38
+ def video_encoder() -> VideoEncoder:
39
+ return build_video_encoder(CHECKPOINT_PATH)
40
+
41
+
42
+ # --- Decode ---
43
+
44
+
45
+ @pytest.mark.bench
46
+ @torch.inference_mode()
47
+ def test_video_vae_decode_non_tiled(
48
+ video_decoder: VideoDecoder,
49
+ bench_run: Callable[..., BenchmarkResult],
50
+ ) -> None:
51
+ """Benchmark non-tiled video VAE decoding (HALF_1080P_121 only — larger sizes OOM)."""
52
+ device = next(video_decoder.parameters()).device
53
+ dtype = next(video_decoder.parameters()).dtype
54
+ latents = torch.randn(*video_latent_torch_shape(HALF_1080P_121), device=device, dtype=dtype)
55
+ generator = torch.Generator(device=device).manual_seed(LTX_2_3_PARAMS.seed)
56
+
57
+ def _decode() -> None:
58
+ with nvtx_range("video_decoder_non_tiled"):
59
+ for _ in video_decoder.decode_video(latents, None, generator=generator):
60
+ pass
61
+
62
+ bench_run(_decode, bench_type="video_decoder_non_tiled", model_name=_MODEL_NAME, resolution=HALF_1080P_121.label)
63
+
64
+
65
+ @pytest.mark.bench
66
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
67
+ @torch.inference_mode()
68
+ def test_video_vae_decode_tiled(
69
+ video_decoder: VideoDecoder,
70
+ bench_run: Callable[..., BenchmarkResult],
71
+ size: InputSize,
72
+ ) -> None:
73
+ """Benchmark tiled video VAE decoding across all sizes."""
74
+ device = next(video_decoder.parameters()).device
75
+ dtype = next(video_decoder.parameters()).dtype
76
+ latents = torch.randn(*video_latent_torch_shape(size), device=device, dtype=dtype)
77
+ generator = torch.Generator(device=device).manual_seed(LTX_2_3_PARAMS.seed)
78
+ tiling_config = TilingConfig.default()
79
+
80
+ def _decode() -> None:
81
+ with nvtx_range("video_decoder_tiled"):
82
+ for _ in video_decoder.decode_video(latents, tiling_config, generator=generator):
83
+ pass
84
+
85
+ bench_run(_decode, bench_type="video_decoder_tiled", model_name=_MODEL_NAME, resolution=size.label)
86
+
87
+
88
+ # --- Encode ---
89
+
90
+
91
+ @pytest.mark.bench
92
+ @torch.inference_mode()
93
+ def test_video_vae_encode_non_tiled(
94
+ video_encoder: VideoEncoder,
95
+ bench_run: Callable[..., BenchmarkResult],
96
+ ) -> None:
97
+ """Benchmark non-tiled video VAE encoding (HALF_1080P_121 only — larger sizes OOM)."""
98
+ device = next(video_encoder.parameters()).device
99
+ dtype = next(video_encoder.parameters()).dtype
100
+ pixels = torch.randn(*pixel_torch_shape(HALF_1080P_121), device=device, dtype=dtype)
101
+
102
+ def _encode() -> None:
103
+ with nvtx_range("video_encoder_non_tiled"):
104
+ video_encoder(pixels)
105
+
106
+ bench_run(_encode, bench_type="video_encoder_non_tiled", model_name=_MODEL_NAME, resolution=HALF_1080P_121.label)
107
+
108
+
109
+ @pytest.mark.bench
110
+ @pytest.mark.parametrize("size", COMPONENT_SIZES, ids=[s.label for s in COMPONENT_SIZES])
111
+ @torch.inference_mode()
112
+ def test_video_vae_encode_tiled(
113
+ video_encoder: VideoEncoder,
114
+ bench_run: Callable[..., BenchmarkResult],
115
+ size: InputSize,
116
+ ) -> None:
117
+ """Benchmark tiled video VAE encoding across all sizes."""
118
+ device = next(video_encoder.parameters()).device
119
+ dtype = next(video_encoder.parameters()).dtype
120
+ pixels = torch.randn(*pixel_torch_shape(size), device=device, dtype=dtype)
121
+ tiling_config = TilingConfig.default()
122
+
123
+ def _encode() -> None:
124
+ with nvtx_range("video_encoder_tiled"):
125
+ video_encoder.tiled_encode(pixels, tiling_config)
126
+
127
+ bench_run(_encode, bench_type="video_encoder_tiled", model_name=_MODEL_NAME, resolution=size.label)
ltx-2-internal/packages/ltx-bench/benchmarks/conftest.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model configuration, fixtures, and helpers for benchmark tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ import torch
9
+
10
+ from conftest import (
11
+ GEMMA_ROOT,
12
+ LTX_2_3_CHECKPOINT_PATH,
13
+ LTX_2_3_DISTILLED_CHECKPOINT_PATH,
14
+ LTX_2_3_DISTILLED_LORA_PATH,
15
+ LTX_2_3_IC_LORA_PATH,
16
+ LTX_2_3_SPATIAL_UPSAMPLER_PATH,
17
+ )
18
+ from ltx_core.components.noisers import GaussianNoiser
19
+ from ltx_core.components.patchifiers import AudioPatchifier, VideoLatentPatchifier, VideoLatentShape
20
+ from ltx_core.tools import AudioLatentTools, VideoLatentTools
21
+ from ltx_core.types import AudioLatentShape, LatentState, VideoPixelShape
22
+ from ltx_pipelines.utils.constants import (
23
+ VIDEO_LATENT_CHANNELS,
24
+ VIDEO_SCALE_FACTORS,
25
+ )
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Input sizes
29
+ # ---------------------------------------------------------------------------
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class InputSize:
34
+ """Video generation target dimensions."""
35
+
36
+ height: int
37
+ width: int
38
+ num_frames: int
39
+ frame_rate: float = 24.0
40
+
41
+ @property
42
+ def label(self) -> str:
43
+ return f"{self.height}x{self.width}x{self.num_frames}"
44
+
45
+
46
+ # Pipeline sizes specify video output (height, width, frames) for end-to-end benchmarks.
47
+ PIPELINE_SIZES = [
48
+ InputSize(1088, 1920, 121), # 1080p 121 frames ~ 5s @ 24 fps
49
+ InputSize(1088, 1920, 241), # 1080p 241 frames ~ 10s @ 24 fps
50
+ InputSize(1472, 2560, 121), # 1440p 121 frames ~ 5s @ 24 fps
51
+ InputSize(1472, 2560, 241), # 1440p 241 frames ~ 10s @ 24 fps
52
+ ]
53
+
54
+ # Component sizes measure the model's latent "base tile" (2x downsampled from pipeline output).
55
+ COMPONENT_SIZES = [InputSize(s.height // 2, s.width // 2, s.num_frames) for s in PIPELINE_SIZES]
56
+
57
+ # Model paths resolved at import time.
58
+ CHECKPOINT_PATH = LTX_2_3_CHECKPOINT_PATH.resolve().as_posix()
59
+ DISTILLED_CHECKPOINT = LTX_2_3_DISTILLED_CHECKPOINT_PATH.resolve().as_posix()
60
+ GEMMA_ROOT_PATH = str(GEMMA_ROOT) if GEMMA_ROOT.exists() else ""
61
+ UPSAMPLER_PATH = str(LTX_2_3_SPATIAL_UPSAMPLER_PATH) if LTX_2_3_SPATIAL_UPSAMPLER_PATH.exists() else ""
62
+ DISTILLED_LORA_PATH = LTX_2_3_DISTILLED_LORA_PATH.resolve().as_posix()
63
+ IC_LORA_PATH = LTX_2_3_IC_LORA_PATH.resolve().as_posix()
64
+
65
+ PIPELINE_ASSETS_DIR = Path(__file__).resolve().parent.parent.parent / "ltx-pipelines" / "tests" / "assets"
66
+ if not PIPELINE_ASSETS_DIR.exists():
67
+ import warnings
68
+
69
+ warnings.warn(f"Pipeline assets not found at {PIPELINE_ASSETS_DIR}", stacklevel=1)
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # IO warmup — read checkpoint files into OS page cache so first benchmark
74
+ # iteration is not penalised by network/disk IO.
75
+ # ---------------------------------------------------------------------------
76
+ def _read_chunked(path: Path, chunk_size: int = 64 * 1024 * 1024) -> None:
77
+ """Read a file in chunks to warm OS page cache without allocating the full file in Python memory."""
78
+ with path.open("rb") as f:
79
+ while f.read(chunk_size):
80
+ pass
81
+
82
+
83
+ def warmup_io() -> None:
84
+ """Read checkpoint and text-encoder files into OS page cache."""
85
+ for p in [LTX_2_3_CHECKPOINT_PATH, LTX_2_3_DISTILLED_CHECKPOINT_PATH, LTX_2_3_SPATIAL_UPSAMPLER_PATH]:
86
+ if p.exists():
87
+ _read_chunked(p)
88
+ if GEMMA_ROOT.exists():
89
+ for p in GEMMA_ROOT.rglob("*.safetensors"):
90
+ _read_chunked(p)
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Shape computation from InputSize
95
+ # ---------------------------------------------------------------------------
96
+
97
+
98
+ def pixel_shape_for(size: InputSize) -> VideoPixelShape:
99
+ """Build a VideoPixelShape from an InputSize."""
100
+ return VideoPixelShape(
101
+ batch=1,
102
+ frames=size.num_frames,
103
+ width=size.width,
104
+ height=size.height,
105
+ fps=size.frame_rate,
106
+ )
107
+
108
+
109
+ def video_latent_shape_for(size: InputSize) -> VideoLatentShape:
110
+ """Compute the video latent shape for a given InputSize."""
111
+ return VideoLatentShape.from_pixel_shape(
112
+ pixel_shape_for(size),
113
+ latent_channels=VIDEO_LATENT_CHANNELS,
114
+ scale_factors=VIDEO_SCALE_FACTORS,
115
+ )
116
+
117
+
118
+ def audio_latent_shape_for(size: InputSize) -> AudioLatentShape:
119
+ """Compute the audio latent shape for a given InputSize."""
120
+ return AudioLatentShape.from_video_pixel_shape(pixel_shape_for(size))
121
+
122
+
123
+ def video_latent_torch_shape(size: InputSize) -> tuple[int, ...]:
124
+ """Return the (B, C, T, H, W) tuple for torch.randn."""
125
+ return video_latent_shape_for(size).to_torch_shape()
126
+
127
+
128
+ def pixel_torch_shape(size: InputSize) -> tuple[int, ...]:
129
+ """Return the (B, C, T, H, W) pixel tensor shape."""
130
+ return (1, 3, size.num_frames, size.height, size.width)
131
+
132
+
133
+ def audio_latent_torch_shape(size: InputSize) -> tuple[int, ...]:
134
+ """Return the (B, C, T, M) audio latent tensor shape."""
135
+ return audio_latent_shape_for(size).to_torch_shape()
136
+
137
+
138
+ DEFAULT_PATCH_SIZE = 1
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Latent state helpers
143
+ # ---------------------------------------------------------------------------
144
+
145
+
146
+ def make_video_latent_state(
147
+ size: InputSize,
148
+ *,
149
+ device: torch.device,
150
+ dtype: torch.dtype,
151
+ patch_size: int = DEFAULT_PATCH_SIZE,
152
+ ) -> LatentState:
153
+ """Create a patchified video LatentState for a given InputSize."""
154
+ shape = video_latent_shape_for(size)
155
+ tools = VideoLatentTools(
156
+ patchifier=VideoLatentPatchifier(patch_size=patch_size),
157
+ target_shape=shape,
158
+ fps=size.frame_rate,
159
+ )
160
+ return tools.create_initial_state(device=device, dtype=dtype)
161
+
162
+
163
+ def make_audio_latent_state(
164
+ size: InputSize,
165
+ *,
166
+ device: torch.device,
167
+ dtype: torch.dtype,
168
+ patch_size: int = DEFAULT_PATCH_SIZE,
169
+ ) -> LatentState:
170
+ """Create a patchified audio LatentState for a given InputSize."""
171
+ audio_shape = audio_latent_shape_for(size)
172
+ tools = AudioLatentTools(
173
+ AudioPatchifier(patch_size=patch_size),
174
+ audio_shape,
175
+ )
176
+ return tools.create_initial_state(device=device, dtype=dtype)
177
+
178
+
179
+ def make_noised_states(
180
+ size: InputSize,
181
+ *,
182
+ device: torch.device,
183
+ dtype: torch.dtype,
184
+ seed: int = 42,
185
+ ) -> tuple[LatentState, LatentState]:
186
+ """Create noised video and audio LatentStates for a given InputSize."""
187
+ noiser = GaussianNoiser(generator=torch.Generator(device=device).manual_seed(seed))
188
+ video_state = noiser(make_video_latent_state(size, device=device, dtype=dtype))
189
+ audio_state = noiser(make_audio_latent_state(size, device=device, dtype=dtype))
190
+ return video_state, audio_state
ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/__init__.py ADDED
File without changes
ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/conftest.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """Re-export benchmark helpers for pipeline tests."""
2
+
3
+ from benchmarks.conftest import ( # noqa: F401
4
+ CHECKPOINT_PATH,
5
+ DISTILLED_CHECKPOINT,
6
+ DISTILLED_LORA_PATH,
7
+ GEMMA_ROOT_PATH,
8
+ PIPELINE_ASSETS_DIR,
9
+ UPSAMPLER_PATH,
10
+ )
ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/test_pipeline_distilled.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: full DistilledPipeline end-to-end (8-step fast inference)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ import torch
10
+
11
+ from benchmarks.conftest import PIPELINE_SIZES, InputSize
12
+ from ltx_bench import regions
13
+ from ltx_bench.core import BenchmarkResult
14
+ from ltx_bench.instrument import instrument
15
+ from ltx_bench.nvtx import nvtx_range
16
+ from ltx_core.model.audio_vae.audio_vae import decode_audio
17
+ from ltx_core.model.upsampler.model import upsample_video
18
+ from ltx_core.model.video_vae import TilingConfig
19
+ from ltx_core.model.video_vae.video_vae import VideoDecoder
20
+ from ltx_core.text_encoders.gemma.encoders.base_encoder import GemmaTextEncoder
21
+ from ltx_pipelines.distilled import DistilledPipeline
22
+ from ltx_pipelines.utils.blocks import DiffusionStage
23
+ from ltx_pipelines.utils.constants import LTX_2_3_PARAMS
24
+ from ltx_pipelines.utils.samplers import euler_denoising_loop
25
+
26
+ from .conftest import DISTILLED_CHECKPOINT, GEMMA_ROOT_PATH, UPSAMPLER_PATH
27
+
28
+
29
+ @pytest.fixture(scope="module")
30
+ def pipeline() -> DistilledPipeline:
31
+ return DistilledPipeline(
32
+ distilled_checkpoint_path=DISTILLED_CHECKPOINT,
33
+ spatial_upsampler_path=UPSAMPLER_PATH,
34
+ gemma_root=GEMMA_ROOT_PATH,
35
+ loras=[],
36
+ )
37
+
38
+
39
+ @pytest.mark.bench
40
+ @pytest.mark.parametrize("size", PIPELINE_SIZES, ids=[s.label for s in PIPELINE_SIZES])
41
+ @torch.inference_mode()
42
+ def test_pipeline_distilled_e2e(
43
+ pipeline: DistilledPipeline,
44
+ bench_run: Callable[..., BenchmarkResult],
45
+ size: InputSize,
46
+ ) -> None:
47
+ """Benchmark distilled pipeline end-to-end (8-step schedule).
48
+
49
+ Two-stage generation with distilled sigma schedule.
50
+ """
51
+ tiling_config = TilingConfig.default()
52
+
53
+ def _run() -> None:
54
+ with nvtx_range("pipeline_distilled_e2e"):
55
+ video_frames, _audio = pipeline(
56
+ prompt="A cat sitting on a windowsill watching birds outside.",
57
+ seed=LTX_2_3_PARAMS.seed,
58
+ height=size.height,
59
+ width=size.width,
60
+ num_frames=size.num_frames,
61
+ frame_rate=size.frame_rate,
62
+ images=[],
63
+ tiling_config=tiling_config,
64
+ )
65
+ for _ in video_frames:
66
+ pass
67
+
68
+ instrument.activate()
69
+ instrument.patch(DiffusionStage._build_transformer, regions.MODEL_LOAD)
70
+ instrument.patch(GemmaTextEncoder.encode, regions.TEXT_ENCODING)
71
+ instrument.patch(euler_denoising_loop, regions.DENOISING)
72
+ instrument.patch(VideoDecoder.forward, regions.VAE_VIDEO_DECODE, aggregate=True)
73
+ instrument.patch(decode_audio, regions.VAE_AUDIO_DECODE)
74
+ instrument.patch(upsample_video, regions.SPATIAL_UPSAMPLE)
75
+ instrument.patch_tqdm()
76
+ try:
77
+ result = bench_run(
78
+ _run,
79
+ warmup=0,
80
+ repeats=1,
81
+ bench_type="distilled",
82
+ model_name=Path(DISTILLED_CHECKPOINT).stem,
83
+ resolution=size.label,
84
+ )
85
+ finally:
86
+ instrument.deactivate()
87
+
88
+ instrument.collect_stages(result)
ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/test_pipeline_two_stage.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: TI2VidTwoStagesPipeline — baseline, RAM streaming, disk streaming."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ import torch
10
+
11
+ from ltx_bench import regions
12
+ from ltx_bench.core import BenchmarkResult
13
+ from ltx_bench.instrument import instrument
14
+ from ltx_bench.nvtx import nvtx_range
15
+ from ltx_core.block_streaming.builder import StreamingModelBuilder
16
+ from ltx_core.block_streaming.disk import DiskBlockReader, DiskTensorReader, LoraSource
17
+ from ltx_core.block_streaming.pool import WeightPool
18
+ from ltx_core.block_streaming.provider import WeightsProvider
19
+ from ltx_core.block_streaming.source import DiskWeightSource
20
+ from ltx_core.loader import LTXV_LORA_COMFY_RENAMING_MAP, LoraPathStrengthAndSDOps
21
+ from ltx_core.model.audio_vae.audio_vae import decode_audio
22
+ from ltx_core.model.upsampler.model import upsample_video
23
+ from ltx_core.model.video_vae import TilingConfig
24
+ from ltx_core.model.video_vae.video_vae import VideoDecoder
25
+ from ltx_core.text_encoders.gemma.encoders.base_encoder import GemmaTextEncoder
26
+ from ltx_pipelines.ti2vid_two_stages import TI2VidTwoStagesPipeline
27
+ from ltx_pipelines.utils.blocks import DiffusionStage, PromptEncoder
28
+ from ltx_pipelines.utils.constants import DEFAULT_NEGATIVE_PROMPT, LTX_2_3_PARAMS
29
+ from ltx_pipelines.utils.samplers import euler_denoising_loop
30
+ from ltx_pipelines.utils.types import OffloadMode
31
+
32
+ from .conftest import CHECKPOINT_PATH, DISTILLED_LORA_PATH, GEMMA_ROOT_PATH, UPSAMPLER_PATH
33
+
34
+ _PROMPT = "A cat sitting on a windowsill watching birds outside."
35
+ _RESOLUTION = f"{LTX_2_3_PARAMS.stage_2_height}x{LTX_2_3_PARAMS.stage_2_width}x{LTX_2_3_PARAMS.num_frames}"
36
+
37
+ _DISTILLED_LORA = [
38
+ LoraPathStrengthAndSDOps(DISTILLED_LORA_PATH, 0.6, LTXV_LORA_COMFY_RENAMING_MAP),
39
+ ]
40
+
41
+
42
+ def _create_pipeline(offload_mode: OffloadMode = OffloadMode.NONE) -> TI2VidTwoStagesPipeline:
43
+ return TI2VidTwoStagesPipeline(
44
+ checkpoint_path=CHECKPOINT_PATH,
45
+ distilled_lora=_DISTILLED_LORA,
46
+ spatial_upsampler_path=UPSAMPLER_PATH,
47
+ gemma_root=GEMMA_ROOT_PATH,
48
+ loras=[],
49
+ offload_mode=offload_mode,
50
+ )
51
+
52
+
53
+ @pytest.fixture(scope="module")
54
+ def pipeline() -> TI2VidTwoStagesPipeline:
55
+ return _create_pipeline()
56
+
57
+
58
+ def _run_pipeline(pipeline: TI2VidTwoStagesPipeline) -> None:
59
+ tiling_config = TilingConfig.default()
60
+ with nvtx_range("pipeline_two_stage_e2e"):
61
+ video_frames, _audio = pipeline(
62
+ prompt=_PROMPT,
63
+ negative_prompt=DEFAULT_NEGATIVE_PROMPT,
64
+ seed=LTX_2_3_PARAMS.seed,
65
+ height=LTX_2_3_PARAMS.stage_2_height,
66
+ width=LTX_2_3_PARAMS.stage_2_width,
67
+ num_frames=LTX_2_3_PARAMS.num_frames,
68
+ frame_rate=LTX_2_3_PARAMS.frame_rate,
69
+ num_inference_steps=LTX_2_3_PARAMS.num_inference_steps,
70
+ video_guider_params=LTX_2_3_PARAMS.video_guider_params,
71
+ audio_guider_params=LTX_2_3_PARAMS.audio_guider_params,
72
+ images=[],
73
+ tiling_config=tiling_config,
74
+ )
75
+ for _ in video_frames:
76
+ pass
77
+
78
+
79
+ def _patch_pipeline_stages() -> None:
80
+ """Register instrument patches for the common pipeline stages."""
81
+ instrument.patch(PromptEncoder.__call__, regions.PROMPT_ENCODER)
82
+ instrument.patch(DiffusionStage.__call__, regions.DIFFUSION_STAGE)
83
+ instrument.patch(GemmaTextEncoder.encode, regions.TEXT_ENCODING)
84
+ instrument.patch(euler_denoising_loop, regions.DENOISING)
85
+ instrument.patch(VideoDecoder.forward, regions.VAE_VIDEO_DECODE, aggregate=True)
86
+ instrument.patch(decode_audio, regions.VAE_AUDIO_DECODE)
87
+ instrument.patch(upsample_video, regions.SPATIAL_UPSAMPLE)
88
+ instrument.patch_tqdm()
89
+
90
+
91
+ def _patch_streaming_stages() -> None:
92
+ """Register instrument patches for block-streaming internals."""
93
+ instrument.patch(WeightsProvider.get, regions.PROVIDER_GET, aggregate=True)
94
+ instrument.patch(WeightsProvider._copy_to_gpu, regions.H2D_COPY, aggregate=True)
95
+ instrument.patch(WeightsProvider._fuse_block_loras, regions.LORA_FUSION, aggregate=True)
96
+ instrument.patch(WeightPool.__init__, regions.WEIGHT_POOL_INIT, aggregate=True)
97
+
98
+
99
+ def _patch_disk_streaming_stages() -> None:
100
+ """Register instrument patches specific to disk streaming."""
101
+ instrument.patch(DiskBlockReader.read_into, regions.DISK_READ_INTO, aggregate=True)
102
+ instrument.patch(DiskTensorReader.__init__, regions.DISK_READER_INIT, aggregate=True)
103
+ instrument.patch(StreamingModelBuilder._build_disk_source, regions.BUILD_DISK_SOURCE)
104
+ instrument.patch(DiskWeightSource.get, regions.DISK_SOURCE_GET, aggregate=True)
105
+ instrument.patch(StreamingModelBuilder._load_non_block_weights, regions.LOAD_NON_BLOCK_WEIGHTS)
106
+ instrument.patch(LoraSource.__init__, regions.LORA_SOURCE_INIT, aggregate=True)
107
+ instrument.patch(LoraSource.get_delta, regions.LORA_GET_DELTA, aggregate=True)
108
+
109
+
110
+ def _patch_ram_streaming_stages() -> None:
111
+ """Register instrument patches specific to RAM streaming."""
112
+ instrument.patch(StreamingModelBuilder._build_pinned_source, regions.BUILD_PINNED_SOURCE)
113
+
114
+
115
+ def _instrument_and_run(
116
+ pipeline: TI2VidTwoStagesPipeline,
117
+ bench_run: Callable[..., BenchmarkResult],
118
+ bench_type: str,
119
+ extra_patches: Callable[[], None] | None = None,
120
+ ) -> BenchmarkResult:
121
+ instrument.activate()
122
+ _patch_pipeline_stages()
123
+ if extra_patches is not None:
124
+ extra_patches()
125
+ try:
126
+ result = bench_run(
127
+ lambda: _run_pipeline(pipeline),
128
+ warmup=0,
129
+ repeats=1,
130
+ bench_type=bench_type,
131
+ model_name=Path(CHECKPOINT_PATH).stem,
132
+ resolution=_RESOLUTION,
133
+ )
134
+ finally:
135
+ instrument.deactivate()
136
+
137
+ instrument.collect_stages(result)
138
+ return result
139
+
140
+
141
+ @pytest.mark.bench
142
+ @torch.inference_mode()
143
+ def test_pipeline_two_stage_e2e(
144
+ pipeline: TI2VidTwoStagesPipeline,
145
+ bench_run: Callable[..., BenchmarkResult],
146
+ ) -> None:
147
+ """Baseline: full model on GPU, no streaming."""
148
+ _instrument_and_run(pipeline, bench_run, "ti2vid_two_stages")
149
+
150
+
151
+ @pytest.mark.bench
152
+ @torch.inference_mode()
153
+ def test_pipeline_two_stage_ram_streaming(
154
+ bench_run: Callable[..., BenchmarkResult],
155
+ ) -> None:
156
+ """RAM streaming: weights pinned in CPU RAM, streamed to GPU per-layer."""
157
+ pipeline = _create_pipeline(OffloadMode.CPU)
158
+
159
+ def _patches() -> None:
160
+ _patch_streaming_stages()
161
+ _patch_ram_streaming_stages()
162
+
163
+ _instrument_and_run(pipeline, bench_run, "ti2vid_two_stages_cpu_offload", extra_patches=_patches)
164
+
165
+
166
+ @pytest.mark.bench
167
+ @torch.inference_mode()
168
+ def test_pipeline_two_stage_disk_streaming(
169
+ bench_run: Callable[..., BenchmarkResult],
170
+ ) -> None:
171
+ """Disk streaming: weights read from safetensors on demand."""
172
+ pipeline = _create_pipeline(OffloadMode.DISK)
173
+
174
+ def _patches() -> None:
175
+ _patch_streaming_stages()
176
+ _patch_disk_streaming_stages()
177
+
178
+ _instrument_and_run(pipeline, bench_run, "ti2vid_two_stages_disk_offload", extra_patches=_patches)
ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/test_pipeline_two_stage_hq.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark: full TI2VidTwoStagesHQPipeline end-to-end (res2s sampler)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ import torch
10
+
11
+ from benchmarks.conftest import PIPELINE_SIZES, InputSize
12
+ from ltx_bench import regions
13
+ from ltx_bench.core import BenchmarkResult
14
+ from ltx_bench.instrument import instrument
15
+ from ltx_bench.nvtx import nvtx_range
16
+ from ltx_core.loader import LTXV_LORA_COMFY_RENAMING_MAP, LoraPathStrengthAndSDOps
17
+ from ltx_core.model.audio_vae.audio_vae import decode_audio
18
+ from ltx_core.model.upsampler.model import upsample_video
19
+ from ltx_core.model.video_vae import TilingConfig
20
+ from ltx_core.model.video_vae.video_vae import VideoDecoder
21
+ from ltx_core.text_encoders.gemma.encoders.base_encoder import GemmaTextEncoder
22
+ from ltx_pipelines.ti2vid_two_stages_hq import TI2VidTwoStagesHQPipeline
23
+ from ltx_pipelines.utils.blocks import DiffusionStage
24
+ from ltx_pipelines.utils.constants import DEFAULT_NEGATIVE_PROMPT, LTX_2_3_HQ_PARAMS
25
+ from ltx_pipelines.utils.samplers import res2s_audio_video_denoising_loop
26
+
27
+ from .conftest import CHECKPOINT_PATH, DISTILLED_LORA_PATH, GEMMA_ROOT_PATH, UPSAMPLER_PATH
28
+
29
+
30
+ @pytest.fixture(scope="module")
31
+ def pipeline() -> TI2VidTwoStagesHQPipeline:
32
+ distilled_lora = [
33
+ LoraPathStrengthAndSDOps(DISTILLED_LORA_PATH, 1.0, LTXV_LORA_COMFY_RENAMING_MAP),
34
+ ]
35
+ return TI2VidTwoStagesHQPipeline(
36
+ checkpoint_path=CHECKPOINT_PATH,
37
+ distilled_lora=distilled_lora,
38
+ distilled_lora_strength_stage_1=0.25,
39
+ distilled_lora_strength_stage_2=0.5,
40
+ spatial_upsampler_path=UPSAMPLER_PATH,
41
+ gemma_root=GEMMA_ROOT_PATH,
42
+ loras=(),
43
+ )
44
+
45
+
46
+ @pytest.mark.bench
47
+ @pytest.mark.parametrize("size", PIPELINE_SIZES, ids=[s.label for s in PIPELINE_SIZES])
48
+ @torch.inference_mode()
49
+ def test_pipeline_two_stage_hq_e2e(
50
+ pipeline: TI2VidTwoStagesHQPipeline,
51
+ bench_run: Callable[..., BenchmarkResult],
52
+ size: InputSize,
53
+ ) -> None:
54
+ """Benchmark HQ two-stage text-to-video pipeline end-to-end.
55
+
56
+ Uses the res2s second-order sampler.
57
+ Stage 1: lower-resolution generation with CFG guidance.
58
+ Stage 2: target-resolution refinement with distilled LoRA.
59
+ """
60
+ tiling_config = TilingConfig.default()
61
+
62
+ def _run() -> None:
63
+ with nvtx_range("pipeline_two_stage_hq_e2e"):
64
+ video_frames, _audio = pipeline(
65
+ prompt="A cat sitting on a windowsill watching birds outside.",
66
+ negative_prompt=DEFAULT_NEGATIVE_PROMPT,
67
+ seed=LTX_2_3_HQ_PARAMS.seed,
68
+ height=size.height,
69
+ width=size.width,
70
+ num_frames=size.num_frames,
71
+ frame_rate=size.frame_rate,
72
+ num_inference_steps=LTX_2_3_HQ_PARAMS.num_inference_steps,
73
+ video_guider_params=LTX_2_3_HQ_PARAMS.video_guider_params,
74
+ audio_guider_params=LTX_2_3_HQ_PARAMS.audio_guider_params,
75
+ images=[],
76
+ tiling_config=tiling_config,
77
+ )
78
+ for _ in video_frames:
79
+ pass
80
+
81
+ instrument.activate()
82
+ instrument.patch(DiffusionStage._build_transformer, regions.MODEL_LOAD)
83
+ instrument.patch(GemmaTextEncoder.encode, regions.TEXT_ENCODING)
84
+ instrument.patch(res2s_audio_video_denoising_loop, regions.DENOISING)
85
+ instrument.patch(VideoDecoder.forward, regions.VAE_VIDEO_DECODE, aggregate=True)
86
+ instrument.patch(decode_audio, regions.VAE_AUDIO_DECODE)
87
+ instrument.patch(upsample_video, regions.SPATIAL_UPSAMPLE)
88
+ instrument.patch_tqdm()
89
+ try:
90
+ result = bench_run(
91
+ _run,
92
+ warmup=0,
93
+ repeats=1,
94
+ bench_type="ti2vid_two_stages_hq",
95
+ model_name=Path(CHECKPOINT_PATH).stem,
96
+ resolution=size.label,
97
+ )
98
+ finally:
99
+ instrument.deactivate()
100
+
101
+ instrument.collect_stages(result)
ltx-2-internal/packages/ltx-bench/pyproject.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "ltx-bench"
3
+ version = "0.1.0"
4
+ description = "Profiling and benchmarking tools for LTX-2"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = ["ltx-core", "ltx-pipelines", "psutil", "torch~=2.7"]
8
+
9
+ [project.entry-points.pytest11]
10
+ ltx_bench = "ltx_bench.conftest"
11
+
12
+ [build-system]
13
+ requires = ["uv_build>=0.9.8,<0.10.0"]
14
+ build-backend = "uv_build"
ltx-2-internal/packages/ltx-bench/src/ltx_bench/__init__.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ltx_bench.core import BenchmarkResult, MemoryTimelineEntry, MemoryTimelineTracker, benchmark, cuda_timer
2
+ from ltx_bench.instrument import instrument
3
+ from ltx_bench.memory_graph import generate_memory_graph
4
+ from ltx_bench.nvtx import nvtx_range
5
+ from ltx_bench.storage import (
6
+ TrendRun,
7
+ download_trend,
8
+ generate_report_md,
9
+ load_results,
10
+ merge_reports,
11
+ save_run,
12
+ upload_to_gcs,
13
+ )
14
+
15
+ __all__ = [
16
+ "BenchmarkResult",
17
+ "MemoryTimelineEntry",
18
+ "MemoryTimelineTracker",
19
+ "TrendRun",
20
+ "benchmark",
21
+ "cuda_timer",
22
+ "download_trend",
23
+ "generate_memory_graph",
24
+ "generate_report_md",
25
+ "instrument",
26
+ "load_results",
27
+ "merge_reports",
28
+ "nvtx_range",
29
+ "save_run",
30
+ "upload_to_gcs",
31
+ ]
ltx-2-internal/packages/ltx-bench/src/ltx_bench/conftest.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """pytest plugin for ltx-bench: --bench flag, fixtures, and result collection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gc
6
+ import logging
7
+ import os
8
+ from collections.abc import Callable, Generator
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import pytest
13
+ import torch
14
+
15
+ from ltx_bench.core import BenchmarkResult, benchmark
16
+ from ltx_bench.instrument import instrument
17
+ from ltx_bench.memory_graph import generate_memory_graph
18
+ from ltx_bench.storage import save_run, upload_to_gcs
19
+
20
+ logger = logging.getLogger("ltx-bench")
21
+
22
+
23
+ def pytest_addoption(parser: pytest.Parser) -> None:
24
+ group = parser.getgroup("bench", "LTX benchmarking options")
25
+ group.addoption("--bench", action="store_true", default=False, help="Run benchmark tests")
26
+ group.addoption(
27
+ "--nsys",
28
+ action="store_true",
29
+ default=False,
30
+ help="Enable NVTX markers for Nsight Systems profiling",
31
+ )
32
+ group.addoption(
33
+ "--bench-mode",
34
+ choices=["timing", "memory"],
35
+ default="timing",
36
+ help="Measurement mode: 'timing' for CUDA event timing, 'memory' for memory timeline profiling",
37
+ )
38
+ group.addoption(
39
+ "--bench-output",
40
+ default="packages/ltx-bench/bench_results",
41
+ help="Directory for benchmark result files",
42
+ )
43
+ group.addoption("--bench-warmup", type=int, default=2, help="Number of warmup iterations")
44
+ group.addoption("--bench-repeats", type=int, default=5, help="Number of measured iterations")
45
+ group.addoption("--bench-gcs-upload", action="store_true", default=False, help="Upload results to GCS")
46
+ group.addoption("--bench-gcs-bucket", default=None, help="Override default GCS bucket name")
47
+
48
+
49
+ def pytest_configure(config: pytest.Config) -> None:
50
+ config.addinivalue_line("markers", "bench: mark test as a benchmark (requires --bench to run)")
51
+ config._bench_results: list[BenchmarkResult] = [] # type: ignore[attr-defined]
52
+ if config.getoption("--nsys"):
53
+ os.environ["LTX_BENCH_NVTX"] = "1"
54
+ else:
55
+ os.environ.pop("LTX_BENCH_NVTX", None)
56
+ instrument.set_bench_mode(config.getoption("--bench-mode"))
57
+
58
+
59
+ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
60
+ if not config.getoption("--bench"):
61
+ skip = pytest.mark.skip(reason="Benchmarks require --bench flag")
62
+ for item in items:
63
+ if "bench" in item.keywords:
64
+ item.add_marker(skip)
65
+
66
+
67
+ @pytest.fixture(scope="session")
68
+ def bench_config(request: pytest.FixtureRequest) -> dict[str, int | str]:
69
+ """Expose benchmark CLI options as a dict fixture."""
70
+ return {
71
+ "warmup": request.config.getoption("--bench-warmup"),
72
+ "repeats": request.config.getoption("--bench-repeats"),
73
+ "output_dir": request.config.getoption("--bench-output"),
74
+ "bench_mode": request.config.getoption("--bench-mode"),
75
+ }
76
+
77
+
78
+ @pytest.fixture
79
+ def bench_collect(request: pytest.FixtureRequest) -> list[BenchmarkResult]:
80
+ """Callable list -- append BenchmarkResults to register them for saving."""
81
+ return request.config._bench_results # type: ignore[attr-defined]
82
+
83
+
84
+ _BenchRunFn = Callable[..., BenchmarkResult]
85
+
86
+
87
+ @pytest.fixture
88
+ def bench_run(
89
+ bench_config: dict[str, int | str],
90
+ bench_collect: list[BenchmarkResult],
91
+ ) -> _BenchRunFn:
92
+ """Convenience fixture: calls ``benchmark()`` with session config and auto-collects."""
93
+ mode = str(bench_config["bench_mode"])
94
+
95
+ def _run(
96
+ fn: Callable[[], Any],
97
+ *,
98
+ bench_type: str = "",
99
+ model_name: str = "",
100
+ warmup: int | None = None,
101
+ repeats: int | None = None,
102
+ **extra: Any, # noqa: ANN401
103
+ ) -> BenchmarkResult:
104
+ extra.setdefault("dtype", "bfloat16")
105
+ effective_warmup = warmup if warmup is not None else int(bench_config["warmup"])
106
+ effective_repeats = repeats if repeats is not None else int(bench_config["repeats"])
107
+ # Memory mode defaults to 1 repeat for a clean timeline (unless overridden)
108
+ if mode == "memory" and repeats is None:
109
+ effective_repeats = 1
110
+ result = benchmark(
111
+ fn,
112
+ warmup=effective_warmup,
113
+ repeats=effective_repeats,
114
+ bench_type=bench_type,
115
+ model_name=model_name,
116
+ bench_mode=mode,
117
+ **extra,
118
+ )
119
+ bench_collect.append(result)
120
+ return result
121
+
122
+ return _run
123
+
124
+
125
+ @pytest.fixture(autouse=True)
126
+ def _bench_cleanup(request: pytest.FixtureRequest) -> Generator[None, None, None]:
127
+ """Free GPU memory before and after each benchmark test."""
128
+ if "bench" not in request.keywords:
129
+ yield
130
+ return
131
+
132
+ gc.collect()
133
+ if torch.cuda.is_available():
134
+ torch.cuda.empty_cache()
135
+
136
+ yield
137
+
138
+ gc.collect()
139
+ if torch.cuda.is_available():
140
+ torch.cuda.empty_cache()
141
+
142
+
143
+ def _format_summary(results: list[BenchmarkResult]) -> str:
144
+ """Render a mode-appropriate summary table."""
145
+ if not results:
146
+ return ""
147
+
148
+ mode = results[0].bench_mode
149
+
150
+ if mode == "memory":
151
+ lines = [
152
+ "",
153
+ "## Benchmark Summary (Memory Mode)",
154
+ "",
155
+ "| Benchmark | Peak VRAM (MB) | Peak USS (MB) | Peak RSS (MB) | Non-evictable (MB) | Samples |",
156
+ "|-----------|----------------|---------------|---------------|----------------|---------|",
157
+ ]
158
+ for r in results:
159
+ lines.append(
160
+ f"| {r.name} | {r.peak_vram_mb:.0f}"
161
+ f" | {r.peak_uss_mb:.0f} | {r.peak_rss_mb:.0f}"
162
+ f" | {r.peak_non_evictable_mb:.0f} | {len(r.memory_timeline)} |"
163
+ )
164
+ else:
165
+ lines = [
166
+ "",
167
+ "## Benchmark Summary (Timing Mode)",
168
+ "",
169
+ "| Benchmark | GPU (ms) | Wall (ms) |",
170
+ "|-----------|----------|-----------|",
171
+ ]
172
+ for r in results:
173
+ lines.append(f"| {r.name} | {r.gpu_time_ms:.1f} | {r.wall_time_ms:.1f} |")
174
+
175
+ lines.append("")
176
+ return "\n".join(lines)
177
+
178
+
179
+ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # noqa: ARG001
180
+ results: list[BenchmarkResult] = session.config._bench_results # type: ignore[attr-defined]
181
+ if results:
182
+ output_dir = session.config.getoption("--bench-output")
183
+
184
+ run_dir = save_run(results, output_dir)
185
+ logger.info("Saved %d results to %s", len(results), run_dir)
186
+ logger.info(_format_summary(results))
187
+
188
+ # Generate memory graphs for pipeline results only (component benchmarks
189
+ # are too short to produce useful timelines).
190
+ if results[0].bench_mode == "memory":
191
+ for r in results:
192
+ if r.memory_timeline and r.extra.get("stage_wall_regions"):
193
+ safe_name = r.name.replace(" / ", "_").replace(" ", "_")
194
+ graph_path = Path(run_dir) / f"{safe_name}_memory.png"
195
+ generate_memory_graph(r, graph_path)
196
+ r.memory_graph_path = str(graph_path)
197
+ logger.info("Memory graph: %s", graph_path)
198
+
199
+ if session.config.getoption("--bench-gcs-upload"):
200
+ branch = results[0].git_branch if results else ""
201
+ commit = results[0].git_commit if results else ""
202
+ bucket_kwarg: dict[str, str] = {}
203
+ custom_bucket = session.config.getoption("--bench-gcs-bucket")
204
+ if custom_bucket:
205
+ bucket_kwarg["bucket_name"] = custom_bucket
206
+ uri = upload_to_gcs(
207
+ run_dir / "results.jsonl",
208
+ branch=branch,
209
+ commit=commit,
210
+ **bucket_kwarg,
211
+ )
212
+ if uri:
213
+ logger.info("Uploaded to %s", uri)
214
+ else:
215
+ logger.warning("GCS upload skipped or failed")
ltx-2-internal/packages/ltx-bench/src/ltx_bench/core.py ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import os
5
+ import subprocess
6
+ import threading
7
+ import time
8
+ from collections.abc import Callable, Generator
9
+ from contextlib import contextmanager
10
+ from dataclasses import dataclass, field, replace
11
+ from datetime import datetime, timezone
12
+ from statistics import median
13
+ from typing import Any
14
+
15
+ import torch
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Data classes
19
+ # ---------------------------------------------------------------------------
20
+
21
+ _MB = 1024 * 1024
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class MemoryTimelineEntry:
26
+ """Single sample in a memory timeline, combining smaps_rollup and VRAM."""
27
+
28
+ elapsed_s: float
29
+
30
+ # /proc/pid/smaps_rollup fields (MB)
31
+ rss_mb: float
32
+ pss_mb: float
33
+ private_clean_mb: float
34
+ private_dirty_mb: float
35
+ shared_clean_mb: float
36
+ shared_dirty_mb: float
37
+ anonymous_mb: float
38
+ swap_mb: float
39
+ uss_mb: float # Private_Clean + Private_Dirty + Private_Hugetlb
40
+
41
+ vram_allocated_mb: float
42
+ vram_reserved_mb: float
43
+
44
+
45
+ @dataclass
46
+ class BenchmarkResult:
47
+ """Single benchmark measurement with timing, memory, and metadata."""
48
+
49
+ bench_type: str
50
+ model_name: str
51
+ bench_mode: str = "timing"
52
+
53
+ # Timing (populated in timing mode only)
54
+ all_wall_times_ms: list[float] = field(default_factory=list)
55
+ all_gpu_times_ms: list[float] = field(default_factory=list)
56
+ num_steps: int = 0
57
+ step_times_gpu_ms: list[float] = field(default_factory=list)
58
+
59
+ # Memory peaks (populated in memory mode only)
60
+ peak_vram_mb: float = 0.0
61
+ peak_uss_mb: float = 0.0
62
+ peak_rss_mb: float = 0.0
63
+ peak_non_evictable_mb: float = 0.0
64
+
65
+ # Memory timeline (populated in memory mode only)
66
+ memory_timeline: list[MemoryTimelineEntry] = field(default_factory=list)
67
+ memory_graph_path: str = ""
68
+
69
+ # Metadata
70
+ git_commit: str = ""
71
+ git_branch: str = ""
72
+ gpu_name: str = ""
73
+ dtype: str = ""
74
+ resolution: str = ""
75
+ timestamp: str = ""
76
+ extra: dict[str, Any] = field(default_factory=dict)
77
+
78
+ @property
79
+ def gpu_time_ms(self) -> float:
80
+ """Median GPU time across all measured iterations."""
81
+ return median(self.all_gpu_times_ms) if self.all_gpu_times_ms else 0.0
82
+
83
+ @property
84
+ def wall_time_ms(self) -> float:
85
+ """Median wall-clock time across all measured iterations."""
86
+ return median(self.all_wall_times_ms) if self.all_wall_times_ms else 0.0
87
+
88
+ @property
89
+ def name(self) -> str:
90
+ """Full benchmark name: bench_type / model_name [/ resolution]."""
91
+ parts = [self.bench_type, self.model_name]
92
+ if self.resolution:
93
+ parts.append(self.resolution)
94
+ return " / ".join(parts)
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # smaps_rollup parsing
99
+ # ---------------------------------------------------------------------------
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class SmapsSnapshot:
104
+ """All useful fields from /proc/pid/smaps_rollup, in bytes."""
105
+
106
+ rss: int = 0
107
+ pss: int = 0
108
+ private_clean: int = 0
109
+ private_dirty: int = 0
110
+ private_hugetlb: int = 0
111
+ shared_clean: int = 0
112
+ shared_dirty: int = 0
113
+ anonymous: int = 0
114
+ swap: int = 0
115
+
116
+ @property
117
+ def uss(self) -> int:
118
+ return self.private_clean + self.private_dirty + self.private_hugetlb
119
+
120
+ @property
121
+ def shared(self) -> int:
122
+ return self.rss - self.uss
123
+
124
+ @staticmethod
125
+ def read(pid: int) -> SmapsSnapshot:
126
+ """Parse /proc/pid/smaps_rollup in a single pass."""
127
+ rss = pss = private_clean = private_dirty = private_hugetlb = 0
128
+ shared_clean = shared_dirty = anonymous = swap = 0
129
+ with open(f"/proc/{pid}/smaps_rollup", "rb") as f:
130
+ for line in f:
131
+ if line.startswith(b"Rss:"):
132
+ rss = int(line.split()[1]) * 1024
133
+ elif line.startswith(b"Pss:"):
134
+ # Pss: is the aggregate; skip Pss_Dirty/Pss_Anon/etc.
135
+ if line[3:4] == b":":
136
+ pss = int(line.split()[1]) * 1024
137
+ elif line.startswith(b"Private_Clean:"):
138
+ private_clean = int(line.split()[1]) * 1024
139
+ elif line.startswith(b"Private_Dirty:"):
140
+ private_dirty = int(line.split()[1]) * 1024
141
+ elif line.startswith(b"Private_Hugetlb:"):
142
+ private_hugetlb = int(line.split()[1]) * 1024
143
+ elif line.startswith(b"Shared_Clean:"):
144
+ shared_clean = int(line.split()[1]) * 1024
145
+ elif line.startswith(b"Shared_Dirty:"):
146
+ shared_dirty = int(line.split()[1]) * 1024
147
+ elif line.startswith(b"Anonymous:"):
148
+ anonymous = int(line.split()[1]) * 1024
149
+ elif line.startswith(b"Swap:") and line[4:5] == b":":
150
+ swap = int(line.split()[1]) * 1024
151
+ return SmapsSnapshot(
152
+ rss=rss,
153
+ pss=pss,
154
+ private_clean=private_clean,
155
+ private_dirty=private_dirty,
156
+ private_hugetlb=private_hugetlb,
157
+ shared_clean=shared_clean,
158
+ shared_dirty=shared_dirty,
159
+ anonymous=anonymous,
160
+ swap=swap,
161
+ )
162
+
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # Memory timeline tracker (memory mode)
166
+ # ---------------------------------------------------------------------------
167
+
168
+
169
+ class MemoryTimelineTracker:
170
+ """Polls smaps_rollup + VRAM from a background thread, recording a timeline.
171
+
172
+ Default 15 ms polling interval provides high resolution without
173
+ meaningful overhead (each /proc read takes ~4 ms on a large process).
174
+ """
175
+
176
+ def __init__(self, interval_s: float = 0.015) -> None:
177
+ self._pid = os.getpid()
178
+ self._interval = interval_s
179
+ self._timeline: list[MemoryTimelineEntry] = []
180
+ self._stop = threading.Event()
181
+ self._thread: threading.Thread | None = None
182
+ self._start_time: float = 0.0
183
+ self._has_cuda = torch.cuda.is_available()
184
+
185
+ def _sample(self) -> None:
186
+ snap = SmapsSnapshot.read(self._pid)
187
+ elapsed = time.perf_counter() - self._start_time
188
+ # memory_allocated / memory_reserved are thread-safe stat queries
189
+ # (atomic counter reads in the caching allocator), safe from any thread.
190
+ if self._has_cuda:
191
+ vram_alloc = torch.cuda.memory_allocated() / _MB
192
+ vram_resv = torch.cuda.memory_reserved() / _MB
193
+ else:
194
+ vram_alloc = 0.0
195
+ vram_resv = 0.0
196
+ self._timeline.append(
197
+ MemoryTimelineEntry(
198
+ elapsed_s=elapsed,
199
+ rss_mb=snap.rss / _MB,
200
+ pss_mb=snap.pss / _MB,
201
+ private_clean_mb=snap.private_clean / _MB,
202
+ private_dirty_mb=snap.private_dirty / _MB,
203
+ shared_clean_mb=snap.shared_clean / _MB,
204
+ shared_dirty_mb=snap.shared_dirty / _MB,
205
+ anonymous_mb=snap.anonymous / _MB,
206
+ swap_mb=snap.swap / _MB,
207
+ uss_mb=snap.uss / _MB,
208
+ vram_allocated_mb=vram_alloc,
209
+ vram_reserved_mb=vram_resv,
210
+ )
211
+ )
212
+
213
+ def sample_vram(self) -> None:
214
+ """Sample VRAM from the main thread and update the last timeline entry.
215
+
216
+ CUDA memory queries must run on the main thread to avoid interfering
217
+ with the allocator. Call this after ``stop()`` or between iterations.
218
+ """
219
+ if not self._timeline or not self._has_cuda:
220
+ return
221
+ self._timeline[-1] = replace(
222
+ self._timeline[-1],
223
+ vram_allocated_mb=torch.cuda.memory_allocated() / _MB,
224
+ vram_reserved_mb=torch.cuda.memory_reserved() / _MB,
225
+ )
226
+
227
+ def start(self) -> None:
228
+ self._timeline.clear()
229
+ self._start_time = time.perf_counter()
230
+ self._sample()
231
+ self._stop.clear()
232
+ self._thread = threading.Thread(target=self._poll, daemon=True)
233
+ self._thread.start()
234
+
235
+ def _poll(self) -> None:
236
+ while not self._stop.is_set():
237
+ self._sample()
238
+ self._stop.wait(self._interval)
239
+
240
+ def stop(self) -> list[MemoryTimelineEntry]:
241
+ """Stop polling and return the full timeline."""
242
+ self._stop.set()
243
+ if self._thread:
244
+ self._thread.join()
245
+ self._sample()
246
+ return self._timeline
247
+
248
+ @property
249
+ def start_time(self) -> float:
250
+ """The ``perf_counter`` origin used by the timeline."""
251
+ return self._start_time
252
+
253
+ @property
254
+ def peak_rss_mb(self) -> float:
255
+ return max((e.rss_mb for e in self._timeline), default=0.0)
256
+
257
+ @property
258
+ def peak_uss_mb(self) -> float:
259
+ return max((e.uss_mb for e in self._timeline), default=0.0)
260
+
261
+ @property
262
+ def peak_uss_entry(self) -> MemoryTimelineEntry | None:
263
+ """Return the timeline sample where USS was highest."""
264
+ return max(self._timeline, key=lambda e: e.uss_mb) if self._timeline else None
265
+
266
+
267
+ # ---------------------------------------------------------------------------
268
+ # CUDA event timer (timing mode)
269
+ # ---------------------------------------------------------------------------
270
+
271
+
272
+ @contextmanager
273
+ def cuda_timer(nvtx_label: str = "") -> Generator[dict[str, float], None, None]:
274
+ """Time a CUDA region using CUDA events (primary) and wall clock (secondary).
275
+
276
+ Uses CUDA events for GPU timing — no synchronize() between start and end events.
277
+ A synchronize() before start drains any prior queued work. end.synchronize()
278
+ waits only for the end event.
279
+
280
+ If *nvtx_label* is provided, the region is also marked with an NVTX range
281
+ for nsys profiling.
282
+ """
283
+ result: dict[str, float] = {}
284
+ torch.cuda.synchronize()
285
+
286
+ if nvtx_label:
287
+ torch.cuda.nvtx.range_push(nvtx_label)
288
+
289
+ start_event = torch.cuda.Event(enable_timing=True)
290
+ end_event = torch.cuda.Event(enable_timing=True)
291
+
292
+ wall_start = time.perf_counter()
293
+ start_event.record()
294
+ try:
295
+ yield result
296
+ finally:
297
+ end_event.record()
298
+ wall_end = time.perf_counter()
299
+
300
+ if nvtx_label:
301
+ torch.cuda.nvtx.range_pop()
302
+
303
+ end_event.synchronize()
304
+ result["gpu_ms"] = start_event.elapsed_time(end_event)
305
+ result["wall_ms"] = (wall_end - wall_start) * 1000.0
306
+
307
+
308
+ # ---------------------------------------------------------------------------
309
+ # Git / GPU metadata
310
+ # ---------------------------------------------------------------------------
311
+
312
+
313
+ @functools.lru_cache(maxsize=1)
314
+ def _git_info() -> tuple[str, str]:
315
+ """Return (commit_hash, branch_name) from git, or empty strings on failure."""
316
+ try:
317
+ commit = subprocess.check_output(
318
+ ["git", "rev-parse", "--short", "HEAD"],
319
+ stderr=subprocess.DEVNULL,
320
+ text=True,
321
+ ).strip()
322
+ branch = subprocess.check_output(
323
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
324
+ stderr=subprocess.DEVNULL,
325
+ text=True,
326
+ ).strip()
327
+ except (subprocess.CalledProcessError, FileNotFoundError):
328
+ return "", ""
329
+ return commit, branch
330
+
331
+
332
+ # ---------------------------------------------------------------------------
333
+ # Internal measurement paths
334
+ # ---------------------------------------------------------------------------
335
+
336
+
337
+ def _run_timing(
338
+ fn: Callable[[], Any],
339
+ *,
340
+ warmup: int,
341
+ repeats: int,
342
+ bench_type: str,
343
+ on_loop_start: Callable[[], None] | None,
344
+ on_iter_end: Callable[[], None] | None,
345
+ ) -> tuple[list[float], list[float]]:
346
+ """Timing-only path: CUDA events + wall clock. No memory measurement."""
347
+ for _ in range(warmup):
348
+ fn()
349
+
350
+ if on_loop_start:
351
+ on_loop_start()
352
+
353
+ all_wall: list[float] = []
354
+ all_gpu: list[float] = []
355
+
356
+ for _ in range(repeats):
357
+ with cuda_timer(nvtx_label=bench_type) as t:
358
+ fn()
359
+ all_gpu.append(t["gpu_ms"])
360
+ all_wall.append(t["wall_ms"])
361
+ if on_iter_end:
362
+ on_iter_end()
363
+
364
+ return all_wall, all_gpu
365
+
366
+
367
+ def _run_memory(
368
+ fn: Callable[[], Any],
369
+ *,
370
+ warmup: int,
371
+ repeats: int,
372
+ on_loop_start: Callable[[], None] | None,
373
+ on_iter_end: Callable[[], None] | None,
374
+ ) -> tuple[list[MemoryTimelineEntry], float, float]:
375
+ """Memory-only path: smaps_rollup timeline + VRAM. No timing wrappers.
376
+
377
+ Returns (timeline, peak_vram_mb, start_time) where start_time is
378
+ the ``perf_counter`` origin for aligning wall-clock region annotations.
379
+ """
380
+ for _ in range(warmup):
381
+ fn()
382
+
383
+ torch.cuda.reset_peak_memory_stats()
384
+
385
+ if on_loop_start:
386
+ on_loop_start()
387
+
388
+ tracker = MemoryTimelineTracker()
389
+ tracker.start()
390
+
391
+ try:
392
+ for _ in range(repeats):
393
+ fn()
394
+ tracker.sample_vram() # VRAM queries on main thread only
395
+ if on_iter_end:
396
+ on_iter_end()
397
+ finally:
398
+ timeline = tracker.stop()
399
+ tracker.sample_vram() # final VRAM snapshot
400
+ peak_vram_mb = torch.cuda.max_memory_allocated() / _MB if torch.cuda.is_available() else 0.0
401
+ return timeline, peak_vram_mb, tracker.start_time
402
+
403
+
404
+ # ---------------------------------------------------------------------------
405
+ # Public API
406
+ # ---------------------------------------------------------------------------
407
+
408
+
409
+ def benchmark(
410
+ fn: Callable[[], Any],
411
+ *,
412
+ warmup: int = 2,
413
+ repeats: int = 5,
414
+ bench_type: str = "",
415
+ model_name: str = "",
416
+ bench_mode: str = "timing",
417
+ on_loop_start: Callable[[], None] | None = None,
418
+ on_iter_end: Callable[[], None] | None = None,
419
+ **extra: Any, # noqa: ANN401
420
+ ) -> BenchmarkResult:
421
+ """Run ``fn`` with warmup iterations, then measure ``repeats`` iterations.
422
+
423
+ *bench_mode* selects the measurement strategy:
424
+ - ``"timing"``: CUDA events + wall clock. No memory tracking.
425
+ - ``"memory"``: smaps_rollup timeline + VRAM. No timing wrappers.
426
+
427
+ Returns a :class:`BenchmarkResult` populated for the chosen mode.
428
+ """
429
+ if bench_mode not in ("timing", "memory"):
430
+ msg = f"bench_mode must be 'timing' or 'memory', got {bench_mode!r}"
431
+ raise ValueError(msg)
432
+ if repeats < 1:
433
+ msg = f"repeats must be >= 1, got {repeats}"
434
+ raise ValueError(msg)
435
+ if warmup < 0:
436
+ msg = f"warmup must be >= 0, got {warmup}"
437
+ raise ValueError(msg)
438
+
439
+ git_commit, git_branch = _git_info()
440
+ gpu_name = torch.cuda.get_device_name() if torch.cuda.is_available() else ""
441
+
442
+ # Extract known extra fields
443
+ num_steps = int(extra.pop("num_steps", 0))
444
+ step_times_gpu_ms = list(extra.pop("step_times_gpu_ms", ()))
445
+ dtype = extra.pop("dtype", "")
446
+ resolution = extra.pop("resolution", "")
447
+
448
+ common = {
449
+ "bench_type": bench_type,
450
+ "model_name": model_name,
451
+ "bench_mode": bench_mode,
452
+ "num_steps": num_steps,
453
+ "step_times_gpu_ms": step_times_gpu_ms,
454
+ "git_commit": git_commit,
455
+ "git_branch": git_branch,
456
+ "gpu_name": gpu_name,
457
+ "dtype": dtype,
458
+ "resolution": resolution,
459
+ "timestamp": datetime.now(tz=timezone.utc).isoformat(),
460
+ "extra": extra,
461
+ }
462
+
463
+ if bench_mode == "timing":
464
+ all_wall, all_gpu = _run_timing(
465
+ fn,
466
+ warmup=warmup,
467
+ repeats=repeats,
468
+ bench_type=bench_type,
469
+ on_loop_start=on_loop_start,
470
+ on_iter_end=on_iter_end,
471
+ )
472
+ return BenchmarkResult(
473
+ all_wall_times_ms=all_wall,
474
+ all_gpu_times_ms=all_gpu,
475
+ **common,
476
+ )
477
+
478
+ timeline, peak_vram_mb, mem_start_time = _run_memory(
479
+ fn,
480
+ warmup=warmup,
481
+ repeats=repeats,
482
+ on_loop_start=on_loop_start,
483
+ on_iter_end=on_iter_end,
484
+ )
485
+
486
+ # Derive peak values from the snapshot where USS was highest.
487
+ peak_entry = max(timeline, key=lambda e: e.uss_mb) if timeline else None
488
+
489
+ # Store the timeline origin so instrument wall-clock regions can be aligned.
490
+ extra["memory_start_time"] = mem_start_time
491
+
492
+ return BenchmarkResult(
493
+ peak_vram_mb=peak_vram_mb,
494
+ peak_uss_mb=peak_entry.uss_mb if peak_entry else 0.0,
495
+ peak_rss_mb=peak_entry.rss_mb if peak_entry else 0.0,
496
+ peak_non_evictable_mb=peak_entry.private_dirty_mb if peak_entry else 0.0,
497
+ memory_timeline=timeline,
498
+ **common,
499
+ )
ltx-2-internal/packages/ltx-bench/src/ltx_bench/instrument.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Per-stage pipeline instrumentation using CUDA events.
2
+
3
+ Instruments production functions by monkey-patching them at test time.
4
+ Zero footprint in production code — all patching happens in benchmark tests
5
+ and is fully restored on ``deactivate()``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ import re
12
+ import sys
13
+ import time
14
+ from collections.abc import Callable
15
+ from contextlib import contextmanager
16
+ from typing import TYPE_CHECKING, Any, Generator
17
+
18
+ if TYPE_CHECKING:
19
+ from ltx_bench.core import BenchmarkResult
20
+
21
+ import torch
22
+
23
+ from ltx_bench.nvtx import nvtx_range
24
+
25
+
26
+ class BenchInstrument:
27
+ """Singleton that collects per-stage CUDA event timings when active.
28
+
29
+ **Lifecycle:**
30
+ 1. ``activate()``
31
+ 2. ``patch()`` / ``patch_tqdm()`` — explicit per-target
32
+ 3. Run pipeline
33
+ 4. ``deactivate()`` — restores all patches
34
+ 5. ``collect()`` or ``collect_stages()``
35
+
36
+ **Threading:** Not thread-safe. All instrumented code must run on a single
37
+ thread and (for correct CUDA event timing) on the default CUDA stream.
38
+
39
+ **Warmup/repeats:** When instrumentation is active, the benchmarked function
40
+ must be called exactly once (``warmup=0, repeats=1``).
41
+
42
+ Usage::
43
+
44
+ instrument.activate()
45
+ instrument.patch(some_func, "region_name")
46
+ instrument.patch_tqdm()
47
+ try:
48
+ result = bench_run(fn, warmup=0, repeats=1, ...)
49
+ finally:
50
+ instrument.deactivate()
51
+
52
+ instrument.collect_stages(result)
53
+ """
54
+
55
+ def __init__(self) -> None:
56
+ self._active = False
57
+ self._bench_mode = "timing"
58
+ self._regions: list[tuple[str, str | None, torch.cuda.Event, torch.cuda.Event]] = []
59
+ self._wall_regions: list[tuple[str, float, float]] = []
60
+ self._name_counts: dict[str, int] = {}
61
+ self._current_region: str | None = None
62
+ self._patches: list[tuple[object, str, Any]] = []
63
+ self._aggregate_names: set[str] = set()
64
+ self.tqdm_stats: dict[str, dict[str, Any]] = {}
65
+
66
+ @property
67
+ def active(self) -> bool:
68
+ return self._active
69
+
70
+ @property
71
+ def current_region(self) -> str | None:
72
+ return self._current_region
73
+
74
+ def set_bench_mode(self, mode: str) -> None:
75
+ """Set the bench mode so ``activate()`` uses the correct strategy."""
76
+ self._bench_mode = mode
77
+
78
+ def activate(self) -> None:
79
+ """Enable instrumentation. Call ``patch()`` after this.
80
+
81
+ In timing mode, records per-region CUDA event timings.
82
+ In memory mode, records wall-clock region boundaries (no CUDA events).
83
+ Call ``set_bench_mode()`` before ``activate()`` to select the mode.
84
+ """
85
+ self._active = True
86
+ self._regions.clear()
87
+ self._wall_regions.clear()
88
+ self._name_counts.clear()
89
+ self._current_region = None
90
+ self._patches.clear()
91
+ self._aggregate_names.clear()
92
+ self.tqdm_stats = {}
93
+
94
+ def deactivate(self) -> None:
95
+ """Disable instrumentation and restore all patches."""
96
+ self._active = False
97
+ self._current_region = None
98
+ for obj, attr, original in reversed(self._patches):
99
+ setattr(obj, attr, original)
100
+ self._patches.clear()
101
+
102
+ # ------------------------------------------------------------------
103
+ # Patching
104
+ # ------------------------------------------------------------------
105
+
106
+ def patch(self, target: Callable[..., Any], region: str, *, aggregate: bool = False) -> None:
107
+ """Monkey-patch a function to record a CUDA event region.
108
+
109
+ ``target`` is the actual callable — a module-level function or an unbound
110
+ class method (e.g. ``VideoDecoder.forward``).
111
+
112
+ When ``aggregate=True``, repeated invocations under the same parent
113
+ region (auto-tagged ``region[1]``, ``region[2]``, ...) are collapsed into
114
+ a single entry by :meth:`collect_stages`. Invocations under different
115
+ parents are kept separate as ``region[0]``, ``region[1]``, etc.
116
+
117
+ The function is patched at its definition site AND at every module that
118
+ imported it (via ``from X import func``). All patches are restored on
119
+ ``deactivate()``.
120
+ """
121
+ owner, attr, original = self._resolve_callable(target)
122
+ if aggregate:
123
+ self._aggregate_names.add(region)
124
+
125
+ @functools.wraps(original)
126
+ def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
127
+ with self.region(region):
128
+ return original(*args, **kwargs)
129
+
130
+ # Preserve staticmethod descriptor when patching class attrs.
131
+ raw_descriptor = owner.__dict__.get(attr) if isinstance(owner, type) else None
132
+ to_set: Any = staticmethod(wrapper) if isinstance(raw_descriptor, staticmethod) else wrapper
133
+
134
+ # Patch at definition site.
135
+ setattr(owner, attr, to_set)
136
+ self._patches.append((owner, attr, raw_descriptor if raw_descriptor is not None else original))
137
+
138
+ # Patch at all import locations — including aliases like
139
+ # ``from pkg import func as alias``.
140
+ for mod in list(sys.modules.values()):
141
+ if mod is None or mod is owner:
142
+ continue
143
+ try:
144
+ for name, val in list(vars(mod).items()):
145
+ if val is original:
146
+ setattr(mod, name, wrapper)
147
+ self._patches.append((mod, name, original))
148
+ except Exception:
149
+ pass
150
+
151
+ def patch_tqdm(self) -> None:
152
+ """Replace ``tqdm.tqdm`` with a capturing subclass at all import locations.
153
+
154
+ Captures denoising step count and it/s into ``tqdm_stats``, keyed by the
155
+ active ``region()`` name at the time the progress bar closes.
156
+ """
157
+ import tqdm as tqdm_module # noqa: PLC0415
158
+
159
+ original_tqdm = tqdm_module.tqdm
160
+ inst = self
161
+
162
+ class _CaptureTqdm(original_tqdm): # type: ignore[misc]
163
+ def close(self) -> None:
164
+ if inst.active and not self.disable and inst.current_region and self.n > 0:
165
+ inst.tqdm_stats[inst.current_region] = {
166
+ "rate": self.format_dict.get("rate"),
167
+ "elapsed": self.format_dict.get("elapsed"),
168
+ "total": self.format_dict.get("total"),
169
+ }
170
+ super().close()
171
+
172
+ # Patch the tqdm module itself.
173
+ tqdm_module.tqdm = _CaptureTqdm
174
+ self._patches.append((tqdm_module, "tqdm", original_tqdm))
175
+
176
+ # Patch modules that did ``from tqdm import tqdm`` (including aliases).
177
+ for mod in list(sys.modules.values()):
178
+ if mod is None or mod is tqdm_module:
179
+ continue
180
+ try:
181
+ for name, val in list(vars(mod).items()):
182
+ if val is original_tqdm:
183
+ setattr(mod, name, _CaptureTqdm)
184
+ self._patches.append((mod, name, original_tqdm))
185
+ except Exception:
186
+ pass
187
+
188
+ # ------------------------------------------------------------------
189
+ # Region recording
190
+ # ------------------------------------------------------------------
191
+
192
+ @contextmanager
193
+ def region(self, name: str) -> Generator[None, None, None]:
194
+ """Context manager that records a named region.
195
+
196
+ Timing mode: CUDA events for GPU timing.
197
+ Memory mode: wall-clock timestamps for graph annotation.
198
+
199
+ When ``LTX_BENCH_NVTX`` is enabled, each region is also wrapped in an
200
+ NVTX range so it appears in Nsight Systems timelines.
201
+ """
202
+ tag = self._tag(name)
203
+ prev_region = self._current_region
204
+ self._current_region = tag
205
+
206
+ if self._bench_mode == "memory":
207
+ wall_start = time.perf_counter()
208
+ try:
209
+ with nvtx_range(tag):
210
+ yield
211
+ finally:
212
+ self._wall_regions.append((tag, wall_start, time.perf_counter()))
213
+ self._current_region = prev_region
214
+ else:
215
+ start = torch.cuda.Event(enable_timing=True)
216
+ end = torch.cuda.Event(enable_timing=True)
217
+ start.record()
218
+ # Append in start order (before entering the region) so that a
219
+ # region's parent always precedes its children in ``_regions``.
220
+ self._regions.append((tag, prev_region, start, end))
221
+ try:
222
+ with nvtx_range(tag):
223
+ yield
224
+ finally:
225
+ end.record()
226
+ self._current_region = prev_region
227
+
228
+ # ------------------------------------------------------------------
229
+ # Collection
230
+ # ------------------------------------------------------------------
231
+
232
+ def collect(self) -> list[tuple[str, str | None, float]]:
233
+ """Synchronize the GPU and return all region timings as (tag, parent, ms).
234
+
235
+ Must be called after ``deactivate()``.
236
+ """
237
+ if not self._regions:
238
+ return []
239
+
240
+ torch.cuda.synchronize()
241
+ return [(tag, parent, start.elapsed_time(end)) for tag, parent, start, end in self._regions]
242
+
243
+ def collect_wall_regions(self) -> list[tuple[str, float, float]]:
244
+ """Return wall-clock region boundaries as (name, start_s, end_s) tuples.
245
+
246
+ Only populated in memory mode. Times are absolute ``perf_counter`` values;
247
+ subtract the benchmark start time to get elapsed seconds.
248
+ """
249
+ return list(self._wall_regions)
250
+
251
+ def collect_stages(self, result: BenchmarkResult) -> None:
252
+ """Collect region timings and tqdm stats into ``result.extra``.
253
+
254
+ Aggregates tiled regions (VAE decode, streaming internals) whose names
255
+ share a prefix into single entries.
256
+ Must be called after ``deactivate()``.
257
+
258
+ In memory mode, also stores wall-clock region boundaries for graph
259
+ annotation.
260
+ """
261
+ if self._bench_mode == "memory":
262
+ result.extra["stage_wall_regions"] = [
263
+ {"name": name, "start_s": start, "end_s": end} for name, start, end in self._wall_regions
264
+ ]
265
+ else:
266
+ stages, counts, parents = self._aggregate_regions(self.collect())
267
+ result.extra["stage_regions"] = stages
268
+ result.extra["stage_region_counts"] = counts
269
+ result.extra["stage_region_parents"] = parents
270
+
271
+ result.extra["tqdm_stats"] = dict(self.tqdm_stats)
272
+
273
+ def _aggregate_regions(
274
+ self, raw: list[tuple[str, str | None, float]]
275
+ ) -> tuple[dict[str, float], dict[str, int], dict[str, str | None]]:
276
+ """Aggregate tagged variants of regions registered with ``aggregate=True``.
277
+
278
+ Invocations with the same name but different parent aggregates are
279
+ kept separate (disambiguated with ``[N]`` suffixes). Unaggregated
280
+ regions pass through with their original tag.
281
+ """
282
+ stages: dict[str, float] = {}
283
+ counts: dict[str, int] = {}
284
+ parents: dict[str, str | None] = {}
285
+
286
+ agg_bucket: dict[tuple[str, str | None], str] = {}
287
+ name_counter: dict[str, int] = {}
288
+ tag_to_display: dict[str, str] = {}
289
+
290
+ for tag, parent_tag, ms in raw:
291
+ parent_display = tag_to_display.get(parent_tag) if parent_tag is not None else None
292
+ base = re.sub(r"\[\d+\]$", "", tag)
293
+ prefix = base if base in self._aggregate_names else None
294
+
295
+ if prefix is not None:
296
+ bucket_key = (prefix, parent_display)
297
+ if bucket_key not in agg_bucket:
298
+ n = name_counter.get(prefix, 0)
299
+ display = prefix if n == 0 else f"{prefix}[{n}]"
300
+ name_counter[prefix] = n + 1
301
+ agg_bucket[bucket_key] = display
302
+ stages[display] = 0.0
303
+ counts[display] = 0
304
+ parents[display] = parent_display
305
+ display = agg_bucket[bucket_key]
306
+ stages[display] += ms
307
+ counts[display] += 1
308
+ else:
309
+ display = tag
310
+ stages[display] = ms
311
+ parents[display] = parent_display
312
+
313
+ tag_to_display[tag] = display
314
+
315
+ # Drop zero-time aggregate buckets.
316
+ stages = {k: v for k, v in stages.items() if v > 0.0 or k not in counts}
317
+ counts = {k: v for k, v in counts.items() if stages.get(k, 0.0) > 0.0}
318
+ parents = {k: v for k, v in parents.items() if k in stages}
319
+ return stages, counts, parents
320
+
321
+ # ------------------------------------------------------------------
322
+ # Internal
323
+ # ------------------------------------------------------------------
324
+
325
+ def _tag(self, name: str) -> str:
326
+ """Return a unique tag, appending [N] for repeated names."""
327
+ count = self._name_counts.get(name, 0)
328
+ self._name_counts[name] = count + 1
329
+ if count == 0:
330
+ return name
331
+ return f"{name}[{count}]"
332
+
333
+ @staticmethod
334
+ def _resolve_callable(target: Callable[..., Any]) -> tuple[object, str, Any]:
335
+ """Resolve a callable to ``(owner, attr_name, original_value)``.
336
+
337
+ For module-level functions the owner is the defining module.
338
+ For class methods (e.g. ``VideoDecoder.forward``) the owner is the class.
339
+ """
340
+ module_name = getattr(target, "__module__", None)
341
+ qualname = getattr(target, "__qualname__", None)
342
+ if module_name is None or qualname is None:
343
+ msg = f"target must have __module__ and __qualname__, got {target!r}"
344
+ raise ValueError(msg)
345
+ if module_name not in sys.modules:
346
+ msg = f"Module {module_name!r} for {target!r} not found in sys.modules"
347
+ raise ValueError(msg)
348
+
349
+ mod = sys.modules[module_name]
350
+ parts = qualname.split(".")
351
+
352
+ # Traverse to the owner (module for top-level funcs, class for methods).
353
+ owner: Any = mod
354
+ try:
355
+ for part in parts[:-1]:
356
+ owner = getattr(owner, part)
357
+ attr = parts[-1]
358
+ resolved = getattr(owner, attr)
359
+ except AttributeError as exc:
360
+ msg = f"Cannot traverse {qualname!r} in module {module_name!r}: {exc}"
361
+ raise ValueError(msg) from exc
362
+
363
+ if resolved is not target:
364
+ msg = (
365
+ f"Resolved {module_name}.{qualname} points to a different object than "
366
+ f"the target passed to patch(). The callable may have been renamed or "
367
+ f"replaced since import."
368
+ )
369
+ raise ValueError(msg)
370
+
371
+ return owner, attr, resolved
372
+
373
+
374
+ # Module-level singleton.
375
+ instrument = BenchInstrument()
ltx-2-internal/packages/ltx-bench/src/ltx_bench/memory_graph.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate memory timeline graphs from benchmark results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from ltx_bench.core import BenchmarkResult
10
+
11
+ _STAGE_COLORS = [
12
+ "#4e79a7",
13
+ "#f28e2b",
14
+ "#e15759",
15
+ "#76b7b2",
16
+ "#59a14f",
17
+ "#edc948",
18
+ "#b07aa1",
19
+ ]
20
+
21
+
22
+ def _aggregate_regions(
23
+ raw_regions: list[dict[str, Any]],
24
+ start_time: float,
25
+ ) -> list[tuple[str, float, float]]:
26
+ """Merge consecutive regions with the same base name into a single span.
27
+
28
+ ``vae_video_decode``, ``vae_video_decode[1]``, ... become one
29
+ ``vae_video_decode`` span covering the full range.
30
+ """
31
+ groups: list[tuple[str, float, float]] = []
32
+ for region in raw_regions:
33
+ name = region["name"].split("[")[0] # strip [N] suffix
34
+ t0 = region["start_s"] - start_time
35
+ t1 = region["end_s"] - start_time
36
+ if groups and groups[-1][0] == name:
37
+ # Extend existing group
38
+ groups[-1] = (name, groups[-1][1], t1)
39
+ else:
40
+ groups.append((name, t0, t1))
41
+ return groups
42
+
43
+
44
+ def _draw_stage_regions(
45
+ axes: list[Any],
46
+ stage_wall_regions: list[dict[str, Any]],
47
+ start_time: float,
48
+ ) -> None:
49
+ """Draw shaded vertical spans and labels for aggregated stage regions."""
50
+ groups = _aggregate_regions(stage_wall_regions, start_time)
51
+
52
+ for i, (_name, t0, t1) in enumerate(groups):
53
+ color = _STAGE_COLORS[i % len(_STAGE_COLORS)]
54
+ for ax in axes:
55
+ ax.axvspan(t0, t1, alpha=0.15, color=color, zorder=0)
56
+
57
+ # Labels on the top axis, horizontal, staggered to avoid overlap.
58
+ ax_top = axes[0]
59
+ y_levels = [1.02, 1.10]
60
+ prev_right_data = -float("inf")
61
+ level_idx = 0
62
+ for i, (name, t0, t1) in enumerate(groups):
63
+ color = _STAGE_COLORS[i % len(_STAGE_COLORS)]
64
+ mid = (t0 + t1) / 2
65
+ # Estimate label width in data coords: ~0.8 data-units per char at fontsize 7
66
+ # on a typical 16-inch-wide figure. This is a rough heuristic.
67
+ x_range = axes[0].get_xlim()[1] - axes[0].get_xlim()[0] if axes[0].get_xlim()[1] > 0 else 1.0
68
+ char_width = x_range / 120 # approx chars that fit in the x range
69
+ half_label = len(name) * char_width / 2
70
+ label_left = mid - half_label
71
+ level_idx = (level_idx + 1) % len(y_levels) if label_left < prev_right_data else 0
72
+ prev_right_data = mid + half_label
73
+ ax_top.annotate(
74
+ name,
75
+ xy=(mid, y_levels[level_idx]),
76
+ xycoords=("data", "axes fraction"),
77
+ ha="center",
78
+ va="bottom",
79
+ fontsize=7,
80
+ fontweight="bold",
81
+ color=color,
82
+ rotation=0,
83
+ annotation_clip=False,
84
+ )
85
+
86
+
87
+ def generate_memory_graph(result: BenchmarkResult, output_path: Path) -> Path:
88
+ """Create a two-panel PNG showing host memory and VRAM over time.
89
+
90
+ Top panel: RSS, USS, Anonymous, Private_Clean, Private_Dirty (host).
91
+ Bottom panel: VRAM allocated + reserved (GPU).
92
+ Stage regions (if present) are drawn as shaded vertical spans.
93
+
94
+ Returns *output_path* for chaining.
95
+ """
96
+ import matplotlib # noqa: PLC0415
97
+
98
+ matplotlib.use("Agg")
99
+ import matplotlib.pyplot as plt # noqa: PLC0415
100
+
101
+ timeline = result.memory_timeline
102
+ if not timeline:
103
+ msg = "No memory timeline data to plot"
104
+ raise ValueError(msg)
105
+
106
+ t = [e.elapsed_s for e in timeline]
107
+
108
+ fig, (ax_host, ax_vram) = plt.subplots(
109
+ 2,
110
+ 1,
111
+ figsize=(16, 9),
112
+ sharex=True,
113
+ gridspec_kw={"height_ratios": [2, 1]},
114
+ )
115
+ fig.suptitle(result.name, fontsize=12, fontweight="bold", y=0.98)
116
+
117
+ # --- Host memory panel ---
118
+ ax_host.plot(t, [e.rss_mb for e in timeline], label="RSS", linewidth=1.5)
119
+ ax_host.plot(t, [e.uss_mb for e in timeline], label="USS", linewidth=1.5)
120
+ ax_host.plot(t, [e.anonymous_mb for e in timeline], label="Anonymous", linewidth=1.2, linestyle="--")
121
+ ax_host.plot(t, [e.private_clean_mb for e in timeline], label="Private_Clean", linewidth=1.0, linestyle=":")
122
+ ax_host.plot(t, [e.private_dirty_mb for e in timeline], label="Private_Dirty", linewidth=1.0, linestyle="-.")
123
+ ax_host.set_ylabel("Host Memory (MB)")
124
+ ax_host.legend(loc="upper right", fontsize=7, framealpha=0.8)
125
+ ax_host.grid(alpha=0.3)
126
+
127
+ # --- VRAM panel ---
128
+ ax_vram.plot(t, [e.vram_allocated_mb for e in timeline], label="Allocated", linewidth=1.5)
129
+ ax_vram.plot(t, [e.vram_reserved_mb for e in timeline], label="Reserved", linewidth=1.2, linestyle="--")
130
+ ax_vram.set_ylabel("VRAM (MB)")
131
+ ax_vram.set_xlabel("Elapsed (s)")
132
+ ax_vram.legend(loc="upper right", fontsize=7, framealpha=0.8)
133
+ ax_vram.grid(alpha=0.3)
134
+
135
+ # --- Stage region annotations ---
136
+ stage_regions = result.extra.get("stage_wall_regions")
137
+ start_time = result.extra.get("memory_start_time", 0.0)
138
+ if stage_regions:
139
+ _draw_stage_regions([ax_host, ax_vram], stage_regions, start_time)
140
+
141
+ fig.subplots_adjust(top=0.84)
142
+ output_path.parent.mkdir(parents=True, exist_ok=True)
143
+ fig.savefig(output_path, dpi=150)
144
+ plt.close(fig)
145
+
146
+ return output_path
ltx-2-internal/packages/ltx-bench/src/ltx_bench/nvtx.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+
7
+ import torch
8
+
9
+
10
+ def _env_flag_enabled(name: str) -> bool:
11
+ value = os.getenv(name, "")
12
+ return value.lower() in {"1", "true", "yes", "on"}
13
+
14
+
15
+ @contextmanager
16
+ def nvtx_range(name: str) -> Generator[None, None, None]:
17
+ """NVTX range marker for nsys profiling; no-op unless LTX_BENCH_NVTX is enabled."""
18
+ if not _env_flag_enabled("LTX_BENCH_NVTX"):
19
+ yield
20
+ return
21
+
22
+ torch.cuda.nvtx.range_push(name)
23
+ try:
24
+ yield
25
+ finally:
26
+ torch.cuda.nvtx.range_pop()