返回 JoyAI-Echo
generator_loader.py
1 """Precision-aware generator loading for Echo 1.5 inference."""
2
3 from __future__ import annotations
4
5 from dataclasses import dataclass
6 import gc
7 from pathlib import Path
8 import warnings
9
10 import torch
11
12 from ltx_distillation.models.ltx_wrapper import create_ltx2_wrapper
13 from ltx_distillation.models.ltx_wrapper import LTX2DiffusionWrapper
14 from ltx_distillation.quantization import (
15 build_prequant_fp8_policy,
16 inspect_prequant_fp8_checkpoint,
17 )
18 from ltx_distillation.release_checkpoint import (
19 ReleaseCheckpoint,
20 resolve_release_checkpoint,
21 )
22 from ltx_core.loader.sft_loader import SafetensorsModelStateDictLoader
23 from ltx_core.model.transformer import LTXModelConfigurator, X0Model
24
25
26 BF16 = "bf16"
27 FP8 = "fp8"
28 FP4 = "fp4"
29
30
31 @dataclass(frozen=True)
32 class GeneratorLoadReport:
33 """Serializable record of the generator topology and weights used at runtime."""
34
35 mode: str
36 checkpoint: str
37 backend: str
38 format: str
39 quantized_modules: int | None = None
40 missing_keys: tuple[str, ...] = ()
41 unexpected_keys: tuple[str, ...] = ()
42
43
44 def _create_wrapper(
45 checkpoint: Path,
46 gemma_path: Path,
47 device: torch.device,
48 dtype: torch.dtype,
49 video_height: int,
50 video_width: int,
51 *,
52 quantization=None,
53 ):
54 return create_ltx2_wrapper(
55 checkpoint_path=str(checkpoint),
56 gemma_path=str(gemma_path),
57 device=device,
58 dtype=dtype,
59 video_height=video_height,
60 video_width=video_width,
61 quantization=quantization,
62 )
63
64
65 def _load_fp4_modelopt_generator(
66 *,
67 components: Path,
68 checkpoint: Path,
69 device: torch.device,
70 video_height: int,
71 video_width: int,
72 ) -> tuple[torch.nn.Module, int]:
73 try:
74 from modelopt.torch.opt.conversion import restore_from_modelopt_state
75 from modelopt.torch.quantization.plugins.diffusion.ltx2 import (
76 register_ltx2_quant_linear,
77 )
78 from modelopt.torch.utils import safe_load
79 except ImportError as error:
80 raise ImportError(
81 "FP4 inference requires NVIDIA ModelOpt 0.45.0; install requirements-fp4.txt"
82 ) from error
83
84 config = SafetensorsModelStateDictLoader().metadata(str(components))
85 if not config:
86 raise ValueError(
87 f"FP4 components checkpoint is missing LTX config metadata: {components}"
88 )
89
90 # Build only the topology. No BF16 DiT parameters are materialized: ModelOpt
91 # mutates this meta graph and the packed checkpoint tensors are then assigned
92 # directly into it. This is the important distinction from modelopt.restore(),
93 # which first required a complete BF16 velocity model.
94 with torch.device("meta"):
95 velocity_model = LTXModelConfigurator.from_config(config)
96
97 register_ltx2_quant_linear()
98 packed = safe_load(
99 str(checkpoint),
100 map_location="cpu",
101 mmap=True,
102 # Official ModelOpt checkpoints contain its QTensor subclasses and
103 # conversion metadata. Only load checkpoints from the trusted release.
104 weights_only=False,
105 )
106 if not isinstance(packed, dict) or not {
107 "modelopt_state",
108 "model_state_dict",
109 }.issubset(packed):
110 raise ValueError(f"invalid packed ModelOpt checkpoint: {checkpoint}")
111 velocity_model = restore_from_modelopt_state(
112 velocity_model,
113 packed["modelopt_state"],
114 )
115 incompatible = velocity_model.load_state_dict(
116 packed["model_state_dict"],
117 strict=True,
118 assign=True,
119 )
120 if incompatible.missing_keys or incompatible.unexpected_keys:
121 raise RuntimeError(
122 "packed FP4 state does not match the LTX transformer topology: "
123 f"missing={len(incompatible.missing_keys)} "
124 f"unexpected={len(incompatible.unexpected_keys)}"
125 )
126 del packed
127 gc.collect()
128
129 generator = LTX2DiffusionWrapper(
130 model=X0Model(velocity_model),
131 video_height=video_height,
132 video_width=video_width,
133 )
134 generator.to(device)
135 # ModelOpt emits this once per unsupported matrix shape and can flood a
136 # single inference log with thousands of multi-line warnings. The public
137 # README documents the fallback; retain all other ModelOpt warnings.
138 warnings.filterwarnings(
139 "ignore",
140 message=r"RealQuantLinear: No real-quant GEMM found:.*",
141 category=UserWarning,
142 module=r"modelopt\.torch\.quantization\.nn\.modules\.quant_linear",
143 )
144 quantized_modules = sum(
145 module.__class__.__name__ == "TensorQuantizer" for module in generator.modules()
146 )
147 return generator, quantized_modules
148
149
150 def load_inference_generator(
151 *,
152 checkpoint: str | Path | ReleaseCheckpoint,
153 gemma_path: str | Path,
154 device: torch.device,
155 dtype: torch.dtype,
156 video_height: int,
157 video_width: int,
158 load_on_cpu: bool = False,
159 ) -> tuple[torch.nn.Module, GeneratorLoadReport]:
160 """Build a generator from one of the three public checkpoint directories."""
161
162 release = (
163 checkpoint
164 if isinstance(checkpoint, ReleaseCheckpoint)
165 else resolve_release_checkpoint(checkpoint)
166 )
167 mode = release.precision
168 model_path = release.model_path
169 gemma = Path(gemma_path).expanduser().resolve()
170 load_device = torch.device("cpu") if load_on_cpu else device
171
172 if mode == BF16:
173 generator = _create_wrapper(
174 model_path, gemma, load_device, dtype, video_height, video_width
175 )
176 generator.eval()
177 return generator, GeneratorLoadReport(
178 mode=mode,
179 checkpoint=str(release.root),
180 backend="torch-bfloat16",
181 format="full_dmd_merged",
182 )
183
184 if mode == FP8:
185 if not hasattr(torch, "_scaled_mm"):
186 raise RuntimeError("this PyTorch build does not provide torch._scaled_mm")
187 info = inspect_prequant_fp8_checkpoint(model_path)
188 generator = _create_wrapper(
189 model_path,
190 gemma,
191 load_device,
192 dtype,
193 video_height,
194 video_width,
195 quantization=build_prequant_fp8_policy(info),
196 )
197 generator.eval()
198 return generator, GeneratorLoadReport(
199 mode=mode,
200 checkpoint=str(release.root),
201 backend="torch-scaled-mm",
202 format="full_prequant_e4m3_scaled_mm",
203 quantized_modules=info.module_count,
204 )
205
206 if release.modelopt_path is None:
207 raise ValueError("echo15_fp4 is missing its packed ModelOpt state")
208 target_device = torch.device("cpu") if load_on_cpu else device
209 generator, quantized_modules = _load_fp4_modelopt_generator(
210 components=model_path,
211 checkpoint=release.modelopt_path,
212 device=target_device,
213 video_height=video_height,
214 video_width=video_width,
215 )
216 generator.eval()
217 return generator, GeneratorLoadReport(
218 mode=mode,
219 checkpoint=str(release.root),
220 backend="modelopt-nvfp4-packed",
221 format="modelopt_nvfp4_e2m1_block16_fp8_scale",
222 quantized_modules=quantized_modules,
223 )
224
224 lines PYTHON