Skip to content

Commit b717bae

Browse files
committed
Add the LTX2 FP8/BF16 support + Some core code changes
Signed-off-by: Jingyu Xin <jingyux@nvidia.com>
1 parent b8b5eaf commit b717bae

9 files changed

Lines changed: 262 additions & 20 deletions

File tree

examples/diffusers/quantization/models_utils.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ModelType(str, Enum):
4242
FLUX_DEV = "flux-dev"
4343
FLUX_SCHNELL = "flux-schnell"
4444
LTX_VIDEO_DEV = "ltx-video-dev"
45+
LTX2 = "ltx-2"
4546
WAN22_T2V_14b = "wan2.2-t2v-14b"
4647
WAN22_T2V_5b = "wan2.2-t2v-5b"
4748

@@ -64,6 +65,7 @@ def get_model_filter_func(model_type: ModelType) -> Callable[[str], bool]:
6465
ModelType.SD3_MEDIUM: filter_func_default,
6566
ModelType.SD35_MEDIUM: filter_func_default,
6667
ModelType.LTX_VIDEO_DEV: filter_func_ltx_video,
68+
ModelType.LTX2: filter_func_ltx_video,
6769
ModelType.WAN22_T2V_14b: filter_func_wan_video,
6870
ModelType.WAN22_T2V_5b: filter_func_wan_video,
6971
}
@@ -80,18 +82,20 @@ def get_model_filter_func(model_type: ModelType) -> Callable[[str], bool]:
8082
ModelType.FLUX_DEV: "black-forest-labs/FLUX.1-dev",
8183
ModelType.FLUX_SCHNELL: "black-forest-labs/FLUX.1-schnell",
8284
ModelType.LTX_VIDEO_DEV: "Lightricks/LTX-Video-0.9.7-dev",
85+
ModelType.LTX2: "Lightricks/LTX-2",
8386
ModelType.WAN22_T2V_14b: "Wan-AI/Wan2.2-T2V-A14B-Diffusers",
8487
ModelType.WAN22_T2V_5b: "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
8588
}
8689

87-
MODEL_PIPELINE: dict[ModelType, type[DiffusionPipeline]] = {
90+
MODEL_PIPELINE: dict[ModelType, type[DiffusionPipeline] | None] = {
8891
ModelType.SDXL_BASE: DiffusionPipeline,
8992
ModelType.SDXL_TURBO: DiffusionPipeline,
9093
ModelType.SD3_MEDIUM: StableDiffusion3Pipeline,
9194
ModelType.SD35_MEDIUM: StableDiffusion3Pipeline,
9295
ModelType.FLUX_DEV: FluxPipeline,
9396
ModelType.FLUX_SCHNELL: FluxPipeline,
9497
ModelType.LTX_VIDEO_DEV: LTXConditionPipeline,
98+
ModelType.LTX2: None,
9599
ModelType.WAN22_T2V_14b: WanPipeline,
96100
ModelType.WAN22_T2V_5b: WanPipeline,
97101
}
@@ -154,6 +158,18 @@ def get_model_filter_func(model_type: ModelType) -> Callable[[str], bool]:
154158
"negative_prompt": "worst quality, inconsistent motion, blurry, jittery, distorted",
155159
},
156160
},
161+
ModelType.LTX2: {
162+
"backbone": "transformer",
163+
"dataset": _SD_PROMPTS_DATASET,
164+
"inference_extra_args": {
165+
"height": 1024,
166+
"width": 1536,
167+
"num_frames": 121,
168+
"frame_rate": 24.0,
169+
"cfg_guidance_scale": 4.0,
170+
"negative_prompt": "worst quality, inconsistent motion, blurry, jittery, distorted",
171+
},
172+
},
157173
ModelType.WAN22_T2V_14b: {
158174
**_WAN_BASE_CONFIG,
159175
"from_pretrained_extra_args": {

examples/diffusers/quantization/quantize.py

Lines changed: 158 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ class ModelConfig:
164164
override_model_path: Path | None = None
165165
cpu_offloading: bool = False
166166
ltx_skip_upsampler: bool = False # Skip upsampler for LTX-Video (faster calibration)
167+
extra_params: dict[str, Any] = field(default_factory=dict)
167168

168169
@property
169170
def model_path(self) -> str:
@@ -232,6 +233,51 @@ def setup_logging(verbose: bool = False) -> logging.Logger:
232233
return logger
233234

234235

236+
def _coerce_extra_param_value(value: str) -> Any:
237+
lowered = value.lower()
238+
if lowered in {"true", "false"}:
239+
return lowered == "true"
240+
try:
241+
return int(value)
242+
except ValueError:
243+
pass
244+
try:
245+
return float(value)
246+
except ValueError:
247+
return value
248+
249+
250+
def parse_extra_params(
251+
kv_args: list[str], unknown_args: list[str], logger: logging.Logger
252+
) -> dict[str, Any]:
253+
extra_params: dict[str, Any] = {}
254+
for item in kv_args:
255+
if "=" not in item:
256+
raise ValueError(f"Invalid --extra-param value: '{item}'. Expected KEY=VALUE.")
257+
key, value = item.split("=", 1)
258+
extra_params[key] = _coerce_extra_param_value(value)
259+
260+
i = 0
261+
while i < len(unknown_args):
262+
token = unknown_args[i]
263+
if token.startswith("--extra_param."):
264+
key = token[len("--extra_param.") :]
265+
value = "true"
266+
if i + 1 < len(unknown_args) and not unknown_args[i + 1].startswith("--"):
267+
value = unknown_args[i + 1]
268+
i += 1
269+
extra_params[key] = _coerce_extra_param_value(value)
270+
elif token.startswith("--extra_param"):
271+
raise ValueError(
272+
"Use --extra_param.KEY VALUE or --extra-param KEY=VALUE for extra parameters."
273+
)
274+
else:
275+
logger.warning("Ignoring unknown argument: %s", token)
276+
i += 1
277+
278+
return extra_params
279+
280+
235281
class PipelineManager:
236282
"""Manages diffusion pipeline creation and configuration."""
237283

@@ -245,8 +291,9 @@ def __init__(self, config: ModelConfig, logger: logging.Logger):
245291
"""
246292
self.config = config
247293
self.logger = logger
248-
self.pipe: DiffusionPipeline | None = None
294+
self.pipe: Any | None = None
249295
self.pipe_upsample: LTXLatentUpsamplePipeline | None = None # For LTX-Video upsampling
296+
self._transformer: torch.nn.Module | None = None
250297

251298
@staticmethod
252299
def create_pipeline_from(
@@ -264,10 +311,13 @@ def create_pipeline_from(
264311
ValueError: If model type is unsupported
265312
"""
266313
try:
314+
pipeline_cls = MODEL_PIPELINE[model_type]
315+
if pipeline_cls is None:
316+
raise ValueError(f"Model type {model_type.value} does not use diffusers pipelines.")
267317
model_id = (
268318
MODEL_REGISTRY[model_type] if override_model_path is None else override_model_path
269319
)
270-
pipe = MODEL_PIPELINE[model_type].from_pretrained(
320+
pipe = pipeline_cls.from_pretrained(
271321
model_id,
272322
torch_dtype=torch_dtype,
273323
use_safetensors=True,
@@ -278,7 +328,7 @@ def create_pipeline_from(
278328
except Exception as e:
279329
raise e
280330

281-
def create_pipeline(self) -> DiffusionPipeline:
331+
def create_pipeline(self) -> Any:
282332
"""
283333
Create and return an appropriate pipeline based on configuration.
284334
@@ -293,6 +343,14 @@ def create_pipeline(self) -> DiffusionPipeline:
293343
self.logger.info(f"Data type: {self.config.model_dtype}")
294344

295345
try:
346+
if self.config.model_type == ModelType.LTX2:
347+
from modelopt.torch.quantization.plugins.diffusion import ltx2 as ltx2_plugin
348+
349+
ltx2_plugin.register_ltx2_quant_linear()
350+
self.pipe = self._create_ltx2_pipeline()
351+
self.logger.info("LTX-2 pipeline created successfully")
352+
return self.pipe
353+
296354
self.pipe = MODEL_PIPELINE[self.config.model_type].from_pretrained(
297355
self.config.model_path,
298356
torch_dtype=self.config.model_dtype,
@@ -325,6 +383,10 @@ def setup_device(self) -> None:
325383
if not self.pipe:
326384
raise RuntimeError("Pipeline not created. Call create_pipeline() first.")
327385

386+
if self.config.model_type == ModelType.LTX2:
387+
self.logger.info("Skipping device setup for LTX-2 pipeline (handled internally)")
388+
return
389+
328390
if self.config.cpu_offloading:
329391
self.logger.info("Enabling CPU offloading for memory efficiency")
330392
self.pipe.enable_model_cpu_offload()
@@ -352,8 +414,58 @@ def get_backbone(self) -> torch.nn.Module:
352414
if not self.pipe:
353415
raise RuntimeError("Pipeline not created. Call create_pipeline() first.")
354416

417+
if self.config.model_type == ModelType.LTX2:
418+
self._ensure_ltx2_transformer_cached()
419+
return self._transformer
355420
return getattr(self.pipe, self.config.backbone)
356421

422+
def _ensure_ltx2_transformer_cached(self) -> None:
423+
if not self.pipe:
424+
raise RuntimeError("Pipeline not created. Call create_pipeline() first.")
425+
if self._transformer is None:
426+
transformer = self.pipe.stage_1_model_ledger.transformer()
427+
self.pipe.stage_1_model_ledger.transformer = lambda: transformer
428+
self._transformer = transformer
429+
430+
def _create_ltx2_pipeline(self) -> Any:
431+
params = dict(self.config.extra_params)
432+
checkpoint_path = params.pop("checkpoint_path", None)
433+
distilled_lora_path = params.pop("distilled_lora_path", None)
434+
distilled_lora_strength = params.pop("distilled_lora_strength", 0.8)
435+
spatial_upsampler_path = params.pop("spatial_upsampler_path", None)
436+
gemma_root = params.pop("gemma_root", None)
437+
fp8transformer = params.pop("fp8transformer", False)
438+
439+
if not checkpoint_path:
440+
raise ValueError("Missing required extra_param: checkpoint_path.")
441+
if not distilled_lora_path:
442+
raise ValueError("Missing required extra_param: distilled_lora_path.")
443+
if not spatial_upsampler_path:
444+
raise ValueError("Missing required extra_param: spatial_upsampler_path.")
445+
if not gemma_root:
446+
raise ValueError("Missing required extra_param: gemma_root.")
447+
448+
from ltx_core.loader import LTXV_LORA_COMFY_RENAMING_MAP, LoraPathStrengthAndSDOps
449+
from ltx_pipelines.ti2vid_two_stages import TI2VidTwoStagesPipeline
450+
451+
distilled_lora = [
452+
LoraPathStrengthAndSDOps(
453+
str(distilled_lora_path),
454+
float(distilled_lora_strength),
455+
LTXV_LORA_COMFY_RENAMING_MAP,
456+
)
457+
]
458+
pipeline_kwargs = {
459+
"checkpoint_path": str(checkpoint_path),
460+
"distilled_lora": distilled_lora,
461+
"spatial_upsampler_path": str(spatial_upsampler_path),
462+
"gemma_root": str(gemma_root),
463+
"loras": [],
464+
"fp8transformer": bool(fp8transformer),
465+
}
466+
pipeline_kwargs.update(params)
467+
return TI2VidTwoStagesPipeline(**pipeline_kwargs)
468+
357469

358470
class Calibrator:
359471
"""Handles model calibration for quantization."""
@@ -417,7 +529,9 @@ def run_calibration(self, batched_prompts: list[list[str]]) -> None:
417529
if i >= self.config.num_batches:
418530
break
419531

420-
if self.model_type == ModelType.LTX_VIDEO_DEV:
532+
if self.model_type == ModelType.LTX2:
533+
self._run_ltx2_calibration(prompt_batch, extra_args)
534+
elif self.model_type == ModelType.LTX_VIDEO_DEV:
421535
# Special handling for LTX-Video
422536
self._run_ltx_video_calibration(prompt_batch, extra_args)
423537
elif self.model_type in [ModelType.WAN22_T2V_14b, ModelType.WAN22_T2V_5b]:
@@ -448,6 +562,29 @@ def _run_wan_video_calibration(
448562

449563
self.pipe(prompt=prompt_batch, **kwargs).frames # type: ignore[misc]
450564

565+
def _run_ltx2_calibration(self, prompt_batch: list[str], extra_args: dict[str, Any]) -> None:
566+
from ltx_core.model.video_vae import TilingConfig
567+
568+
prompt = prompt_batch[0]
569+
extra_params = self.pipeline_manager.config.extra_params
570+
kwargs = {
571+
"negative_prompt": extra_args.get(
572+
"negative_prompt", "worst quality, inconsistent motion, blurry, jittery, distorted"
573+
),
574+
"seed": extra_params.get("seed", 0),
575+
"height": extra_params.get("height", extra_args.get("height", 1024)),
576+
"width": extra_params.get("width", extra_args.get("width", 1536)),
577+
"num_frames": extra_params.get("num_frames", extra_args.get("num_frames", 121)),
578+
"frame_rate": extra_params.get("frame_rate", extra_args.get("frame_rate", 24.0)),
579+
"num_inference_steps": self.config.n_steps,
580+
"cfg_guidance_scale": extra_params.get(
581+
"cfg_guidance_scale", extra_args.get("cfg_guidance_scale", 4.0)
582+
),
583+
"images": extra_params.get("images", []),
584+
"tiling_config": extra_params.get("tiling_config", TilingConfig.default()),
585+
}
586+
self.pipe(prompt=prompt, **kwargs) # type: ignore[misc]
587+
451588
def _run_ltx_video_calibration(
452589
self, prompt_batch: list[str], extra_args: dict[str, Any]
453590
) -> None:
@@ -568,7 +705,7 @@ def quantize_model(
568705
backbone: torch.nn.Module,
569706
quant_config: Any,
570707
forward_loop: callable, # type: ignore[valid-type]
571-
) -> None:
708+
) -> torch.nn.Module:
572709
"""
573710
Apply quantization to the model.
574711
@@ -590,6 +727,7 @@ def quantize_model(
590727
mtq.disable_quantizer(backbone, model_filter_func)
591728

592729
self.logger.info("Quantization completed successfully")
730+
return backbone
593731

594732

595733
class ExportManager:
@@ -754,7 +892,7 @@ def create_argument_parser() -> argparse.ArgumentParser:
754892
model_group.add_argument(
755893
"--model-dtype",
756894
type=str,
757-
default="Half",
895+
default="BFloat16",
758896
choices=[d.value for d in DataType],
759897
help="Precision for loading the pipeline. If you want different dtypes for separate components, "
760898
"please specify using --component-dtype",
@@ -778,6 +916,16 @@ def create_argument_parser() -> argparse.ArgumentParser:
778916
action="store_true",
779917
help="Skip upsampler pipeline for LTX-Video (faster calibration, only quantizes main transformer)",
780918
)
919+
model_group.add_argument(
920+
"--extra-param",
921+
action="append",
922+
default=[],
923+
metavar="KEY=VALUE",
924+
help=(
925+
"Extra model-specific parameters in KEY=VALUE form. Can be provided multiple times. "
926+
"These override model-specific CLI arguments when present."
927+
),
928+
)
781929
quant_group = parser.add_argument_group("Quantization Configuration")
782930
quant_group.add_argument(
783931
"--format",
@@ -859,7 +1007,7 @@ def create_argument_parser() -> argparse.ArgumentParser:
8591007

8601008
def main() -> None:
8611009
parser = create_argument_parser()
862-
args = parser.parse_args()
1010+
args, unknown_args = parser.parse_known_args()
8631011

8641012
model_type = ModelType(args.model)
8651013
if args.backbone is None:
@@ -875,6 +1023,7 @@ def main() -> None:
8751023
logger.info("Starting Enhanced Diffusion Model Quantization")
8761024

8771025
try:
1026+
extra_params = parse_extra_params(args.extra_param, unknown_args, logger)
8781027
model_config = ModelConfig(
8791028
model_type=model_type,
8801029
model_dtype=model_dtype,
@@ -885,6 +1034,7 @@ def main() -> None:
8851034
else None,
8861035
cpu_offloading=args.cpu_offloading,
8871036
ltx_skip_upsampler=args.ltx_skip_upsampler,
1037+
extra_params=extra_params,
8881038
)
8891039

8901040
quant_config = QuantizationConfig(
@@ -950,6 +1100,7 @@ def main() -> None:
9501100
quantizer = Quantizer(quant_config, model_config, logger)
9511101
backbone_quant_config = quantizer.get_quant_config(calib_config.n_steps, backbone)
9521102

1103+
# Pipe loads the ckpt just before the inference.
9531104
def forward_loop(mod):
9541105
calibrator.run_calibration(batched_prompts)
9551106

examples/diffusers/quantization/utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from diffusers.utils import load_image
2626

2727
import modelopt.torch.quantization as mtq
28-
from modelopt.torch.quantization.plugins.diffusers import AttentionModuleMixin
28+
from modelopt.torch.quantization.plugins.diffusion.diffusers import AttentionModuleMixin
2929

3030
USE_PEFT = True
3131
try:
@@ -69,7 +69,9 @@ def check_conv_and_mha(backbone, if_fp4, quantize_mha):
6969

7070
def filter_func_ltx_video(name: str) -> bool:
7171
"""Filter function specifically for LTX-Video models."""
72-
pattern = re.compile(r".*(proj_in|time_embed|caption_projection|proj_out).*")
72+
pattern = re.compile(
73+
r".*(proj_in|time_embed|caption_projection|proj_out|patchify_proj|adaln_single).*"
74+
)
7375
return pattern.match(name) is not None
7476

7577

modelopt/torch/opt/dynamic.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,10 @@ def bind_forward_method_if_needed(self):
621621
# accelerate patched module
622622
bind_forward_method(self, self.__class__.forward)
623623
else:
624+
if not hasattr(self, "_forward_pre_dm"):
625+
# Keep the patched forward for downstream modules that want to call it.
626+
self._forward_pre_dm = self.forward
627+
bind_forward_method(self, self.__class__.forward)
624628
warnings.warn(
625629
"Received a module with monkey patched forward method. Dynamic converted module"
626630
" might not work."

modelopt/torch/quantization/nn/modules/quant_module.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ class QuantInputBase(QuantModule):
110110
def forward(self, input, *args, **kwargs):
111111
"""Quantize the input before calling the original forward method."""
112112
input = self.input_quantizer(input)
113-
output = super().forward(input, *args, **kwargs)
113+
if hasattr(self, "_forward_pre_dm"):
114+
output = self._forward_pre_dm(input, *args, **kwargs)
115+
else:
116+
output = super().forward(input, *args, **kwargs)
114117
if isinstance(output, tuple):
115118
return (self.output_quantizer(output[0]), *output[1:])
116119
return self.output_quantizer(output)

0 commit comments

Comments
 (0)