[diffusion] feat: add memory-aware component load order (#25457)

This commit is contained in:
Mick
2026-05-17 13:22:55 +08:00
committed by GitHub
parent 6dcacb1159
commit c1d9e37a52
6 changed files with 404 additions and 13 deletions
@@ -0,0 +1,174 @@
"""Memory-aware ordering for pipeline component weight loads to avoid OOM while loading.
Load the VRAM-intensive components earlier than others
The pipeline owns component selection, path resolution, and actual loading; this
module only ranks already-selected load specs.
"""
import glob
import json
import os
from dataclasses import dataclass
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload_components import (
is_dit_component_name,
is_image_encoder_component_name,
is_text_encoder_component_name,
is_vae_component_name,
)
@dataclass(frozen=True)
class ComponentLoadSpec:
"""One pipeline component that still needs a real weight load."""
module_name: str
load_module_name: str
component_model_path: str
transformers_or_diffusers: str
architecture: str | None
index: int
_WEIGHT_FILE_SUFFIXES = (".bin", ".pt", ".pth")
def _component_base_name(component_name: str) -> str:
prefix, separator, suffix = component_name.rpartition("_")
if separator and suffix.isdigit():
return prefix
return component_name
def _component_variant_priority(component_name: str) -> int:
_, separator, suffix = component_name.rpartition("_")
if separator and suffix.isdigit():
return -int(suffix)
return 0
def component_load_risk_rank(component_name: str) -> int:
"""Fallback type rank when checkpoint size cannot be inferred."""
candidate_names = (component_name, _component_base_name(component_name))
if any(is_dit_component_name(name) for name in candidate_names):
return 0
if any(is_text_encoder_component_name(name) for name in candidate_names):
return 1
if any(is_image_encoder_component_name(name) for name in candidate_names):
return 2
if any(is_vae_component_name(name) for name in candidate_names):
return 3
return 10
def _safe_file_size(file_path: str) -> int | None:
try:
return os.path.getsize(file_path)
except OSError:
return None
def _safetensors_payload_size_bytes(file_path: str) -> int | None:
try:
with open(file_path, "rb") as f:
header_size_bytes = f.read(8)
if len(header_size_bytes) != 8:
return _safe_file_size(file_path)
header_size = int.from_bytes(header_size_bytes, "little")
header = json.loads(f.read(header_size))
except (OSError, json.JSONDecodeError, ValueError):
return _safe_file_size(file_path)
payload_size = 0
for tensor_name, tensor_info in header.items():
if tensor_name == "__metadata__":
continue
offsets = tensor_info.get("data_offsets")
if not isinstance(offsets, list) or len(offsets) != 2:
return _safe_file_size(file_path)
payload_size += offsets[1] - offsets[0]
return payload_size
def _safetensors_files_from_index(component_model_path: str) -> list[str]:
indexed_files: set[str] = set()
index_paths = sorted(
glob.glob(os.path.join(component_model_path, "*.safetensors.index.json"))
)
for index_path in index_paths:
try:
with open(index_path) as f:
weight_map = json.load(f).get("weight_map", {})
except (OSError, json.JSONDecodeError):
continue
for shard_name in weight_map.values():
shard_path = os.path.join(component_model_path, shard_name)
if os.path.isfile(shard_path):
indexed_files.add(shard_path)
return sorted(indexed_files)
def _list_component_safetensors_files(component_model_path: str) -> list[str]:
if os.path.isfile(component_model_path):
if component_model_path.endswith(".safetensors"):
return [component_model_path]
return []
if not os.path.isdir(component_model_path):
return []
indexed_files = _safetensors_files_from_index(component_model_path)
if indexed_files:
return indexed_files
return sorted(glob.glob(os.path.join(component_model_path, "*.safetensors")))
def infer_component_weight_size_bytes(component_model_path: str) -> int | None:
"""Infer checkpoint payload size from safetensors without materializing tensors."""
safetensors_files = _list_component_safetensors_files(component_model_path)
if safetensors_files:
sizes = [
size
for size in (
_safetensors_payload_size_bytes(file_path)
for file_path in safetensors_files
)
if size is not None
]
return sum(sizes) if sizes else None
if os.path.isfile(component_model_path):
if component_model_path.endswith(_WEIGHT_FILE_SUFFIXES):
return _safe_file_size(component_model_path)
return None
if not os.path.isdir(component_model_path):
return None
weight_files = []
for suffix in _WEIGHT_FILE_SUFFIXES:
weight_files.extend(glob.glob(os.path.join(component_model_path, f"*{suffix}")))
if not weight_files:
return None
sizes = [
size
for size in (_safe_file_size(file_path) for file_path in weight_files)
if size is not None
]
return sum(sizes) if sizes else None
def order_component_load_specs(
component_specs: list[ComponentLoadSpec],
) -> list[ComponentLoadSpec]:
# load larger weight payloads before small helpers to reduce startup peak OOMs
return sorted(
component_specs,
key=lambda spec: (
# 1. model size inferred from checkpoints
-(infer_component_weight_size_bytes(spec.component_model_path) or 0),
# 2. infer from component name
component_load_risk_rank(spec.load_module_name),
_component_variant_priority(spec.load_module_name),
spec.index,
),
)
@@ -12,6 +12,7 @@ from sglang.multimodal_gen.runtime.managers.memory_managers.component_resident_s
LayerwiseOffloadStrategy,
ResidentStrategy,
VanillaD2HStrategy,
is_fsdp_managed_module,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
is_layerwise_offloaded_module,
@@ -93,10 +94,6 @@ class ComponentResidencyPipeline(Protocol):
component_residency_strategies: MutableMapping[str, "ComponentResidencyStrategy"]
def is_fsdp_managed_module(module: nn.Module) -> bool:
return module.__class__.__name__.startswith("FSDP")
def should_cpu_offload_component(
component_name: str, module: nn.Module, server_args: ServerArgs
) -> bool:
@@ -57,6 +57,10 @@ def _module_ready_on_local_device(
return dtype is None or tensor.dtype == dtype
def is_fsdp_managed_module(module: nn.Module) -> bool:
return module.__class__.__name__.startswith("FSDP")
class ComponentResidencyStrategy:
"""Baseclass for describing how a component should be treated (regarding where its weights locates)
@@ -75,7 +79,6 @@ class ComponentResidencyStrategy:
use: ComponentUse,
state: ResidencyState,
) -> None:
"""hook called"""
self.enter(module)
def wait_for_use(
@@ -144,8 +147,9 @@ class ResidentStrategy(ComponentResidencyStrategy):
use: ComponentUse,
state: ResidencyState,
) -> None:
if use.target_dtype is not None:
_module_to_local_device(module, dtype=use.target_dtype)
if is_fsdp_managed_module(module):
return
_module_to_local_device(module, dtype=use.target_dtype)
class SnapshotModuleResidency:
@@ -24,6 +24,10 @@ from sglang.multimodal_gen.runtime.layers.attention.selector import (
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
PipelineComponentLoader,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.component_loading_order import (
ComponentLoadSpec,
order_component_load_specs,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.component_manager import (
ComponentResidencyManager,
ComponentResidencyStrategy,
@@ -385,10 +389,16 @@ class ComposedPipelineBase(ABC):
logger.info("Loading required components: %s", required_modules)
loaded_components = {}
for module_name, (
transformers_or_diffusers,
architecture,
) in tqdm(iterable=model_index.items(), desc="Loading required modules"):
component_load_specs: list[ComponentLoadSpec] = []
# enqueue only real weight loads (e.g., scheduler, tokenizer is excluded); skipped/provided modules keep old handling
for index, (
module_name,
(
transformers_or_diffusers,
architecture,
),
) in enumerate(model_index.items()):
if transformers_or_diffusers is None:
logger.warning(
"Module %s in model_index.json has null value, removing from required_config_modules",
@@ -405,15 +415,43 @@ class ComposedPipelineBase(ABC):
loaded_components[module_name] = loaded_modules[module_name]
continue
# we load the module from the extra config module map if it exists
if module_name in self._extra_config_module_map:
load_module_name = self._extra_config_module_map[module_name]
else:
load_module_name = module_name
component_model_path = self._resolve_component_path(
server_args, module_name, load_module_name
)
# collect loading specs
component_load_specs.append(
ComponentLoadSpec(
module_name=module_name,
load_module_name=load_module_name,
component_model_path=component_model_path,
transformers_or_diffusers=transformers_or_diffusers,
architecture=architecture,
index=index,
)
)
# reorder loading order to avoid OOM
component_load_specs: ComponentLoadSpec = order_component_load_specs(
component_load_specs
)
logger.info(
"Memory-aware component load order: %s",
[spec.module_name for spec in component_load_specs],
)
for spec in tqdm(
iterable=component_load_specs, desc="Loading required modules"
):
module_name: str = spec.module_name
load_module_name: str = spec.load_module_name
transformers_or_diffusers: str = spec.transformers_or_diffusers
architecture: str = spec.architecture
component_model_path: str = spec.component_model_path
attn_backend, matched_backend_key = (
server_args.resolve_component_attention_backend(
module_name, load_module_name
@@ -0,0 +1,130 @@
import json
from sglang.multimodal_gen.runtime.managers.memory_managers.component_loading_order import (
ComponentLoadSpec,
component_load_risk_rank,
infer_component_weight_size_bytes,
order_component_load_specs,
)
def _spec(
component_name: str, index: int, component_model_path: str = "/missing"
) -> ComponentLoadSpec:
return ComponentLoadSpec(
module_name=component_name,
load_module_name=component_name,
component_model_path=component_model_path,
transformers_or_diffusers="diffusers",
architecture=None,
index=index,
)
def _write_safetensors(path, payload_size: int) -> None:
header = {
"weight": {
"dtype": "F16",
"shape": [payload_size // 2],
"data_offsets": [0, payload_size],
}
}
header_bytes = json.dumps(header).encode("utf-8")
path.write_bytes(
len(header_bytes).to_bytes(8, "little") + header_bytes + b"\0" * payload_size
)
def test_component_load_order_prioritizes_weight_heavy_components():
specs = [
_spec("scheduler", 0),
_spec("tokenizer", 1),
_spec("text_encoder", 2),
_spec("transformer", 3),
_spec("vae", 4),
]
ordered_names = [spec.module_name for spec in order_component_load_specs(specs)]
assert ordered_names == [
"transformer",
"text_encoder",
"vae",
"scheduler",
"tokenizer",
]
def test_component_load_order_prioritizes_larger_numbered_variants():
specs = [
_spec("transformer", 0),
_spec("transformer_2", 1),
_spec("text_encoder", 2),
_spec("text_encoder_3", 3),
_spec("text_encoder_2", 4),
]
ordered_names = [spec.module_name for spec in order_component_load_specs(specs)]
assert ordered_names == [
"transformer_2",
"transformer",
"text_encoder_3",
"text_encoder_2",
"text_encoder",
]
def test_component_load_order_uses_load_module_name_for_extra_config_alias():
specs = [
ComponentLoadSpec(
module_name="condition_image_encoder",
load_module_name="condition_image_encoder",
component_model_path="/missing",
transformers_or_diffusers="diffusers",
architecture=None,
index=0,
),
ComponentLoadSpec(
module_name="encoder_alias",
load_module_name="text_encoder_2",
component_model_path="/missing",
transformers_or_diffusers="transformers",
architecture=None,
index=1,
),
]
ordered_names = [spec.module_name for spec in order_component_load_specs(specs)]
assert ordered_names == ["encoder_alias", "condition_image_encoder"]
def test_component_load_risk_rank_keeps_small_helpers_last():
assert component_load_risk_rank("transformer") < component_load_risk_rank(
"scheduler"
)
assert component_load_risk_rank("text_encoder_2") < component_load_risk_rank(
"processor"
)
assert component_load_risk_rank("vae") < component_load_risk_rank("tokenizer")
def test_component_load_order_prefers_inferred_safetensors_size(tmp_path):
small_transformer_path = tmp_path / "small_transformer"
large_encoder_path = tmp_path / "large_encoder"
small_transformer_path.mkdir()
large_encoder_path.mkdir()
_write_safetensors(small_transformer_path / "model.safetensors", 16)
_write_safetensors(large_encoder_path / "model.safetensors", 64)
specs = [
_spec("transformer", 0, str(small_transformer_path)),
_spec("text_encoder", 1, str(large_encoder_path)),
_spec("scheduler", 2),
]
ordered_names = [spec.module_name for spec in order_component_load_specs(specs)]
assert ordered_names == ["text_encoder", "transformer", "scheduler"]
assert infer_component_weight_size_bytes(str(large_encoder_path)) == 64
@@ -9,10 +9,14 @@ from sglang.multimodal_gen.runtime.layers.quantization.modelopt_quant import (
from sglang.multimodal_gen.runtime.loader.transformer_load_utils import (
_ModelOptFp8OffloadAdapter,
)
from sglang.multimodal_gen.runtime.managers.memory_managers import (
component_resident_strategies as component_resident_strategies_mod,
)
from sglang.multimodal_gen.runtime.managers.memory_managers import (
layerwise_offload as layerwise_offload_mod,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.component_manager import (
ComponentUse,
build_component_residency_strategy,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.component_resident_strategies import (
@@ -346,6 +350,50 @@ def test_component_cpu_offload_strategy_remains_flag_driven():
assert isinstance(strategy, ResidentStrategy)
def test_resident_strategy_prepares_local_device_without_dtype(monkeypatch):
calls = []
def fake_module_to_local_device(module, *, dtype=None):
calls.append((module, dtype))
monkeypatch.setattr(
component_resident_strategies_mod,
"_module_to_local_device",
fake_module_to_local_device,
)
module = _DummyModel()
ResidentStrategy().prepare_for_use(
module,
ComponentUse(stage_name="DenoisingStage", component_name="transformer"),
SimpleNamespace(),
)
assert calls == [(module, None)]
def test_resident_strategy_keeps_fsdp_managed_module_owned_by_fsdp(monkeypatch):
calls = []
def fake_module_to_local_device(module, *, dtype=None):
calls.append((module, dtype))
monkeypatch.setattr(
component_resident_strategies_mod,
"_module_to_local_device",
fake_module_to_local_device,
)
module = type("FSDPDummyModel", (_DummyModel,), {})()
ResidentStrategy().prepare_for_use(
module,
ComponentUse(stage_name="TextEncodingStage", component_name="text_encoder"),
SimpleNamespace(),
)
assert calls == []
def test_layerwise_offload_aligns_contiguous_tensor_offsets(monkeypatch):
monkeypatch.setattr(
layerwise_offload_mod.torch, "get_device_module", lambda: _FakeDeviceModule