Spaces:
Sleeping
Sleeping
Switch to HDR IC-LoRA app with on-the-fly PromptEncoder + bundled ltx-2-internal
Browse filesReplaces 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
- README.md +4 -4
- app.py +281 -193
- ltx-2-internal/LICENSE +381 -0
- ltx-2-internal/README.md +66 -0
- ltx-2-internal/README.public.md +121 -0
- ltx-2-internal/conftest.py +217 -0
- ltx-2-internal/internal/ltx-kernels/MANIFEST.in +1 -0
- ltx-2-internal/internal/ltx-kernels/README.md +43 -0
- ltx-2-internal/internal/ltx-kernels/csrc/all2all/all2all.cpp +408 -0
- ltx-2-internal/internal/ltx-kernels/csrc/all2all/all2all.hpp +231 -0
- ltx-2-internal/internal/ltx-kernels/csrc/all2all/cuda/all2all_heads.cu +372 -0
- ltx-2-internal/internal/ltx-kernels/csrc/all2all/cuda/allgather.cu +197 -0
- ltx-2-internal/internal/ltx-kernels/csrc/all2all/cuda/api.cuh +99 -0
- ltx-2-internal/internal/ltx-kernels/csrc/include/cuda/configs.cuh +90 -0
- ltx-2-internal/internal/ltx-kernels/csrc/include/cuda/exceptions.cuh +170 -0
- ltx-2-internal/internal/ltx-kernels/csrc/include/cuda/utils.cuh +356 -0
- ltx-2-internal/internal/ltx-kernels/csrc/include/event.hpp +114 -0
- ltx-2-internal/internal/ltx-kernels/pyproject.toml +18 -0
- ltx-2-internal/internal/ltx-kernels/setup.py +39 -0
- ltx-2-internal/internal/ltx-kernels/src/ltx_kernels/__init__.py +6 -0
- ltx-2-internal/internal/ltx-kernels/src/ltx_kernels/all_to_all.py +90 -0
- ltx-2-internal/internal/test-txt2img-parity/conftest.py +85 -0
- ltx-2-internal/internal/test-txt2img-parity/pyproject.toml +38 -0
- ltx-2-internal/internal/test-txt2img-parity/src/test_txt2img_parity/__init__.py +0 -0
- ltx-2-internal/internal/test-txt2img-parity/src/test_txt2img_parity/testing_facilities.py +282 -0
- ltx-2-internal/internal/test-txt2img-parity/uv.lock +0 -0
- ltx-2-internal/packages/ltx-bench/README.md +65 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/__init__.py +0 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/components/__init__.py +0 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/components/conftest.py +140 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/components/test_audio_vae.py +119 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/components/test_component_loading.py +68 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/components/test_lora_fusion.py +56 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/components/test_text_encoder.py +45 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/components/test_transformer.py +282 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/components/test_upsampler.py +67 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/components/test_video_vae.py +127 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/conftest.py +190 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/__init__.py +0 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/conftest.py +10 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/test_pipeline_distilled.py +88 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/test_pipeline_two_stage.py +178 -0
- ltx-2-internal/packages/ltx-bench/benchmarks/pipelines/test_pipeline_two_stage_hq.py +101 -0
- ltx-2-internal/packages/ltx-bench/pyproject.toml +14 -0
- ltx-2-internal/packages/ltx-bench/src/ltx_bench/__init__.py +31 -0
- ltx-2-internal/packages/ltx-bench/src/ltx_bench/conftest.py +215 -0
- ltx-2-internal/packages/ltx-bench/src/ltx_bench/core.py +499 -0
- ltx-2-internal/packages/ltx-bench/src/ltx_bench/instrument.py +375 -0
- ltx-2-internal/packages/ltx-bench/src/ltx_bench/memory_graph.py +146 -0
- 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
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: indigo
|
| 5 |
-
colorTo:
|
| 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 |
-
|
|
|
|
| 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(
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
LTX_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LTX-2")
|
| 15 |
-
LTX_COMMIT_SHA = "ae855f8538843825f9015a419cf4ba5edaf5eec2"
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
subprocess.run(
|
| 28 |
-
[sys.executable, "-m", "pip", "install",
|
| 29 |
-
|
| 30 |
-
"-e",
|
|
|
|
| 31 |
check=True,
|
| 32 |
)
|
| 33 |
-
|
| 34 |
-
sys.path.insert(0,
|
| 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 |
-
|
| 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
|
| 52 |
from ltx_core.quantization import QuantizationPolicy
|
| 53 |
-
from ltx_pipelines.
|
| 54 |
-
from ltx_pipelines.utils.
|
| 55 |
-
from ltx_pipelines.utils.media_io import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
#
|
| 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 |
-
#
|
| 79 |
RESOLUTIONS = {
|
| 80 |
-
"
|
| 81 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 89 |
print("=" * 80)
|
| 90 |
-
print("Downloading LTX-2.3 distilled
|
| 91 |
print("=" * 80)
|
| 92 |
|
| 93 |
-
checkpoint_path = hf_hub_download(repo_id=LTX_MODEL_REPO, filename=
|
| 94 |
-
spatial_upsampler_path = hf_hub_download(repo_id=LTX_MODEL_REPO, filename=
|
|
|
|
| 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 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
gemma_root=gemma_root,
|
| 106 |
-
|
| 107 |
-
|
| 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 |
-
|
| 133 |
-
|
| 134 |
-
print("=" * 80)
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
| 148 |
return "16:9"
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
else:
|
| 154 |
return "16:9"
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
| 157 |
return min(candidates, key=lambda k: abs(ratio - candidates[k]))
|
| 158 |
|
| 159 |
|
| 160 |
-
def
|
| 161 |
-
"""Auto-set resolution when image is uploaded."""
|
| 162 |
-
aspect = detect_aspect_ratio(image)
|
| 163 |
tier = "high" if high_res else "low"
|
| 164 |
-
|
| 165 |
-
return gr.update(value=w), gr.update(value=h)
|
| 166 |
|
| 167 |
|
| 168 |
-
def
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
@torch.inference_mode()
|
| 178 |
def generate_video(
|
| 179 |
-
|
| 180 |
prompt: str,
|
| 181 |
duration: float,
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
| 187 |
progress=gr.Progress(track_tqdm=True),
|
| 188 |
):
|
|
|
|
| 189 |
try:
|
| 190 |
torch.cuda.reset_peak_memory_stats()
|
| 191 |
-
log_memory("start")
|
| 192 |
|
| 193 |
-
|
|
|
|
| 194 |
|
| 195 |
-
|
|
|
|
|
|
|
| 196 |
num_frames = int(duration * frame_rate) + 1
|
| 197 |
num_frames = ((num_frames - 1 + 7) // 8) * 8 + 1
|
| 198 |
|
| 199 |
-
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 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 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
-
|
|
|
|
| 216 |
|
| 217 |
-
|
| 218 |
-
prompt=prompt,
|
| 219 |
seed=current_seed,
|
| 220 |
-
height=int(
|
| 221 |
-
width=int(
|
| 222 |
num_frames=num_frames,
|
| 223 |
-
frame_rate=frame_rate,
|
| 224 |
-
|
| 225 |
tiling_config=tiling_config,
|
| 226 |
-
|
| 227 |
)
|
| 228 |
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
video_chunks_number=video_chunks_number,
|
| 238 |
-
)
|
| 239 |
|
| 240 |
-
|
| 241 |
-
return str(output_path), current_seed
|
| 242 |
|
| 243 |
except Exception as e:
|
| 244 |
import traceback
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
return None, current_seed
|
| 248 |
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
with gr.Row():
|
| 259 |
-
with gr.Column():
|
| 260 |
-
|
|
|
|
| 261 |
prompt = gr.Textbox(
|
| 262 |
label="Prompt",
|
| 263 |
-
info="
|
| 264 |
-
|
| 265 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
| 299 |
)
|
| 300 |
|
| 301 |
generate_btn.click(
|
| 302 |
fn=generate_video,
|
| 303 |
inputs=[
|
| 304 |
-
|
| 305 |
-
seed, randomize_seed,
|
| 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(
|
| 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 |
+
[](https://ltx.io)
|
| 4 |
+
[](https://huggingface.co/Lightricks/LTX-2.3)
|
| 5 |
+
[](https://console.ltx.video/playground)
|
| 6 |
+
[](https://arxiv.org/abs/2601.03233)
|
| 7 |
+
[](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", <x_kernels::all2all::All2All::get_local_ipc_handle,
|
| 397 |
+
"Returns the IPC handle for this rank's buffer.")
|
| 398 |
+
.def("sync", <x_kernels::all2all::All2All::sync, "Opens IPC mappings to all peer GPUs using gathered handles.")
|
| 399 |
+
.def("destroy", <x_kernels::all2all::All2All::destroy,
|
| 400 |
+
"Releases all GPU resources. Must be called before destruction.")
|
| 401 |
+
.def("send_recv_heads", <x_kernels::all2all::All2All::send_recv_heads,
|
| 402 |
+
"All2All operation to redistribute attention heads.")
|
| 403 |
+
.def("gather_heads", <x_kernels::all2all::All2All::gather_heads,
|
| 404 |
+
"Inverse All2All to gather heads back to original distribution.")
|
| 405 |
+
.def("allgather", <x_kernels::all2all::All2All::allgather, "Gathers sequence tokens from all ranks.")
|
| 406 |
+
.def("set_rank_tokens", <x_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()
|