Skip to content

Commit 7c8dde8

Browse files
[TRTLLM-12647][feat] Parallelize LTX-2 LoRA weight loading (#13911)
Signed-off-by: Yibin Li <109242046+yibinl-nvidia@users.noreply.github.com>
1 parent 011e849 commit 7c8dde8

2 files changed

Lines changed: 163 additions & 34 deletions

File tree

tensorrt_llm/_torch/visual_gen/models/ltx2/pipeline_ltx2_two_stages.py

Lines changed: 90 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import math
77
import time
8+
from concurrent.futures import ThreadPoolExecutor
89
from dataclasses import dataclass, field
910
from typing import Any, Callable, Dict, List, Optional, Union
1011

@@ -49,6 +50,7 @@
4950
_FP8_DTYPES = (torch.float8_e4m3fn, torch.float8_e5m2)
5051
# Baseline BF16 peak memory ~75 GiB, saving BF16 weights snapshot total ~108 GiB.
5152
_BF16_WEIGHTS_SNAPSHOT_FREE_MEMORY_THRESHOLD_GIB = 115.0
53+
_QKV_SUFFIXES = (".to_q", ".to_k", ".to_v")
5254

5355

5456
# ---------------------------------------------------------------------------
@@ -99,6 +101,36 @@ def _should_save_bf16_weights(
99101
return save_state
100102

101103

104+
def _map_lora_param_name(base_name: str, strip_prefixes: List[str]) -> str:
105+
param_name = base_name
106+
for prefix in strip_prefixes:
107+
if param_name.startswith(prefix):
108+
param_name = param_name[len(prefix) :]
109+
break
110+
111+
# Apply the same key remapping as LTXModel.load_weights() so LoRA delta
112+
# keys match TRT-LLM parameter names.
113+
for ff_prefix in (".ff.", ".audio_ff."):
114+
if ff_prefix + "net.0.proj" in param_name:
115+
param_name = param_name.replace(ff_prefix + "net.0.proj", ff_prefix + "up_proj")
116+
elif ff_prefix + "net.2" in param_name:
117+
param_name = param_name.replace(ff_prefix + "net.2", ff_prefix + "down_proj")
118+
param_name = param_name.replace(".q_norm.", ".norm_q.")
119+
param_name = param_name.replace(".k_norm.", ".norm_k.")
120+
return param_name
121+
122+
123+
def _has_lora_target(param_name: str, model_params: set[str]) -> bool:
124+
if param_name in model_params or f"{param_name}.weight" in model_params:
125+
return True
126+
127+
for suffix in _QKV_SUFFIXES:
128+
if param_name.endswith(suffix):
129+
attn_prefix = param_name[: -len(suffix)]
130+
return f"{attn_prefix}.qkv_proj.weight" in model_params
131+
return False
132+
133+
102134
def _load_lora_deltas(
103135
lora_path: str,
104136
transformer: torch.nn.Module,
@@ -117,7 +149,9 @@ def _load_lora_deltas(
117149
sft_paths = _find_safetensors_files(lora_path)
118150
if not sft_paths:
119151
raise ValueError(f"No safetensors files found at {lora_path}")
120-
_prefetch_ltx2_safetensors_files(sft_paths)
152+
# This helper can run in a background thread while base components load.
153+
# Avoid distributed prefetch collectives here; every rank must enter those
154+
# from the same foreground load sequence to avoid hangs.
121155

122156
raw: Dict[str, torch.Tensor] = {}
123157
alpha_dict: Dict[str, float] = {}
@@ -163,39 +197,28 @@ def _strip(key, suffixes):
163197
if suffix:
164198
strip_prefixes.append(suffix)
165199

200+
model_params = {n for n, _ in transformer.named_parameters()}
166201
deltas: Dict[str, torch.Tensor] = {}
202+
skipped_non_targets = 0
167203
for base_name in down_keys:
168204
if base_name not in up_keys:
169205
continue
206+
207+
param_name = _map_lora_param_name(base_name, strip_prefixes)
208+
if not _has_lora_target(param_name, model_params):
209+
skipped_non_targets += 1
210+
continue
211+
170212
A = down_keys[base_name] # (rank, in_features)
171213
B = up_keys[base_name] # (out_features, rank)
172214
rank = A.shape[0]
173215
alpha = alpha_dict.get(base_name, float(rank))
174216
scale = strength * alpha / rank
175-
176-
param_name = base_name
177-
for prefix in strip_prefixes:
178-
if param_name.startswith(prefix):
179-
param_name = param_name[len(prefix) :]
180-
break
181-
182-
# Apply the same key remapping as LTXModel.load_weights() so
183-
# that LoRA delta keys match TRT-LLM parameter names.
184-
for ff_prefix in (".ff.", ".audio_ff."):
185-
if ff_prefix + "net.0.proj" in param_name:
186-
param_name = param_name.replace(ff_prefix + "net.0.proj", ff_prefix + "up_proj")
187-
elif ff_prefix + "net.2" in param_name:
188-
param_name = param_name.replace(ff_prefix + "net.2", ff_prefix + "down_proj")
189-
param_name = param_name.replace(".q_norm.", ".norm_q.")
190-
param_name = param_name.replace(".k_norm.", ".norm_k.")
191-
192217
delta = (B.float() @ A.float()) * scale
193218
deltas[param_name] = delta
194219

195220
# Fuse separate to_q / to_k / to_v deltas into a single qkv_proj
196221
# delta when the transformer uses QKV fusion (FUSE_QKV mode).
197-
model_params = {n for n, _ in transformer.named_parameters()}
198-
_QKV_SUFFIXES = (".to_q", ".to_k", ".to_v")
199222
fused_keys: set = set()
200223
qkv_groups: Dict[str, List[str]] = {}
201224
for key in list(deltas.keys()):
@@ -223,7 +246,10 @@ def _strip(key, suffixes):
223246
for key in fused_keys:
224247
del deltas[key]
225248

226-
logger.info(f"Loaded {len(deltas)} LoRA deltas from {lora_path} (strength={strength})")
249+
logger.info(
250+
f"Loaded {len(deltas)} LoRA deltas from {lora_path} "
251+
f"(strength={strength}, skipped_non_targets={skipped_non_targets})"
252+
)
227253
return deltas
228254

229255

@@ -967,17 +993,49 @@ def load_standard_components(
967993
# The BF16 snapshot threshold is a whole-pipeline capacity gate, so
968994
# record it before loading model/runtime components that consume HBM.
969995
self._bf16_snapshot_preload_free_gib = _get_free_gpu_memory_gib(device=device)
970-
super().load_standard_components(
971-
checkpoint_dir,
972-
device,
973-
skip_components=skip_components,
974-
**kwargs,
975-
)
976-
977996
dtype = self.pipeline_config.torch_dtype
978997
spatial_upsampler_path = self.pipeline_config.extra_attrs.get("spatial_upsampler_path", "")
979998
distilled_lora_path = self.pipeline_config.extra_attrs.get("distilled_lora_path", "")
980999

1000+
lora_executor = None
1001+
lora_future = None
1002+
if distilled_lora_path:
1003+
logger.info(f"Starting distilled LoRA pre-compute from {distilled_lora_path}...")
1004+
lora_executor = ThreadPoolExecutor(max_workers=1)
1005+
lora_future = lora_executor.submit(
1006+
_load_lora_deltas,
1007+
distilled_lora_path,
1008+
self.transformer,
1009+
self._TRANSFORMER_PREFIX,
1010+
)
1011+
1012+
try:
1013+
super().load_standard_components(
1014+
checkpoint_dir,
1015+
device,
1016+
skip_components=skip_components,
1017+
**kwargs,
1018+
)
1019+
1020+
self._load_two_stage_components(
1021+
device,
1022+
dtype,
1023+
spatial_upsampler_path,
1024+
distilled_lora_path,
1025+
lora_future,
1026+
)
1027+
finally:
1028+
if lora_executor is not None:
1029+
lora_executor.shutdown(wait=True)
1030+
1031+
def _load_two_stage_components(
1032+
self,
1033+
device: torch.device,
1034+
dtype: torch.dtype,
1035+
spatial_upsampler_path: str,
1036+
distilled_lora_path: str,
1037+
lora_future,
1038+
) -> None:
9811039
# --- Spatial upsampler ---
9821040
if spatial_upsampler_path:
9831041
logger.info(f"Loading spatial upsampler from {spatial_upsampler_path}...")
@@ -1018,12 +1076,10 @@ def load_standard_components(
10181076
self._distilled_lora_deltas: Dict[str, torch.Tensor] = {}
10191077
self._distilled_lora_weight_cache: Optional[_PersistentLoRAWeightCache] = None
10201078
if distilled_lora_path:
1021-
logger.info(f"Loading distilled LoRA from {distilled_lora_path}...")
1022-
self._distilled_lora_deltas = _load_lora_deltas(
1023-
distilled_lora_path,
1024-
self.transformer,
1025-
transformer_prefix=self._TRANSFORMER_PREFIX,
1026-
)
1079+
logger.info("Waiting for distilled LoRA pre-compute...")
1080+
if lora_future is None:
1081+
raise RuntimeError("Distilled LoRA pre-compute was not started.")
1082+
self._distilled_lora_deltas = lora_future.result()
10271083
logger.info(
10281084
f"Distilled LoRA ready: {len(self._distilled_lora_deltas)} parameter deltas"
10291085
)

tests/unittest/_torch/visual_gen/test_ltx2_pipeline.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,79 @@ def test_cache_dit_config_prevents_promotion_with_explicit_auxiliary_paths(
960960
assert LTX2Pipeline.resolve_variant(config) is LTX2Pipeline
961961

962962

963+
class TestTwoStageLoRALoadingOverlap:
964+
"""Test two-stage LoRA load orchestration without loading checkpoints."""
965+
966+
def test_load_standard_components_overlaps_lora_with_base_components(self, monkeypatch):
967+
from types import SimpleNamespace
968+
969+
from tensorrt_llm._torch.visual_gen.models.ltx2 import pipeline_ltx2_two_stages
970+
from tensorrt_llm._torch.visual_gen.models.ltx2.pipeline_ltx2 import LTX2Pipeline
971+
from tensorrt_llm._torch.visual_gen.models.ltx2.pipeline_ltx2_two_stages import (
972+
LTX2TwoStagesPipeline,
973+
)
974+
975+
events = []
976+
lora_deltas = {"linear.weight": torch.ones(1)}
977+
978+
class FakeFuture:
979+
def result(self):
980+
events.append("lora_result")
981+
return lora_deltas
982+
983+
class FakeExecutor:
984+
def __init__(self, max_workers):
985+
assert max_workers == 1
986+
987+
def submit(self, fn, *args):
988+
events.append("lora_submit")
989+
assert fn is pipeline_ltx2_two_stages._load_lora_deltas
990+
assert args == (
991+
"/fake/lora.safetensors",
992+
pipeline.transformer,
993+
pipeline._TRANSFORMER_PREFIX,
994+
)
995+
return FakeFuture()
996+
997+
def shutdown(self, wait=True):
998+
events.append(f"shutdown:{wait}")
999+
1000+
def fake_base_load(self, checkpoint_dir, device, skip_components=None, **kwargs):
1001+
events.append("base_load")
1002+
assert checkpoint_dir == "/fake/checkpoint"
1003+
assert device == torch.device("cpu")
1004+
1005+
monkeypatch.setattr(pipeline_ltx2_two_stages, "ThreadPoolExecutor", FakeExecutor)
1006+
monkeypatch.setattr(
1007+
pipeline_ltx2_two_stages,
1008+
"_get_free_gpu_memory_gib",
1009+
lambda device=None: None,
1010+
)
1011+
monkeypatch.setattr(LTX2Pipeline, "load_standard_components", fake_base_load)
1012+
1013+
class FakeTransformer:
1014+
def named_modules(self):
1015+
return []
1016+
1017+
def named_parameters(self):
1018+
return []
1019+
1020+
pipeline = LTX2TwoStagesPipeline.__new__(LTX2TwoStagesPipeline)
1021+
pipeline.pipeline_config = SimpleNamespace(
1022+
torch_dtype=torch.float32,
1023+
extra_attrs={
1024+
"distilled_lora_path": "/fake/lora.safetensors",
1025+
},
1026+
)
1027+
pipeline.model_config = pipeline.pipeline_config
1028+
pipeline.transformer = FakeTransformer()
1029+
1030+
pipeline.load_standard_components("/fake/checkpoint", torch.device("cpu"))
1031+
1032+
assert events == ["lora_submit", "base_load", "lora_result", "shutdown:True"]
1033+
assert pipeline._distilled_lora_deltas is lora_deltas
1034+
1035+
9631036
class TestTwoStageUpsamplerBuildingBlocks:
9641037
"""Test upsampler components without checkpoints (random weights)."""
9651038

0 commit comments

Comments
 (0)