Skip to content

Commit 5db9414

Browse files
[https://nvbugs/6179761][fix] Save LTX-2 BF16 weights to speed up perf (#14639)
Signed-off-by: yibinl-nvidia <109242046+yibinl-nvidia@users.noreply.github.com>
1 parent 6222112 commit 5db9414

2 files changed

Lines changed: 122 additions & 22 deletions

File tree

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

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,46 @@
3939

4040
STAGE_2_DISTILLED_SIGMA_VALUES = [0.909375, 0.725, 0.421875, 0.0]
4141
_FP8_DTYPES = (torch.float8_e4m3fn, torch.float8_e5m2)
42+
# Baseline BF16 peak memory ~75 GiB, saving BF16 weights snopshot total ~108 GiB.
43+
_BF16_WEIGHTS_SNAPSHOT_FREE_MEMORY_THRESHOLD_GIB = 115.0
4244

4345

4446
# ---------------------------------------------------------------------------
4547
# LoRA helpers
4648
# ---------------------------------------------------------------------------
4749

4850

51+
def _get_free_gpu_memory_gib() -> Optional[float]:
52+
"""Return free memory on the current CUDA device, or ``None`` if unavailable."""
53+
if not torch.cuda.is_available():
54+
return None
55+
56+
try:
57+
free_bytes, _ = torch.cuda.mem_get_info()
58+
except (RuntimeError, OSError) as exc:
59+
logger.warning(f"Unable to query CUDA free memory for BF16 weight snapshots: {exc}")
60+
return None
61+
62+
return free_bytes / (1024**3)
63+
64+
65+
def _should_save_bf16_weights(
66+
threshold_gib: float = _BF16_WEIGHTS_SNAPSHOT_FREE_MEMORY_THRESHOLD_GIB,
67+
) -> bool:
68+
free_gib = _get_free_gpu_memory_gib()
69+
if free_gib is None:
70+
logger.debug("BF16 weight snapshots disabled: CUDA free memory is unavailable")
71+
return False
72+
73+
save_state = free_gib > threshold_gib
74+
relation = ">" if save_state else "<="
75+
logger.debug(
76+
f"BF16 weight snapshots {'enabled' if save_state else 'disabled'}: "
77+
f"free GPU memory {free_gib:.2f} GiB {relation} {threshold_gib:.2f} GiB threshold"
78+
)
79+
return save_state
80+
81+
4982
def _load_lora_deltas(
5083
lora_path: str,
5184
transformer: torch.nn.Module,
@@ -338,11 +371,12 @@ def _apply_lora_deltas(
338371
module: torch.nn.Module,
339372
deltas: Dict[str, torch.Tensor],
340373
sign: float = 1.0,
374+
save_bf16_weights: bool = False,
341375
) -> tuple:
342376
"""Add (sign=+1) or remove (sign=-1) pre-computed LoRA deltas.
343377
344-
For standard (BF16/FP16) weights the delta is added directly and
345-
later removed by subtracting the same delta.
378+
For BF16 weights the delta is added directly and later removed either
379+
by restoring an optional saved snapshot or by subtracting the same delta.
346380
For FP8-quantized weights (same shape, float8 dtype), we
347381
dequantize → apply → requantize. FP4 weights are handled through
348382
the packed-FP4 branch because the current static and dynamic NVFP4
@@ -355,13 +389,15 @@ def _apply_lora_deltas(
355389
state afterwards.
356390
357391
Returns ``(applied_count, saved_lora_state, snapshot_required_count)`` where
358-
*saved_lora_state* maps each touched quantized parameter name to its
359-
original tensor. For packed FP4 it also stores the parent
392+
*saved_lora_state* maps each touched snapshotted parameter name to its
393+
original tensor. BF16 weights are snapshotted only when
394+
*save_bf16_weights* is true. This does not change FP8 or FP4 LoRA
395+
handling: those paths always snapshot quantized state. For packed FP4 it
396+
also stores the parent
360397
``quant_method`` so that stage 2 can run with BF16 weights and then
361398
restore the exact original FP4 state without another quantization round
362-
trip. Dense BF16/FP16/FP32 weights are not stored in
363-
*saved_lora_state*. *snapshot_required_count* is the number of
364-
quantized weights that must be restored from saved snapshots.
399+
trip. *snapshot_required_count* is the number of weights that must be
400+
restored from saved snapshots.
365401
"""
366402
applied = 0
367403
snapshot_required = 0
@@ -424,8 +460,11 @@ def _apply_lora_deltas(
424460
ws_param.data.copy_(new_scale)
425461
snapshot_required += 1
426462
else:
427-
# BF16/FP16/FP32: direct in-place addition. These are
428-
# restored by subtracting the same delta after stage 2.
463+
if save_bf16_weights and param.dtype == torch.bfloat16:
464+
saved_state[param_name] = param.data.clone()
465+
snapshot_required += 1
466+
# BF16: direct in-place addition, then restore by snapshot copy
467+
# when memory allows. Other dense weights restore by subtraction.
429468
param.data.add_(
430469
delta.to(param.device, param.dtype),
431470
alpha=sign,
@@ -808,11 +847,16 @@ def forward(
808847
# ================================================================
809848
# For FP4 models (static-packed or dynamic), stage 2 always runs in
810849
# BF16: the quant_method is swapped to UnquantizedLinearMethod inside
811-
# _apply_lora_deltas and restored afterwards. BF16 models are unaffected.
850+
# _apply_lora_deltas and restored afterwards. FP8 handling is also
851+
# unchanged and always restores from saved quantized state. BF16 weights
852+
# save snapshots only when enough free GPU memory is available;
853+
# otherwise they fall back to on-the-fly LoRA subtraction.
854+
save_bf16_weights = _should_save_bf16_weights()
812855
n, saved_lora_state, snapshot_required = _apply_lora_deltas(
813856
self.transformer,
814857
self._distilled_lora_deltas,
815858
sign=1.0,
859+
save_bf16_weights=save_bf16_weights,
816860
)
817861
logger.info(f"Merged distilled LoRA ({n} params) for stage 2 (BF16 weights)")
818862

@@ -840,8 +884,7 @@ def forward(
840884
self.transformer.set_ulysses_enabled(True)
841885
if snapshot_required and not saved_lora_state:
842886
raise RuntimeError(
843-
"Quantized LoRA state was not saved; cannot safely restore "
844-
"FP8/FP4 stage 2 weights."
887+
"LoRA state was not saved; cannot safely restore stage 2 weights."
845888
)
846889

847890
snapshot_restored = 0
@@ -851,8 +894,8 @@ def forward(
851894
_restore_lora_state(self.transformer, saved_lora_state)
852895
snapshot_restored = _count_saved_lora_weight_tensors(saved_lora_state)
853896

854-
# Dense BF16/FP16/FP32 weights are not snapshotted; restore them by
855-
# subtracting the LoRA deltas directly.
897+
# BF16 weights that were not snapshotted, plus any other dense
898+
# floating-point weights, are restored by subtracting LoRA deltas.
856899
dense_restored = _subtract_dense_lora_deltas(
857900
self.transformer,
858901
self._distilled_lora_deltas,

tests/unittest/_torch/visual_gen/test_ltx2_pipeline.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,58 @@ def test_latent_shape_matches_batch(self):
537537
class TestTwoStageLoRAHelpers:
538538
"""Test LoRA delta loading and application without checkpoints."""
539539

540+
def test_bf16_weight_snapshot_gate_uses_cuda_free_memory(self, monkeypatch):
541+
from tensorrt_llm._torch.visual_gen.models.ltx2.pipeline_ltx2_two_stages import (
542+
_should_save_bf16_weights,
543+
)
544+
545+
gib = 1024**3
546+
monkeypatch.setattr(torch.cuda, "is_available", lambda: True)
547+
monkeypatch.setattr(torch.cuda, "mem_get_info", lambda: (116 * gib, 180 * gib))
548+
assert _should_save_bf16_weights()
549+
550+
monkeypatch.setattr(torch.cuda, "mem_get_info", lambda: (115 * gib, 180 * gib))
551+
assert not _should_save_bf16_weights()
552+
553+
def _raise_mem_query_error():
554+
raise RuntimeError("mem_get_info failed")
555+
556+
monkeypatch.setattr(torch.cuda, "mem_get_info", _raise_mem_query_error)
557+
assert not _should_save_bf16_weights()
558+
559+
monkeypatch.setattr(torch.cuda, "is_available", lambda: False)
560+
assert not _should_save_bf16_weights()
561+
562+
def test_bf16_weight_snapshot_saved_when_requested(self):
563+
from tensorrt_llm._torch.visual_gen.models.ltx2.pipeline_ltx2_two_stages import (
564+
_apply_lora_deltas,
565+
_restore_lora_state,
566+
_subtract_dense_lora_deltas,
567+
)
568+
569+
linear = torch.nn.Linear(32, 32, bias=False).bfloat16()
570+
original_weight = linear.weight.data.clone()
571+
deltas = {"weight": torch.randn(32, 32) * 0.1}
572+
573+
applied, saved_state, snapshot_required = _apply_lora_deltas(
574+
linear,
575+
deltas,
576+
sign=1.0,
577+
save_bf16_weights=True,
578+
)
579+
580+
assert applied == 1
581+
assert snapshot_required == 1
582+
assert "weight" in saved_state
583+
assert torch.allclose(saved_state["weight"], original_weight)
584+
assert not torch.allclose(linear.weight.data, original_weight)
585+
586+
removed = _subtract_dense_lora_deltas(linear, deltas, saved_state)
587+
assert removed == 0
588+
589+
_restore_lora_state(linear, saved_state)
590+
assert torch.allclose(linear.weight.data, original_weight)
591+
540592
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
541593
def test_apply_and_remove_deltas_bf16(self):
542594
"""Merge then unmerge in BF16 should leave weights approximately unchanged."""
@@ -554,7 +606,7 @@ def test_apply_and_remove_deltas_bf16(self):
554606

555607
applied, saved_state, snapshot_required = _apply_lora_deltas(linear, deltas, sign=1.0)
556608
assert applied == 1, "Expected one parameter to be modified"
557-
assert saved_state == {}, "Dense BF16 weights should not be snapshotted"
609+
assert saved_state == {}, "BF16 weights should not be snapshotted by default"
558610
assert snapshot_required == 0
559611
assert not torch.allclose(linear.weight.data, original_weight), (
560612
"Weights should have changed after applying delta"
@@ -597,16 +649,16 @@ def test_multi_round_drift_bounded(self):
597649
rounds = 10
598650
for _ in range(rounds):
599651
_, saved_state, snapshot_required = _apply_lora_deltas(model, deltas, sign=1.0)
600-
assert saved_state == {}, "Dense BF16 weights should not be snapshotted"
652+
assert saved_state == {}, "BF16 weights should not be snapshotted by default"
601653
assert snapshot_required == 0
602654
_subtract_dense_lora_deltas(model, deltas, saved_state)
603655

604656
drift = (model.weight.data.float() - original.float()).abs().max().item()
605657
assert drift < 0.1, f"bf16 drift after {rounds} rounds too large: {drift:.2e}"
606658

607659
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
608-
def test_dense_lora_state_not_saved_and_subtract_restores(self):
609-
"""Dense weights are restored by subtraction without snapshot storage."""
660+
def test_fp32_state_not_saved_and_subtract_restores(self):
661+
"""FP32 weights restore by subtraction, even when BF16 snapshots are enabled."""
610662
from tensorrt_llm._torch.visual_gen.models.ltx2.pipeline_ltx2_two_stages import (
611663
_apply_lora_deltas,
612664
_subtract_dense_lora_deltas,
@@ -617,16 +669,21 @@ def test_dense_lora_state_not_saved_and_subtract_restores(self):
617669
original_weight = linear.weight.data.clone()
618670

619671
deltas = {"weight": torch.randn(32, 32, device=device) * 0.1}
620-
_, saved_state, snapshot_required = _apply_lora_deltas(linear, deltas, sign=1.0)
672+
_, saved_state, snapshot_required = _apply_lora_deltas(
673+
linear,
674+
deltas,
675+
sign=1.0,
676+
save_bf16_weights=True,
677+
)
621678

622-
assert saved_state == {}, "Dense FP32 weights should not be snapshotted"
679+
assert saved_state == {}, "FP32 weights should not use the BF16 snapshot path"
623680
assert snapshot_required == 0
624681
assert not torch.allclose(linear.weight.data, original_weight)
625682

626683
removed = _subtract_dense_lora_deltas(linear, deltas, saved_state)
627684
assert removed == 1
628685
assert torch.allclose(linear.weight.data, original_weight), (
629-
"Dense LoRA subtraction should restore weights"
686+
"FP32 LoRA subtraction should restore weights"
630687
)
631688

632689

@@ -1111,7 +1168,7 @@ def test_two_stage_lora_deltas_match_transformer(self, ltx2_two_stage_assets_exi
11111168
print(f"\n[Two-Stage] LoRA apply rate: {match_rate:.1f}% ({applied}/{total})")
11121169
assert match_rate > 99.0, f"Expected >99% LoRA match rate, got {match_rate:.1f}%"
11131170

1114-
assert saved_state == {}, "BF16 checkpoint should not snapshot dense weights"
1171+
assert saved_state == {}, "BF16 checkpoint should not snapshot weights by default"
11151172
assert snapshot_required == 0
11161173

11171174
# Verify dense unmerge by subtraction

0 commit comments

Comments
 (0)