Skip to content

Commit c9c5585

Browse files
[perf]: MiniMax H3 on GB10 - skip text encoder CPU offload on unified memory (5m49s to 30ms) (#1710)
1 parent 9212f4f commit c9c5585

13 files changed

Lines changed: 449 additions & 10 deletions

File tree

.buildkite/scripts/unit_test.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ exec pytest \
77
./fastvideo/tests/dataset/ \
88
./fastvideo/tests/workflow/ \
99
./fastvideo/tests/entrypoints/ \
10+
./fastvideo/tests/loader/ \
1011
./fastvideo/tests/train/ \
1112
./fastvideo/tests/stages/ \
1213
./fastvideo/tests/ops/ \

fastvideo/fastvideo_args.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525

2626
logger = init_logger(__name__)
2727

28+
# Offload flags that trade device memory for host memory. Keeping the policy
29+
# centralized lets components share the same worker-local device decision.
30+
UNIFIED_MEMORY_OFFLOAD_FLAGS = ("text_encoder_cpu_offload", )
31+
2832

2933
class ExecutionMode(str, Enum):
3034
"""
@@ -914,6 +918,39 @@ def check_fastvideo_args(self) -> None:
914918
self.pipeline_config.vae_config.load_encoder = True
915919
self.preprocess_config.check_preprocess_config()
916920

921+
def disable_offload_on_unified_memory(self, device_id: int = 0, *, offload_flag: str | None = None) -> bool:
922+
"""Disable host offload after a worker has selected its device.
923+
924+
CUDA's unified-memory probe reads runtime device properties and may
925+
initialize a CUDA context. Callers must therefore use this only inside
926+
a device-owning process, after selecting and binding ``device_id``.
927+
Returning the classification lets direct component-loader callers
928+
apply the same policy to explicit per-call overrides. When
929+
``offload_flag`` is given, the return value says whether this policy
930+
covers that component role.
931+
"""
932+
from fastvideo.platforms import current_platform
933+
934+
if not current_platform.has_unified_memory(device_id):
935+
return False
936+
937+
try:
938+
device_name = current_platform.get_device_name(device_id)
939+
except Exception:
940+
# Device naming is diagnostic only. NVML can be unavailable on an
941+
# integrated GPU (for example Jetson), and its physical-ordinal
942+
# lookup cannot interpret CUDA_VISIBLE_DEVICES UUID/MIG selectors.
943+
# Neither case should undo an authoritative driver classification.
944+
device_name = current_platform.device_name
945+
946+
for flag in UNIFIED_MEMORY_OFFLOAD_FLAGS:
947+
if getattr(self, flag):
948+
logger.info(
949+
"Disabling %s: %s has unified memory, so moving weights to the host duplicates "
950+
"them rather than freeing device memory.", flag, device_name)
951+
setattr(self, flag, False)
952+
return offload_flag is None or offload_flag in UNIFIED_MEMORY_OFFLOAD_FLAGS
953+
917954

918955
_current_fastvideo_args = None
919956

fastvideo/models/loader/component_loader.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,11 +347,24 @@ def load_model(
347347
dtype: str = "fp16",
348348
use_text_encoder_override: bool = False, # prevent subclasses from misusing
349349
cpu_offload: bool | None = None,
350+
offload_flag: str = "text_encoder_cpu_offload",
350351
):
351-
if cpu_offload is None:
352-
cpu_offload = fastvideo_args.text_encoder_cpu_offload
353-
use_cpu_offload = (cpu_offload and len(getattr(model_config, "_fsdp_shard_conditions", [])) > 0)
354352
runtime_device = get_local_torch_device()
353+
device_id = runtime_device.index if runtime_device.index is not None else 0
354+
requested_cpu_offload = getattr(fastvideo_args, offload_flag) if cpu_offload is None else cpu_offload
355+
disable_cpu_offload = fastvideo_args.disable_offload_on_unified_memory(device_id,
356+
offload_flag=offload_flag)
357+
358+
if requested_cpu_offload and disable_cpu_offload:
359+
# Direct loader callers can choose a CPU target before the worker
360+
# applies its device-local policy. Reset both the request and the
361+
# target so the model is never constructed on the host first.
362+
logger.info("Disabling %s on unified-memory device %d", offload_flag, device_id)
363+
cpu_offload = False
364+
target_device = runtime_device
365+
else:
366+
cpu_offload = requested_cpu_offload
367+
use_cpu_offload = (cpu_offload and len(getattr(model_config, "_fsdp_shard_conditions", [])) > 0)
355368

356369
from fastvideo.platforms import current_platform
357370

@@ -555,6 +568,7 @@ def load(self, model_path: str, fastvideo_args: FastVideoArgs):
555568
fastvideo_args,
556569
encoder_precision,
557570
cpu_offload=fastvideo_args.image_encoder_cpu_offload,
571+
offload_flag="image_encoder_cpu_offload",
558572
)
559573

560574

fastvideo/platforms/cuda.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
pynvml. However, it should not initialize cuda context.
55
"""
66

7+
import ctypes
78
import os
89
from collections.abc import Callable
910
from functools import lru_cache, wraps
@@ -24,6 +25,56 @@
2425

2526
pynvml = import_pynvml() # type: ignore[no-untyped-call]
2627

28+
_CUDA_SUCCESS = 0
29+
# Stable value from CUDA's public CUdevice_attribute enum.
30+
_CU_DEVICE_ATTRIBUTE_INTEGRATED = 18
31+
_CUDA_DRIVER_LIBRARY = "nvcuda.dll" if os.name == "nt" else "libcuda.so.1"
32+
33+
34+
def _cuda_driver_device_is_integrated(device_id: int) -> bool:
35+
"""Query ``CU_DEVICE_ATTRIBUTE_INTEGRATED`` without creating a context.
36+
37+
``cuInit`` loads the CUDA driver, while these device-management calls only
38+
inspect the logical device ordinal. They neither create nor retain a CUDA
39+
context. Driver initialization is not pre-fork safe, so this probe must
40+
remain worker-local after process creation and device binding. Using the
41+
driver ordinal preserves CUDA_VISIBLE_DEVICES ordering, including UUID and
42+
MIG selectors.
43+
"""
44+
try:
45+
driver = ctypes.CDLL(_CUDA_DRIVER_LIBRARY)
46+
driver.cuInit.argtypes = [ctypes.c_uint]
47+
driver.cuInit.restype = ctypes.c_int
48+
driver.cuDeviceGet.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
49+
driver.cuDeviceGet.restype = ctypes.c_int
50+
driver.cuDeviceGetAttribute.argtypes = [
51+
ctypes.POINTER(ctypes.c_int),
52+
ctypes.c_int,
53+
ctypes.c_int,
54+
]
55+
driver.cuDeviceGetAttribute.restype = ctypes.c_int
56+
57+
if driver.cuInit(0) != _CUDA_SUCCESS:
58+
return False
59+
60+
device = ctypes.c_int()
61+
if driver.cuDeviceGet(ctypes.byref(device), device_id) != _CUDA_SUCCESS:
62+
return False
63+
64+
is_integrated = ctypes.c_int()
65+
if driver.cuDeviceGetAttribute(
66+
ctypes.byref(is_integrated),
67+
_CU_DEVICE_ATTRIBUTE_INTEGRATED,
68+
device,
69+
) != _CUDA_SUCCESS:
70+
return False
71+
return bool(is_integrated.value)
72+
except Exception:
73+
# Missing/incompatible driver libraries and unavailable devices must
74+
# preserve the established discrete-memory offload policy.
75+
return False
76+
77+
2778
# pytorch 2.5 uses cudnn sdpa by default, which will cause crash on some models
2879
# see https://github.com/huggingface/diffusers/issues/9704 for details
2980
torch.backends.cuda.enable_cudnn_sdp(False)
@@ -79,6 +130,12 @@ def get_device_name(cls, device_id: int = 0) -> str:
79130
def get_device_total_memory(cls, device_id: int = 0) -> int:
80131
raise NotImplementedError
81132

133+
@classmethod
134+
def has_unified_memory(cls, device_id: int = 0) -> bool:
135+
# This is cudaDeviceProp::integrated's driver-level source of truth. It
136+
# is true on parts such as GB10 and Jetson whose GPU reads host memory.
137+
return _cuda_driver_device_is_integrated(device_id)
138+
82139
@classmethod
83140
def is_async_output_supported(cls, enforce_eager: bool | None) -> bool:
84141
if enforce_eager:

fastvideo/platforms/interface.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ def is_mps(self) -> bool:
118118
def is_npu(self) -> bool:
119119
return self._enum == PlatformEnum.NPU
120120

121+
@classmethod
122+
def has_unified_memory(cls, device_id: int = 0) -> bool:
123+
"""Whether host and device allocations come out of one physical pool.
124+
125+
Where this is true, moving a tensor between host and device frees
126+
nothing: both ends are the same RAM. Anything that offloads to save
127+
memory needs to know, because on such a device the copy is at best a
128+
no-op and at worst holds two copies at once.
129+
130+
Implementations may need runtime device properties. Call this only
131+
after the current worker has selected and initialized ``device_id``.
132+
"""
133+
return False
134+
121135
@classmethod
122136
def get_attn_backend_cls(cls, selected_backend: AttentionBackendEnum | None, head_size: int,
123137
dtype: torch.dtype) -> str:

fastvideo/platforms/mps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ class MpsPlatform(Platform):
1616
dispatch_key: str = "MPS"
1717
device_control_env_var: str = "MPS_VISIBLE_DEVICES"
1818

19+
@classmethod
20+
def has_unified_memory(cls, device_id: int = 0) -> bool:
21+
# Apple silicon shares one pool between CPU and GPU.
22+
return True
23+
1924
@classmethod
2025
def get_device_capability(cls, device_id: int = 0) -> DeviceCapability | None:
2126
raise NotImplementedError
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Regression tests for text-encoder placement on unified memory."""
3+
from __future__ import annotations
4+
5+
from types import SimpleNamespace
6+
from unittest.mock import Mock
7+
8+
import pytest
9+
import torch
10+
import torch.nn as nn
11+
12+
from fastvideo.fastvideo_args import FastVideoArgs
13+
from fastvideo.models.loader.component_loader import ImageEncoderLoader, TextEncoderLoader
14+
15+
16+
class _PassthroughEncoder(nn.Module):
17+
supports_hf_from_pretrained = True
18+
loaded_device: torch.device | None = None
19+
20+
@classmethod
21+
def from_pretrained_local(cls, model_path, model_config, *, dtype, device):
22+
del model_path, model_config, dtype
23+
cls.loaded_device = torch.device(device)
24+
return cls()
25+
26+
27+
def _model_config():
28+
return SimpleNamespace(architectures=["PassthroughEncoder"], _fsdp_shard_conditions=[], quant_config=None)
29+
30+
31+
@pytest.mark.parametrize(
32+
("cpu_offload", "requested_target"),
33+
[
34+
(None, torch.device("cuda:5")),
35+
(None, torch.device("cpu")),
36+
(True, torch.device("cpu")),
37+
],
38+
)
39+
def test_unified_memory_uses_worker_device_before_model_construction(monkeypatch, tmp_path, cpu_offload,
40+
requested_target) -> None:
41+
probe = Mock(return_value=True)
42+
monkeypatch.setattr("fastvideo.models.loader.component_loader.get_local_torch_device",
43+
lambda: torch.device("cuda:5"))
44+
monkeypatch.setattr("fastvideo.platforms.current_platform.has_unified_memory", probe)
45+
monkeypatch.setattr("fastvideo.platforms.current_platform.get_device_name", lambda device_id: "NVIDIA GB10")
46+
monkeypatch.setattr(
47+
"fastvideo.models.loader.component_loader.ModelRegistry.resolve_model_cls",
48+
lambda architectures: (_PassthroughEncoder, None),
49+
)
50+
args = FastVideoArgs(model_path=str(tmp_path), text_encoder_cpu_offload=True)
51+
52+
model = TextEncoderLoader().load_model(
53+
str(tmp_path),
54+
_model_config(),
55+
requested_target,
56+
args,
57+
cpu_offload=cpu_offload,
58+
)
59+
60+
assert isinstance(model, _PassthroughEncoder)
61+
assert _PassthroughEncoder.loaded_device == torch.device("cuda:5")
62+
assert args.text_encoder_cpu_offload is False
63+
probe.assert_called_once_with(5)
64+
65+
66+
def test_discrete_memory_preserves_explicit_cpu_target(monkeypatch, tmp_path) -> None:
67+
monkeypatch.setattr("fastvideo.models.loader.component_loader.get_local_torch_device",
68+
lambda: torch.device("cuda:2"))
69+
monkeypatch.setattr("fastvideo.platforms.current_platform.has_unified_memory", lambda device_id: False)
70+
monkeypatch.setattr(
71+
"fastvideo.models.loader.component_loader.ModelRegistry.resolve_model_cls",
72+
lambda architectures: (_PassthroughEncoder, None),
73+
)
74+
args = FastVideoArgs(model_path=str(tmp_path), text_encoder_cpu_offload=False)
75+
76+
TextEncoderLoader().load_model(
77+
str(tmp_path),
78+
_model_config(),
79+
torch.device("cuda:2"),
80+
args,
81+
cpu_offload=True,
82+
)
83+
84+
assert _PassthroughEncoder.loaded_device == torch.device("cpu")
85+
86+
87+
def test_text_policy_does_not_change_inherited_image_encoder_path(monkeypatch, tmp_path) -> None:
88+
probe = Mock(return_value=True)
89+
monkeypatch.setattr("fastvideo.models.loader.component_loader.get_local_torch_device",
90+
lambda: torch.device("cuda:4"))
91+
monkeypatch.setattr("fastvideo.platforms.current_platform.has_unified_memory", probe)
92+
monkeypatch.setattr("fastvideo.platforms.current_platform.get_device_name", lambda device_id: "NVIDIA GB10")
93+
monkeypatch.setattr(
94+
"fastvideo.models.loader.component_loader.ModelRegistry.resolve_model_cls",
95+
lambda architectures: (_PassthroughEncoder, None),
96+
)
97+
args = FastVideoArgs(
98+
model_path=str(tmp_path),
99+
text_encoder_cpu_offload=True,
100+
image_encoder_cpu_offload=True,
101+
)
102+
103+
ImageEncoderLoader().load_model(
104+
str(tmp_path),
105+
_model_config(),
106+
torch.device("cpu"),
107+
args,
108+
cpu_offload=True,
109+
offload_flag="image_encoder_cpu_offload",
110+
)
111+
112+
assert _PassthroughEncoder.loaded_device == torch.device("cpu")
113+
assert args.text_encoder_cpu_offload is False
114+
assert args.image_encoder_cpu_offload is True
115+
probe.assert_called_once_with(4)

0 commit comments

Comments
 (0)