Compare commits

...
Author SHA1 Message Date
Hongsheng Liu 1c3d59796f [Diffusion] Document DLO mmap model opt-in
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-15 06:57:38 +08:00
Hongsheng Liu 76e257b0f3 [Diffusion] Address rank-local mmap review feedback
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-14 17:12:05 +08:00
Hongsheng Liu 2956af1e9d [Diffusion] Add rank-local mmap storage for DLO
Decouple node-local checkpoint storage from the DLO AllGather transfer protocol. Compatible rank-local workers retain shared file-backed sources and stream complete blocks through bounded staging without changing request scheduling.

Refs #6195

Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-14 14:38:06 +08:00
11 changed files with 989 additions and 221 deletions

No files matched your search

@@ -5,6 +5,8 @@ models. DLO keeps only a small number of DiT blocks on the accelerator and
streams the remaining blocks from host memory. The distributed backend can
either shard those host-side weights across an existing parallel group or keep
the standard loader's rank-local weights and avoid an additional collective.
The rank-local mmap storage design is tracked in
[RFC #6195](https://github.com/vllm-project/vllm-omni/issues/6195).
For user-facing commands, see the
[distributed layerwise offloading guide](../../user_guide/diffusion/cpu_offload.md)
@@ -16,8 +18,10 @@ DLO is implemented for multi-device diffusion execution. The default
AllGather path is the primary path for DP and SP deployments. The
`--dlo-no-use-allgather` path is a rank-local compatibility mode: it is useful
for standard-loader sharding, workstation bring-up, and systems where an
additional DLO collective is undesirable, but it does not reduce host weight
storage across ranks.
additional DLO collective is undesirable. For explicitly mmap-compatible
models in a TP1, non-HSDP topology, this path shares file-backed DiT checkpoint
pages across processes and keeps only bounded staging buffers private. TP,
HSDP, and other models retain standard-loader rank-local host storage.
The compatibility matrix below describes the current implementation. The
unit-level guards are covered, but not every parallelism combination has a
@@ -49,6 +53,10 @@ block's shard is copied to a device buffer and reconstructed with
`all_gather_into_tensor` on a communication stream while the current block is
executing.
Multi-rank AllGather behavior is unchanged. When the effective DLO group size
is one, the backend now uses rank-local mmap staging; this closes a previous
loader/backend mismatch that could leave parameters on the `meta` device.
```text
Compute: [Block N] [Block N+1] [Block N+2]
H2D: [shard N+1] [shard N+2]
@@ -68,9 +76,18 @@ requests must take the same execution path at every denoising step.
### Rank-local path without DLO AllGather
With `--dlo-no-use-allgather`, DLO forces its internal offload shard size to
one. The regular model loader remains responsible for preparing each rank's
weights, including TP-local tensors or HSDP-managed parameters. DLO then
streams those rank-local tensors block by block using H2D copies only.
one and streams complete runtime blocks using H2D copies only. Host storage is
selected independently from that transfer protocol:
- For an explicitly mmap-compatible model with TP1 and no HSDP, each worker
retains immutable safetensors views backed by the node's shared OS page
cache. Two pinned host buffers, sized to the largest streamed block, stage
mmap pages for asynchronous H2D. Checkpoint-to-runtime adapters run while a
block is packed, so transformed weights do not become a persistent private
model copy.
- With TP, HSDP, online quantization, or a model without mmap support, the
regular loader remains responsible for preparing rank-local tensors. This
preserves TP/HSDP/quantization loader callbacks.
This mode means:
@@ -80,19 +97,26 @@ This mode means:
not shard weights across SP ranks.
- TP/HSDP/SP collectives, if configured, are not disabled by this flag; only
DLO's additional weight AllGather is disabled.
- Pure DP deployments keep a full host-side model copy per rank, subject to
shared OS page-cache behavior.
- Pure DP deployments with a compatible mmap checkpoint share the streamed
DiT's file-backed pages. Other components and fallback paths may still keep
rank-private host weights.
- The scheduler does not require a synchronized DP request wave for DLO.
This path is intentionally not implemented by reusing the AllGather mmap
loader. It relies on the standard loader so model-specific TP/HSDP transforms
remain intact.
The mmap source remains immutable in both transfer modes. AllGather copies a
persistent `1 / group_size` shard from it and then closes the mappings;
rank-local mode keeps the mappings open and copies a complete block through
bounded staging without adding a collective.
The current implementation packs mmap views into a staging slot synchronously
on the host; the subsequent pinned H2D transfer overlaps with block compute.
This keeps host memory bounded but makes CPU packing bandwidth part of the
rank-local path's performance profile.
## Parallelism compatibility
| Parallelism | DLO + AllGather | DLO without AllGather |
|---|---|---|
| **DP** | Supported primary path. DLO shards host weights across the DP group and can run DP multi-concurrency. | Supported rank-local path. DP replicas remain independent; no DLO weight sharding or cross-DP DLO collective. |
| **DP** | Supported primary path. DLO shards host weights across the DP group and can run DP multi-concurrency. | Supported rank-local path. DP replicas remain independent; no DLO weight collective. Compatible TP1 models share file-backed DiT pages across replicas. |
| **SP** | Supported in the implementation. With DP=1, DLO uses the SP group for host-weight sharding; SP still shards sequence/activation work. | SP remains active, but DLO keeps standard-loader rank-local weights and adds no SP weight collective. |
| **TP > 1** | Unsupported in the DLO mmap path. TP-aware loader callbacks are bypassed, so the backend rejects this configuration when it enters that path. | Standard loading is retained and TP-local tensors can be streamed. This is the intended compatibility path, but it still needs broader model and hardware validation. |
| **HSDP** | Rejected. HSDP has already sharded parameters, so DLO AllGather would double-shard them. | Accepted by configuration. HSDP owns parameter sharding and its own gathers; DLO only stages rank-local parameters. End-to-end coverage is limited. |
@@ -122,11 +146,35 @@ AllGather DP multi-concurrency requires:
The no-AllGather path does not impose these DLO-specific synchronized-wave
requirements.
The mmap loader is used only by the supported DLO+AllGather path when the
model has a compatible checkpoint layout. Online quantization is incompatible
with that sharded mmap path; use `--dlo-no-use-allgather` or disable online
quantization. Models that do not meet the mmap requirements use the regular
loader path.
The mmap loader is used when the model explicitly declares a compatible
checkpoint layout. It is independent of AllGather for TP1/non-HSDP execution.
Models can declare a per-parameter checkpoint-to-runtime adapter; the adapter
must preserve dtype and element count so it can be applied while packing one
staging block. MiniMax-H3 uses this mechanism for grouped QKV. Online
quantization and TP/HSDP no-AllGather execution use the regular loader path.
### Adding mmap support to another model
A new pipeline should opt in only after its ordinary loader contract can be
represented without materializing a private full-model copy:
1. Populate `weights_sources` with safetensors component sources and prefixes
that place every DiT checkpoint key in the pipeline parameter namespace.
2. If prefixed checkpoint keys already equal runtime parameter names, declare
`_supports_mmap_loading = True`. If names differ, provide
`_remap_ckpt_key` to map checkpoint names to runtime names (or return
`None` for intentionally ignored keys).
3. If a parameter needs a TP1 layout conversion, attach a callable
`mmap_weight_transform` to that parameter during model construction. The
transform must preserve dtype and element count because it runs while one
bounded block is packed.
4. Verify that mmap and the regular loader produce the same runtime tensors,
that every expected DiT tensor maps successfully, and that TP, HSDP, and
online-quantized configurations retain their standard-loader behavior.
Setting `_supports_mmap_loading = True` alone is not sufficient: explicit
opt-ins fail during initialization if any expected DiT tensor remains on the
`meta` device after checkpoint mapping.
## Validation coverage
@@ -136,6 +184,8 @@ Current source-level validation includes:
- HSDP + DLO without AllGather acceptance at configuration level;
- TP rejection in the DLO+AllGather mmap path;
- resident-layer requests requiring no-AllGather;
- rank-local mmap source retention, layout transforms, and bounded host
staging;
- DP request-wave validation for denoising-step compatibility;
- sharding, double-buffer, AllGather-size, and heterogeneous-block regression
tests.
@@ -150,7 +200,8 @@ on the target CUDA/NCCL or CANN/HCCL hardware.
scaling path.
- Use **SP + DLO AllGather** for long-sequence workloads when DP concurrency is
not the goal.
- Use **no-AllGather** to bring up TP or workstation PCIe configurations, with
the expectation of higher host-memory use and lower validation confidence.
- Use **no-AllGather** when replicas must avoid DLO collectives. TP1 models with
mmap support can share DiT checkpoint pages; TP/HSDP and unsupported models
retain the higher-memory standard-loader path.
- Prefer **HSDP alone** for production HSDP deployments until the combined
HSDP + DLO no-AllGather path has broader end-to-end coverage.
+25 -13
View File
@@ -189,7 +189,10 @@ multi-device deployments. In the default mode, each rank stores only
reconstructed at runtime via **AllGather** on a dedicated communication stream,
overlapped with computation via a fixed double-buffer scheme. The
`--dlo-no-use-allgather` mode instead streams standard-loader rank-local
weights independently and does not shard weights across ranks.
weights independently and does not shard weights across ranks. For explicitly
mmap-compatible TP1 models, the same mode can retain node-shared file-backed
DiT weights and use bounded per-worker host staging instead of a private full
DiT copy.
For the implementation design and compatibility matrix, see
[Distributed Layerwise Offload](../../design/feature/distributed_layerwise_offload.md).
@@ -201,8 +204,9 @@ For the implementation design and compatibility matrix, see
time, regardless of model size
- **DP multi-concurrency**: N concurrent requests processed in parallel
(AllGather only gathers weight shards, which are request-independent)
- **mmap weight loading**: weights loaded as mmap views pointing to shared OS
page cache, eliminating O(dp_size × model_size) RSS during model creation
- **mmap weight storage**: compatible checkpoints use views into the shared OS
page cache. AllGather copies persistent rank shards; rank-local mode keeps
the views and packs complete blocks through two bounded host staging slots
- **Platform-agnostic**: works on NVIDIA GPU (CUDA/NCCL) and Ascend NPU
(CANN/HCCL) via vLLM-Omni's platform abstraction
@@ -224,7 +228,7 @@ vllm serve /path/to/model --omni \
--enable-distributed-layerwise-offload \
--data-parallel-size 4
# Without DLO AllGather (each rank streams standard-loader rank-local weights)
# Without DLO AllGather (compatible TP1 models use node-shared mmap storage)
vllm serve /path/to/model --omni \
--enable-distributed-layerwise-offload \
--data-parallel-size 4 \
@@ -258,20 +262,26 @@ m = Omni(
### How model weights are loaded (mmap path)
When DLO + AllGather is active, the offloader:
When DLO and an mmap-compatible model are active, the offloader:
1. **Saves non-persistent buffers** (e.g. RoPE `inv_freq`, timestep `freqs`)
from the normally-created transformer
2. **Converts to meta device** via `to_empty(device="meta")`, releasing random
initialization weights
3. **Loads checkpoint weights as mmap views** via `safe_open().get_tensor()`,
which return views into the OS page cache (shared across all ranks, 0 RSS)
4. **Calls `post_load_weights()`** to apply model-specific dtype conversions
(e.g. Cosmos3's `time_embedder` → FP32)
which return views into the OS page cache without eagerly copying the full
tensor into anonymous process memory
4. **Applies declared per-parameter layout adapters while packing**, when the
checkpoint and TP1 runtime layouts differ (for example MiniMax-H3 grouped
QKV)
5. **Restores non-persistent buffers** from saved copies
This approach requires **zero model-specific code changes** — no pipeline or
transformer modifications are needed.
With AllGather, each rank copies only its persistent shard and then releases
the source mappings. Without AllGather, mappings remain open and every worker
owns two pinned staging slots sized to the largest streamed block. The latter
shares file-backed pages, not process-local Python objects, and does not alter
request routing or scheduling. Packing into the host slot is synchronous; the
pinned H2D copy can overlap with computation.
### OffloadPlan (declarative topology metadata)
@@ -313,7 +323,7 @@ request while AllGather synchronizes only weight shards (request-independent).
`--dlo-no-use-allgather` or disable online quantization.
- Tensor Parallel > 1 is rejected in the DLO+AllGather mmap path because TP-aware
weight-loader callbacks are bypassed. The no-AllGather path retains the
standard TP loader and is experimental.
standard TP loader instead of rank-local mmap and is experimental.
- HSDP + AllGather is rejected because it would double-shard parameters. HSDP
with no-AllGather is accepted at configuration level, but has limited
end-to-end validation.
@@ -373,6 +383,7 @@ reasoner/generator components inside the model forward pass.
| StableDiffusion3Pipeline | `stabilityai/stable-diffusion-3.5-medium` | `SD3Transformer2DModel` | - | ✓ | - | `"transformer_blocks"` |
| Wan22I2VPipeline | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | - | `"blocks"` |
| Wan22Pipeline | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | - | `"blocks"` |
| MiniMaxH3Pipeline | `MiniMaxAI/MiniMax-H3` (`FL2VA`) | `MiniMaxH3DiTModel` | - | ✓ | ✓ | `"blocks"` |
| SoulXSingerPipeline / SoulXSingerSVCPipeline | `Soul-AILab/SoulX-Singer` | `DiffLlama` (`cfm_decoder.model.diff_estimator`) | ✓ | ✓ | - | `"layers"` |
| BagelPipeline | `ByteDance-Seed/BAGEL-7B-MoT` | `Qwen2MoTModel` | - | ✓ | - | `"layers"`, `"customized modules"` |
| Cosmos3OmniDiffusersPipeline | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | `Cosmos3VFMTransformer`, `Cosmos3LanguageModel` | ✓ | ✓ | ✓ | `"layers"`, `"gen_layers"` |
@@ -382,8 +393,9 @@ reasoner/generator components inside the model forward pass.
- Layerwise Offloading requires DiT class to define `_layerwise_offload_blocks_attrs` pointing to transformer blocks
- Distributed Layerwise Offloading reuses the layerwise block topology, but the
model and parallelism combination still needs validation. AllGather mode
additionally requires a compatible checkpoint/mmap layout; no-AllGather mode
retains the standard loader's rank-local tensors. See the
additionally requires a compatible checkpoint/mmap layout. For explicitly
compatible TP1 models, no-AllGather uses node-shared mmap sources; other
layouts retain the standard loader's rank-local tensors. See the
[DLO design](../../design/feature/distributed_layerwise_offload.md) and
[Cosmos3 DistOffload recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/cosmos3/Cosmos3-DistOffload.md)
for usage examples.
@@ -11,6 +11,13 @@ Examples:
python examples/offline_inference/minimax_h3/dlo_dp2_lifecycle.py \
--model /path/to/MiniMax-H3/FL2VA --mode dlo-dp2
# Rank-local mmap storage, with no DLO collective or DP wave scheduling.
VLLM_WORKER_MULTIPROC_METHOD=spawn \
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=14400 \
CUDA_VISIBLE_DEVICES=0,1 \
python examples/offline_inference/minimax_h3/dlo_dp2_lifecycle.py \
--model /path/to/MiniMax-H3/FL2VA --mode dlo-dp2-no-allgather
VLLM_WORKER_MULTIPROC_METHOD=spawn \
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=14400 \
CUDA_VISIBLE_DEVICES=0,1 \
@@ -45,9 +52,9 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--model", required=True, help="Path to MiniMax-H3/FL2VA")
parser.add_argument(
"--mode",
choices=("dlo-dp2", "request"),
choices=("dlo-dp2", "dlo-dp2-no-allgather", "request"),
required=True,
help="DLO AllGather DP2 or ordinary non-DLO TP2 request mode",
help="DLO DP2 (with or without AllGather) or ordinary non-DLO TP2 request mode",
)
parser.add_argument("--steps", type=int, default=2)
parser.add_argument("--duration", type=float, default=5.0)
@@ -74,14 +81,14 @@ def engine_kwargs(args: argparse.Namespace) -> dict[str, Any]:
"stage_init_timeout": args.init_timeout,
"init_timeout": args.init_timeout,
}
if args.mode == "dlo-dp2":
if args.mode in {"dlo-dp2", "dlo-dp2-no-allgather"}:
common.update(
tensor_parallel_size=1,
data_parallel_size=2,
text_encoder_tp_size=1,
vae_patch_parallel_size=1,
enable_distributed_layerwise_offload=True,
dlo_use_allgather=True,
dlo_use_allgather=args.mode == "dlo-dp2",
dlo_resident_layers=0,
)
else:
@@ -191,6 +191,30 @@ def test_model_load_weights_transforms_before_calling_vllm_loader():
]
def test_unquantized_qkv_declares_equivalent_mmap_transform(monkeypatch):
from vllm_omni.diffusion.models.minimax_h3 import minimax_h3_transformer as h3
monkeypatch.setattr(h3, "QKVParallelLinear", _FakeLinear)
monkeypatch.setattr(h3, "RowParallelLinear", _FakeLinear)
monkeypatch.setattr(h3, "Attention", _FakeAttention)
arch = h3.MiniMaxH3DiTArchConfig(
hidden_size=1,
num_attention_heads=2,
attention_head_dim=1,
rope_inv_freq_len=1,
)
attention = h3.MiniMaxH3Attention(
arch,
quant_config=None,
prefix="blocks.0.attn",
)
transform = attention.qkv_proj.weight.mmap_weight_transform
checkpoint_weight = torch.arange(6, dtype=torch.float32).reshape(6, 1)
assert transform(checkpoint_weight)[:, 0].tolist() == [0, 3, 1, 4, 2, 5]
def test_pipeline_resolves_transformer_component_quant_config():
from vllm_omni.diffusion.models.minimax_h3.pipeline_minimax_h3 import (
_resolve_component_quant_config,
@@ -30,7 +30,13 @@ from vllm_omni.diffusion.offloader.distributed_layerwise_backend import (
PinnedResidentLayerGroup,
)
from vllm_omni.diffusion.offloader.module_residency import PinnedModuleStager
from vllm_omni.diffusion.offloader.offload_plan import OffloadPlan, get_offload_plan
from vllm_omni.diffusion.offloader.offload_plan import (
OffloadPlan,
get_offload_plan,
has_online_quantization,
should_select_dlo_mmap,
supports_mmap_loading,
)
from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
@@ -48,6 +54,9 @@ class DummyEvent:
def record(self, _stream) -> None:
return None
def synchronize(self) -> None:
return None
@contextmanager
def dummy_stream(_stream):
@@ -350,6 +359,79 @@ class TestDistributedLayerwiseOffloadHook:
assert torch.equal(hook.cpu_shards[torch.float32], torch.tensor([0.0, 1.0]))
def test_rank_local_mmap_retains_source_and_uses_staging(self, dist_group, patched_offload_runtime):
current_block = nn.Linear(2, 2, bias=False)
next_block = nn.Linear(2, 2, bias=False)
next_block.weight.data.copy_(torch.arange(4, dtype=torch.float32).view(2, 2))
source_storage = next_block.weight.untyped_storage().data_ptr()
next_block.weight.mmap_weight_transform = lambda tensor: tensor.flip(0)
next_block.weight.mmap_weight_transform_pending = True
hook = DistributedLayerwiseOffloadHook(
next_block=next_block,
device=torch.device("cpu"),
dp_group=None,
dp_size=1,
rank=0,
pin_memory=False,
rank_local_mmap=True,
)
hook.initialize_hook(current_block)
source = hook.cpu_sources[torch.float32][0]["tensor"]
assert source.untyped_storage().data_ptr() == source_storage
assert hook.cpu_shards == {}
assert next_block.weight.numel() == 0
hook.cpu_staging_buffers = [
{torch.float32: torch.empty(4)},
{torch.float32: torch.empty(4)},
]
hook.prefetch_layer(slot=0, non_blocking=False)
assert torch.equal(
next_block.weight,
torch.tensor([[2.0, 3.0], [0.0, 1.0]]),
)
assert torch.equal(source, torch.arange(4, dtype=torch.float32).view(2, 2))
def test_rank_local_mmap_staging_is_bounded_by_largest_block(self, patched_offload_runtime):
hooks = []
for size in (4, 9):
current_block = nn.Linear(1, 1, bias=False)
next_block = nn.Module()
next_block.weight = nn.Parameter(torch.arange(size, dtype=torch.float32))
hook = DistributedLayerwiseOffloadHook(
next_block=next_block,
device=torch.device("cpu"),
dp_group=None,
dp_size=1,
rank=0,
pin_memory=False,
shared_buffers=[None, None],
rank_local_mmap=True,
)
hook.initialize_hook(current_block)
hooks.append(hook)
resident_block = nn.Module()
resident_block.weight = nn.Parameter(torch.arange(12, dtype=torch.float32))
resident_group = PinnedResidentLayerGroup(
[resident_block],
device=torch.device("cpu"),
copy_stream=DummyStream(),
pin_memory=False,
rank_local_mmap=True,
)
staging = DistributedLayerwiseOffloadBackend._allocate_shared_cpu_staging_buffers(
hooks,
resident_group,
)
assert len(staging) == 2
assert staging[0][torch.float32].numel() == 12
assert staging[1][torch.float32].numel() == 12
class TestPinnedResidentLayerGroup:
def test_load_offload_reuses_pinned_master_weights(self, patched_offload_runtime):
@@ -386,6 +468,42 @@ class TestPinnedResidentLayerGroup:
group.offload()
def test_rank_local_mmap_resident_layers_retain_sources(self, patched_offload_runtime):
blocks = [nn.Linear(2, 2, bias=False) for _ in range(3)]
expected = []
source_storage = []
for index, block in enumerate(blocks):
block.weight.data.fill_(index + 1)
expected.append(block.weight.detach().clone())
source_storage.append(block.weight.untyped_storage().data_ptr())
group = PinnedResidentLayerGroup(
blocks,
device=torch.device("cpu"),
copy_stream=DummyStream(),
pin_memory=False,
rank_local_mmap=True,
)
for index, state in enumerate(group._states):
source = state["cpu_sources"][torch.float32][0]["tensor"]
assert source.untyped_storage().data_ptr() == source_storage[index]
assert state["cpu_shards"] == {}
assert len(group._cpu_staging_buffers) == 2
group.load()
for block, weight in zip(blocks, expected):
assert torch.equal(block.weight, weight)
blocks[0].weight.data.zero_()
group.offload()
group.load()
for block, weight in zip(blocks, expected):
assert torch.equal(block.weight, weight)
group.offload()
assert all(block.weight.numel() == 0 for block in blocks)
def test_mmap_loader_attrs_survive_to_empty_parameter_replacement(self):
module = nn.Linear(2, 2, bias=False)
@@ -779,6 +897,46 @@ class TestOffloadPlan:
with pytest.raises(Exception):
plan.block_attrs = {"x": ("y",)} # type: ignore
def test_explicit_component_sources_can_opt_in_to_mmap(self):
class Pipeline(nn.Module):
_supports_mmap_loading = True
weights_sources = [object()]
assert supports_mmap_loading(Pipeline())
def test_component_sources_do_not_implicitly_enable_mmap(self):
class Pipeline(nn.Module):
weights_sources = [object()]
assert not supports_mmap_loading(Pipeline())
def test_online_quantization_detection(self):
model = nn.Sequential(nn.Linear(1, 1))
assert not has_online_quantization(model)
model[0].quant_method = SimpleNamespace(uses_meta_device=True)
assert has_online_quantization(model)
@pytest.mark.parametrize(
("kwargs", "expected"),
[
({"dlo_use_allgather": True, "tensor_parallel_size": 2}, True),
({"dlo_use_allgather": False, "tensor_parallel_size": 1}, True),
({"dlo_use_allgather": False, "tensor_parallel_size": 2}, False),
({"dlo_use_allgather": False, "tensor_parallel_size": 1, "use_hsdp": True}, False),
({"dlo_use_allgather": False, "tensor_parallel_size": 1, "has_online_quant": True}, False),
],
)
def test_dlo_mmap_policy(self, kwargs, expected):
defaults = {
"mmap_supported": True,
"dlo_use_allgather": False,
"has_online_quant": False,
"tensor_parallel_size": 1,
"use_hsdp": False,
}
assert should_select_dlo_mmap(**(defaults | kwargs)) is expected
def test_all_resident_dit_still_prepares_planned_submodules(self, patched_offload_runtime):
class TokenRefiner(nn.Module):
def __init__(self):
@@ -827,6 +985,93 @@ class TestOffloadPlan:
class TestMmapValidation:
"""Tests for mmap loader validation: TP rejection, strict check, validate_loaded_weights."""
def test_component_source_prefix_builds_model_namespace(self, tmp_path):
source_root = tmp_path / "partition"
checkpoint_dir = source_root / "transformer"
checkpoint_dir.mkdir(parents=True)
checkpoint_file = checkpoint_dir / "model.safetensors"
save_file({"blocks.0.weight": torch.arange(4, dtype=torch.float32)}, str(checkpoint_file))
class Pipeline(nn.Module):
_supports_mmap_loading = True
def __init__(self):
super().__init__()
self.weights_sources = [
SimpleNamespace(
model_or_path=str(source_root),
subfolder="transformer",
revision=None,
prefix="transformer.",
)
]
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
),
torch.device("cpu"),
)
model_map = backend._build_mmap_model_map(Pipeline())
assert model_map == {
"transformer.blocks.0.weight": (
"blocks.0.weight",
str(checkpoint_file),
)
}
def test_explicit_mmap_opt_in_rejects_missing_model_tensors(self, tmp_path, patched_offload_runtime, monkeypatch):
monkeypatch.setattr(
"vllm.distributed.parallel_state.get_tensor_model_parallel_world_size",
lambda: 1,
)
source_root = tmp_path / "partition"
checkpoint_dir = source_root / "transformer"
checkpoint_dir.mkdir(parents=True)
save_file(
{"blocks.0.weight": torch.ones((2, 2), dtype=torch.float32)},
str(checkpoint_dir / "model.safetensors"),
)
class Transformer(nn.Module):
_layerwise_offload_blocks_attrs = ["blocks"]
def __init__(self):
super().__init__()
self.blocks = nn.ModuleList([nn.Linear(2, 2, bias=False), nn.Linear(2, 2, bias=False)])
class Pipeline(nn.Module):
_supports_mmap_loading = True
def __init__(self):
super().__init__()
self.transformer = Transformer()
self.weights_sources = [
SimpleNamespace(
model_or_path=str(source_root),
subfolder="transformer",
revision=None,
prefix="transformer.",
)
]
pipeline = Pipeline()
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
),
torch.device("cpu"),
)
modules = SimpleNamespace(dits=[pipeline.transformer], dit_names=["transformer"])
with pytest.raises(RuntimeError, match="Explicit mmap loading left 1"):
backend._load_weights_via_mmap(pipeline, modules)
def test_tp_rejected_when_tp_world_size_gt_1(self, tmp_path, patched_offload_runtime, monkeypatch):
"""DLO+AllGather should reject when TP world size > 1."""
import json
@@ -363,22 +363,39 @@ class DiffusersPipelineLoader:
else:
model = self._init_from_load_format(load_format, target_device, custom_pipeline_name, is_hsdp=False)
# Skip load_weights only for DLO+AllGather when the model
# supports mmap loading and no online quantization is active.
# This condition MUST match the gate in
# DistributedLayerwiseOffloadBackend.enable() so that the
# loader skips ⟺ enable() uses mmap.
from vllm_omni.diffusion.offloader.offload_plan import supports_mmap_loading
# Skip load_weights when DLO can keep checkpoint tensors as
# mmap-backed host masters. Storage selection is independent
# of whether DLO reconstructs weights with AllGather. The
# no-AllGather path can use mmap only when the checkpoint is
# already the rank-local runtime layout (currently TP=1 and no
# HSDP); TP/HSDP still require the regular loader callbacks.
# The loader and backend share should_select_dlo_mmap() so
# skipping load_weights always selects mmap in enable().
from vllm_omni.diffusion.offloader.offload_plan import (
should_select_dlo_mmap,
supports_mmap_loading,
)
_dist_offload = getattr(self.od_config, "enable_distributed_layerwise_offload", False)
_use_ag = getattr(self.od_config, "dlo_use_allgather", True)
_has_online_quant = self._has_online_quant(model)
_supports_mmap = supports_mmap_loading(model)
_tp_size = int(getattr(self.parallel_config, "tensor_parallel_size", 1))
_use_hsdp = bool(getattr(self.parallel_config, "use_hsdp", False))
_skip_load = _dist_offload and _use_ag and _supports_mmap and not _has_online_quant
_skip_load = _dist_offload and should_select_dlo_mmap(
mmap_supported=_supports_mmap,
dlo_use_allgather=_use_ag,
has_online_quant=_has_online_quant,
tensor_parallel_size=_tp_size,
use_hsdp=_use_hsdp,
)
if _skip_load:
logger.info("DLO+AllGather active: skipping load_weights (will load via mmap in enable())")
logger.info(
"DLO mmap storage active (%s): skipping load_weights (will map checkpoint weights in enable())",
"AllGather" if _use_ag else "rank-local",
)
else:
if _dist_offload and _use_ag and _has_online_quant:
raise ValueError(
@@ -441,11 +458,11 @@ class DiffusersPipelineLoader:
"""Whether any layer uses an online-quant method that defers weight
materialization onto the ``meta`` device (upstream vLLM
``uses_meta_device=True``, e.g. online FP8)."""
for module in model.modules():
quant_method = getattr(module, "quant_method", None)
if getattr(quant_method, "uses_meta_device", False):
return True
return False
from vllm_omni.diffusion.offloader.offload_plan import (
has_online_quantization,
)
return has_online_quantization(model)
def _apply_skip_softmax_calibration(self, model: nn.Module) -> None:
from vllm_omni.diffusion.attention.backends.trtllm_calibration import (
@@ -11,6 +11,7 @@ from __future__ import annotations
import math
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from functools import partial
from typing import TYPE_CHECKING, Any
import torch
@@ -350,6 +351,18 @@ class MiniMaxH3Attention(nn.Module):
prefix=f"{prefix}.qkv_proj",
return_bias=True,
)
if quant_config is None:
# The checkpoint interleaves Q/K/V by query group, while vLLM's
# fused QKV linear consumes contiguous Q, then K, then V rows. The
# regular loader performs this conversion before weight_loader().
# DLO mmap applies the same adapter while packing one bounded host
# staging block; the raw file-backed master remains node-shared.
self.qkv_proj.weight.mmap_weight_transform = partial(
_reorder_grouped_qkv_to_qkv,
num_query_groups=arch.num_attention_heads,
heads_per_group=1,
head_dim=arch.attention_head_dim,
)
self.num_heads = self.qkv_proj.num_heads
self.num_kv_heads = self.qkv_proj.num_kv_heads
self.rot_dim = 6 * arch.rope_inv_freq_len
@@ -532,13 +532,12 @@ class MiniMaxH3Pipeline(
encoder_block_attrs={"text_encoder": ("vision.blocks", "text_model.layers")},
on_demand_component_paths=frozenset({"text_encoder", "video_vae", "audio_vae"}),
)
# H3's regular loader performs checkpoint-layout conversions (grouped QKV
# and fused MLP). Keep it on that path until mmap can run the same loader
# callbacks for every affected parameter.
_supports_mmap_loading: ClassVar[bool] = False
# TODO(offload): Re-enable after the generic rank-local mmap path can run
# this model's grouped-QKV reorder and fused-MLP packing before TP sharding.
# Do not bypass the regular loader until that equivalence is tested.
# Explicit mmap opt-in: weights_sources below provide exact pipeline
# prefixes, and TP1 DLO mmap applies the grouped-QKV adapter while packing
# each block. The fused MLP checkpoint is already gate-then-up, matching
# its TP1 runtime layout. TP>1 and quantized layouts stay on the regular
# loader. See the DLO design doc for the model opt-in contract.
_supports_mmap_loading: ClassVar[bool] = True
_PROFILER_TARGETS: ClassVar[list[str]] = [
"_prepare_reference_videos",
"encode_prompt",
@@ -648,6 +647,10 @@ class MiniMaxH3Pipeline(
od_config.quantization_config,
"transformer",
)
if transformer_quant_config is not None:
# Quantized parameters may have packed weights and scale tensors
# whose runtime layout is owned by quant-method loader callbacks.
self._supports_mmap_loading = False
self.transformer = MiniMaxH3DiTModel(
od_config,
quant_config=transformer_quant_config,
+8 -1
View File
@@ -32,6 +32,10 @@ class OffloadConfig:
dlo_use_allgather: bool = True
dlo_resident_layers: int = 0 # leading DiT layers kept on device
model_path: str | None = None # checkpoint path for mmap weight loading
# Rank-local mmap bypasses TP-aware loader callbacks, so the backend needs
# the configured TP size when selecting mmap versus standard-loader host
# storage. Appended to preserve the positional order of existing fields.
tensor_parallel_size: int = 1
@classmethod
def from_od_config(cls, od_config: OmniDiffusionConfig) -> "OffloadConfig":
@@ -58,6 +62,7 @@ class OffloadConfig:
parallel_config = getattr(od_config, "parallel_config", None)
use_hsdp = getattr(parallel_config, "use_hsdp", False) if parallel_config else False
tensor_parallel_size = int(getattr(parallel_config, "tensor_parallel_size", 1)) if parallel_config else 1
# Derive dp_size from parallel_config — not user-configurable.
# The offload adapts to whatever DP/SP is already configured.
@@ -116,7 +121,8 @@ class OffloadConfig:
dp_size = 1
logger.info(
"Distributed layerwise offload: dlo_use_allgather=False, "
"streaming standard-loader rank-local weights (no DP shard or AllGather)"
"streaming complete rank-local blocks (no DLO shard or AllGather); "
"the backend will select mmap or standard-loader host storage"
)
# HSDP already shards parameters into DTensors. Running distributed
@@ -134,6 +140,7 @@ class OffloadConfig:
strategy=strategy,
pin_cpu_memory=pin_cpu_memory,
use_hsdp=use_hsdp,
tensor_parallel_size=tensor_parallel_size,
dp_size=dp_size,
dlo_use_allgather=dlo_use_allgather,
dlo_resident_layers=dlo_resident_layers,
@@ -1,15 +1,17 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Distributed Layerwise Offload backend with H2D + AllGather overlap.
"""Distributed Layerwise Offload backend with double-buffered H2D.
This module implements the RFC-1 "Distributed Layerwise Offload" mechanism that:
* Shards model weights across DP ranks and stores only the local shard
(1/DP_size of full model) in host pinned memory per rank.
* Optionally shards model weights across DP ranks and reconstructs each block
with AllGather, or streams a complete rank-local block without a collective.
* Can retain compatible checkpoint tensors as node-shared mmap sources instead
of creating a persistent private host copy in every rank.
* Uses a fixed double-buffer scheme that keeps only two layers' worth of
weights on each device at any time.
* Asynchronously pipelines both H2D transfers and AllGather communications
on dedicated streams, fully overlapping them with computation.
* Pipelines H2D transfers and, when enabled, AllGather communications on
dedicated streams, overlapping them with computation.
* Is hardware-agnostic, supporting both NVIDIA GPU (CUDA) and Ascend NPU
(CANN) platforms via vLLM-Omni's platform abstraction layer.
"""
@@ -31,7 +33,13 @@ from vllm_omni.platforms import current_omni_platform
from .base import OffloadBackend, OffloadConfig
from .block_discovery import get_blocks_from_dit
from .module_collector import ModuleDiscovery
from .offload_plan import OffloadPlan, get_offload_plan, supports_mmap_loading
from .offload_plan import (
OffloadPlan,
get_offload_plan,
has_online_quantization,
should_select_dlo_mmap,
supports_mmap_loading,
)
from .tensor_utils import (
dtype_size as _dtype_size,
)
@@ -74,6 +82,7 @@ class DistributedLayerwiseOffloadHook(ModelHook):
comm_stream: Any | None = None,
pin_memory: bool = True,
shared_buffers: list[dict[torch.dtype, torch.Tensor] | None] | None = None,
rank_local_mmap: bool = False,
):
assert isinstance(next_block, nn.Module), "transformer block must be type `torch.nn.Module`"
@@ -83,6 +92,7 @@ class DistributedLayerwiseOffloadHook(ModelHook):
self.dp_size = dp_size
self.rank = rank
self.pin_memory = pin_memory
self.rank_local_mmap = rank_local_mmap
self.copy_stream = copy_stream or current_omni_platform.Stream()
self.comm_stream = comm_stream or current_omni_platform.Stream()
@@ -98,8 +108,18 @@ class DistributedLayerwiseOffloadHook(ModelHook):
# Sharded host weights for the next block, keyed by dtype
self.cpu_shards: dict[torch.dtype, torch.Tensor] = {}
# File-backed source tensors for rank-local mmap. Unlike cpu_shards,
# these remain immutable views of the checkpoint and are never pinned
# or flattened into a model-sized private allocation.
self.cpu_sources: dict[torch.dtype, list[dict[str, Any]]] = {}
self.metadata: dict[torch.dtype, list[dict[str, Any]]] = {}
# Rank-local mmap uses two host staging slots shared by every hook in
# this worker. They are assigned by the backend after all block sizes
# are known, mirroring the shared device-buffer allocation.
self.cpu_staging_buffers: list[dict[torch.dtype, torch.Tensor] | None] = [None, None]
self.cpu_staging_events: list[Any | None] = [None, None]
# Current slot index (0 or 1). Updated dynamically by the previous
# hook's prefetch_layer call via _prefetched_slot. This ensures
# correct slot tracking for ALL block counts (including odd N).
@@ -154,14 +174,20 @@ class DistributedLayerwiseOffloadHook(ModelHook):
self.next_block_parameters = dict(self.next_block.named_parameters())
self.next_block_buffers = dict(self.next_block.named_buffers())
# Shard next block's weights and store local shard in pinned CPU memory
self.cpu_shards, self.metadata = self._shard_and_pin(
self.next_block_parameters,
self.next_block_buffers,
self.dp_size,
self.rank,
self.pin_memory,
)
if self.rank_local_mmap:
self.cpu_sources, self.metadata = self._collect_mmap_sources(
self.next_block_parameters,
self.next_block_buffers,
)
else:
# Shard next block's weights and store local shard in pinned CPU memory.
self.cpu_shards, self.metadata = self._shard_and_pin(
self.next_block_parameters,
self.next_block_buffers,
self.dp_size,
self.rank,
self.pin_memory,
)
# Allocate device buffers only if not using shared buffers from backend
if self._owns_buffers:
@@ -185,11 +211,65 @@ class DistributedLayerwiseOffloadHook(ModelHook):
self._ag_output_sizes: dict[torch.dtype, int] = {}
for dtype, metas in self.metadata.items():
total_numel = sum(m["numel"] for m in metas)
shard_numel = self.cpu_shards[dtype].numel()
self._ag_output_sizes[dtype] = shard_numel * self.dp_size if self.dp_size > 1 else total_numel
if self.rank_local_mmap:
self._ag_output_sizes[dtype] = total_numel
else:
shard_numel = self.cpu_shards[dtype].numel()
self._ag_output_sizes[dtype] = shard_numel * self.dp_size if self.dp_size > 1 else total_numel
return module
@staticmethod
def _collect_mmap_sources(
params: dict[str, nn.Parameter],
bufs: dict[str, torch.Tensor],
) -> tuple[dict[torch.dtype, list[dict[str, Any]]], dict[torch.dtype, list[dict[str, Any]]]]:
"""Retain file-backed tensors and replace module storage with placeholders.
The returned sources preserve safetensors mmap storage. Runtime-layout
adapters are applied only while packing a bounded staging slot, so no
full-model anonymous CPU copy is retained by a worker.
"""
cpu_sources: dict[torch.dtype, list[dict[str, Any]]] = {}
metadata: dict[torch.dtype, list[dict[str, Any]]] = {}
offsets: dict[torch.dtype, int] = {}
for name, target in chain(params.items(), bufs.items()):
source = target.to_local() if hasattr(target, "to_local") else target
if source.device.type != "cpu":
raise ValueError(
f"Rank-local mmap storage requires CPU checkpoint views, but {name!r} is on {source.device}."
)
dtype = source.dtype
offset = offsets.get(dtype, 0)
transform = getattr(target, "mmap_weight_transform", None)
if not getattr(target, "mmap_weight_transform_pending", False):
transform = None
cpu_sources.setdefault(dtype, []).append(
{
"name": name,
"tensor": source.detach(),
"transform": transform,
}
)
metadata.setdefault(dtype, []).append(
{
"name": name,
"offset": offset,
"numel": source.numel(),
"shape": source.shape,
}
)
offsets[dtype] = offset + source.numel()
# The detached source above keeps the mmap storage alive while the
# module parameter/buffer is rebound to the rotating device slot.
set_tensor_storage(target, make_offload_placeholder(target))
return cpu_sources, metadata
@staticmethod
def _shard_and_pin(
params: dict[str, nn.Parameter],
@@ -306,6 +386,52 @@ class DistributedLayerwiseOffloadHook(ModelHook):
# Prefetch: H2D + AllGather (overlapped on dedicated streams) #
# ------------------------------------------------------------------ #
@staticmethod
def _pack_mmap_sources(
cpu_sources: dict[torch.dtype, list[dict[str, Any]]],
metadata: dict[torch.dtype, list[dict[str, Any]]],
slot_buffers: dict[torch.dtype, torch.Tensor],
) -> dict[torch.dtype, torch.Tensor]:
staged: dict[torch.dtype, torch.Tensor] = {}
for dtype, metas in metadata.items():
total_numel = sum(meta["numel"] for meta in metas)
destination = slot_buffers[dtype][:total_numel]
sources = cpu_sources[dtype]
for source_info, meta in zip(sources, metas, strict=True):
source = source_info["tensor"]
transform = source_info["transform"]
if callable(transform):
source = transform(source)
if source.dtype != dtype or source.numel() != meta["numel"]:
raise ValueError(
"mmap weight transform changed tensor layout for "
f"{source_info['name']!r}: expected dtype={dtype}, "
f"numel={meta['numel']}, got dtype={source.dtype}, "
f"numel={source.numel()}"
)
start = meta["offset"]
destination[start : start + meta["numel"]].copy_(source.reshape(-1))
staged[dtype] = destination
return staged
def _stage_mmap_sources(self, slot: int) -> dict[torch.dtype, torch.Tensor]:
"""Pack this block's mmap views into one bounded host staging slot."""
previous_copy = self.cpu_staging_events[slot]
if previous_copy is not None:
synchronize = getattr(previous_copy, "synchronize", None)
if callable(synchronize):
synchronize()
else:
# Platform events normally expose synchronize(). Retain a
# correctness fallback for test and non-CUDA platform shims.
current_omni_platform.synchronize()
self.cpu_staging_events[slot] = None
slot_buffers = self.cpu_staging_buffers[slot]
if slot_buffers is None:
raise RuntimeError(f"cpu_staging_buffers[{slot}] was not allocated")
return self._pack_mmap_sources(self.cpu_sources, self.metadata, slot_buffers)
@torch.compiler.disable
def prefetch_layer(self, slot: int, non_blocking: bool = True) -> None:
"""Prepare next block's weights into the shared device buffer for *slot*.
@@ -321,11 +447,17 @@ class DistributedLayerwiseOffloadHook(ModelHook):
assert gpu_weights is not None, f"gpu_buffers[{slot}] not allocated"
if self.dp_size <= 1 or self.dp_group is None:
cpu_weights = self._stage_mmap_sources(slot) if self.rank_local_mmap else self.cpu_shards
with current_omni_platform.stream(self.copy_stream):
for dtype, cpu_shard in self.cpu_shards.items():
for dtype, cpu_shard in cpu_weights.items():
gw = gpu_weights[dtype]
gw[: cpu_shard.numel()].copy_(cpu_shard, non_blocking=non_blocking)
async_copy = non_blocking and cpu_shard.is_pinned()
gw[: cpu_shard.numel()].copy_(cpu_shard, non_blocking=async_copy)
evt.record(self.copy_stream)
if self.rank_local_mmap:
# The CPU slot may be overwritten only after this H2D copy has
# finished. The shared event protects reuse by another hook.
self.cpu_staging_events[slot] = evt
else:
gpu_shards: dict[torch.dtype, torch.Tensor] = {}
shard_bufs = self.gpu_shard_buffers[slot]
@@ -468,6 +600,7 @@ def apply_distributed_block_hook(
comm_stream: Any | None = None,
pin_memory: bool = True,
shared_buffers: list[dict[torch.dtype, torch.Tensor] | None] | None = None,
rank_local_mmap: bool = False,
) -> DistributedLayerwiseOffloadHook:
"""Register a DistributedLayerwiseOffloadHook on *module*."""
registry = HookRegistry.get_or_create(module)
@@ -481,6 +614,7 @@ def apply_distributed_block_hook(
comm_stream=comm_stream,
pin_memory=pin_memory,
shared_buffers=shared_buffers,
rank_local_mmap=rank_local_mmap,
)
registry.register_hook(DistributedLayerwiseOffloadHook._HOOK_NAME, hook)
return hook
@@ -495,15 +629,16 @@ def remove_distributed_block_hook(module: nn.Module) -> None:
class PinnedResidentLayerGroup:
"""Keep selected layers in pinned host memory between requests.
"""Keep selected layers available for stage-scoped device residency.
TODO(offload): Extract this alongside PinnedModuleStager after the
distributed shard-and-pin operation becomes a shared storage primitive.
It currently remains here because it depends on DLO's local-shard layout.
Unlike ``module.to(device)``/``module.to("cpu")``, this group retains a
pinned CPU master copy and never copies generated device weights back to
host. Entering the denoise stage performs one asynchronous H2D pass;
pinned CPU master copy (or mmap source plus bounded staging) and never
copies generated device weights back to host. Entering the denoise stage
performs one asynchronous H2D pass;
leaving it only restores zero-sized placeholders and releases the device
buffers. This lets the following VAE stage reuse the same HBM.
@@ -518,10 +653,14 @@ class PinnedResidentLayerGroup:
device: torch.device,
copy_stream: Any,
pin_memory: bool,
rank_local_mmap: bool = False,
defer_staging: bool = False,
) -> None:
self.device = device
self.copy_stream = copy_stream
self.loaded = False
self.rank_local_mmap = rank_local_mmap
self.pin_memory = pin_memory
self._states: list[dict[str, Any]] = []
self._gpu_buffers: list[dict[torch.dtype, torch.Tensor]] = []
@@ -529,21 +668,47 @@ class PinnedResidentLayerGroup:
params = dict(block.named_parameters())
bufs = dict(block.named_buffers())
targets: dict[str, torch.Tensor] = {**params, **bufs}
cpu_shards, metadata = DistributedLayerwiseOffloadHook._shard_and_pin(
params,
bufs,
dp_size=1,
rank=0,
pin_memory=pin_memory,
)
if rank_local_mmap:
cpu_sources, metadata = DistributedLayerwiseOffloadHook._collect_mmap_sources(
params,
bufs,
)
cpu_shards = {}
else:
cpu_shards, metadata = DistributedLayerwiseOffloadHook._shard_and_pin(
params,
bufs,
dp_size=1,
rank=0,
pin_memory=pin_memory,
)
cpu_sources = {}
self._states.append(
{
"targets": targets,
"cpu_shards": cpu_shards,
"cpu_sources": cpu_sources,
"metadata": metadata,
}
)
self._cpu_staging_buffers: list[dict[torch.dtype, torch.Tensor]] = []
self._cpu_staging_events: list[Any | None] = [None, None]
if rank_local_mmap and not defer_staging:
max_sizes: dict[torch.dtype, int] = {}
for state in self._states:
for dtype, metas in state["metadata"].items():
total = sum(meta["numel"] for meta in metas)
max_sizes[dtype] = max(max_sizes.get(dtype, 0), total)
for _ in range(2):
buffers = {}
for dtype, total in max_sizes.items():
buffer = torch.empty(total, dtype=dtype, device="cpu")
if pin_memory:
buffer = buffer.pin_memory()
buffers[dtype] = buffer
self._cpu_staging_buffers.append(buffers)
def load(self) -> None:
if self.loaded:
return
@@ -554,20 +719,41 @@ class PinnedResidentLayerGroup:
gpu_buffers: list[dict[torch.dtype, torch.Tensor]] = []
for state in self._states:
block_buffers: dict[torch.dtype, torch.Tensor] = {}
for dtype, cpu_shard in state["cpu_shards"].items():
block_buffers[dtype] = torch.empty(
cpu_shard.shape,
dtype=dtype,
device=self.device,
)
for dtype, metas in state["metadata"].items():
total = sum(meta["numel"] for meta in metas)
block_buffers[dtype] = torch.empty(total, dtype=dtype, device=self.device)
gpu_buffers.append(block_buffers)
self.copy_stream.wait_stream(current_omni_platform.current_stream())
ready = current_omni_platform.Event()
with current_omni_platform.stream(self.copy_stream):
for state, block_buffers in zip(self._states, gpu_buffers):
for dtype, cpu_shard in state["cpu_shards"].items():
block_buffers[dtype].copy_(cpu_shard, non_blocking=True)
for index, (state, block_buffers) in enumerate(zip(self._states, gpu_buffers)):
if self.rank_local_mmap:
slot = index % 2
previous_copy = self._cpu_staging_events[slot]
if previous_copy is not None:
synchronize = getattr(previous_copy, "synchronize", None)
if callable(synchronize):
synchronize()
else:
current_omni_platform.synchronize()
cpu_weights = DistributedLayerwiseOffloadHook._pack_mmap_sources(
state["cpu_sources"],
state["metadata"],
self._cpu_staging_buffers[slot],
)
else:
cpu_weights = state["cpu_shards"]
for dtype, cpu_weight in cpu_weights.items():
block_buffers[dtype].copy_(
cpu_weight,
non_blocking=cpu_weight.is_pinned(),
)
if self.rank_local_mmap:
slot_ready = current_omni_platform.Event()
slot_ready.record(self.copy_stream)
self._cpu_staging_events[slot] = slot_ready
ready.record(self.copy_stream)
for state, block_buffers in zip(self._states, gpu_buffers):
@@ -630,6 +816,8 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self._all_hook_groups: list[list[DistributedLayerwiseOffloadHook]] = []
self._resident_blocks: list[nn.Module] = []
self._resident_layer_group: PinnedResidentLayerGroup | None = None
self._using_mmap = False
self._using_rank_local_mmap = False
def load_resident_layers(self) -> None:
"""Load the model-declared leading blocks for the denoise stage."""
@@ -664,70 +852,146 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
if attr == "mmap_weight_transform":
replacement.mmap_weight_transform_pending = True
def _load_weights_via_mmap(self, pipeline: nn.Module, modules) -> None:
"""Load DiT weights from safetensors via mmap views (no RSS).
When the transformer is created on meta device, this method
replaces meta params with mmap views of the checkpoint files.
The views point to OS page cache (shared across ranks), so
no private copy is created. _shard_and_pin then copies only
the 1/dp_size shard from the mmap view to a private buffer.
Non-DiT modules (VAE, encoders) are NOT affected — they were
created on CPU with real weights via from_pretrained.
"""
@staticmethod
def _add_safetensors_source(
checkpoint_dir: str,
prefix: str,
remap_fn: Any | None,
model_to_ckpt: dict[str, tuple[str, str]],
) -> int:
"""Index one checkpoint directory into the pipeline parameter namespace."""
import glob
import json
from safetensors import safe_open
model_path = self.config.model_path
if not model_path:
logger.warning("No model_path for mmap weight loading, skipping")
return
indexed_keys = 0
index_files = glob.glob(os.path.join(checkpoint_dir, "*.safetensors.index.json"))
if index_files:
for index_file in index_files:
with open(index_file, encoding="utf-8") as handle:
index = json.load(handle)
for checkpoint_key, filename in index.get("weight_map", {}).items():
source_name = prefix + checkpoint_key
model_name = remap_fn(source_name) if remap_fn is not None else source_name
if model_name is not None:
model_to_ckpt[model_name] = (
checkpoint_key,
os.path.join(checkpoint_dir, filename),
)
indexed_keys += 1
return indexed_keys
# Resolve HF repo ID to local snapshot path.
# If model_path is a local directory, use it directly; otherwise
# download the safetensors index + weight files via HuggingFace Hub.
if not os.path.isdir(model_path):
from vllm.model_executor.model_loader.weight_utils import (
download_weights_from_hf,
single_files = glob.glob(os.path.join(checkpoint_dir, "*.safetensors"))
for filename in single_files:
with safe_open(filename, framework="pt", device="cpu") as handle:
for checkpoint_key in handle.keys():
source_name = prefix + checkpoint_key
model_name = remap_fn(source_name) if remap_fn is not None else source_name
if model_name is not None:
model_to_ckpt[model_name] = (checkpoint_key, filename)
indexed_keys += 1
return indexed_keys
def _build_mmap_model_map(self, pipeline: nn.Module) -> dict[str, tuple[str, str]]:
"""Resolve safetensors sources to ``model name -> (key, file)``."""
remap_fn = getattr(type(pipeline), "_remap_ckpt_key", None)
model_to_ckpt: dict[str, tuple[str, str]] = {}
indexed_keys = 0
# Prefer the same component sources consumed by DiffusersPipelineLoader.
# Besides avoiding unrelated checkpoint subdirectories, the source
# prefix gives models such as MiniMax-H3 an exact pipeline namespace.
sources = tuple(getattr(pipeline, "weights_sources", ()))
for source in sources:
source_root = source.model_or_path
if not os.path.isdir(source_root):
from vllm.model_executor.model_loader.weight_utils import (
download_weights_from_hf,
)
source_root = download_weights_from_hf(
model_name_or_path=source.model_or_path,
cache_dir=None,
allow_patterns=["*.safetensors", "*.safetensors.index.json"],
revision=source.revision,
subfolder=source.subfolder,
)
checkpoint_dir = (
os.path.join(source_root, source.subfolder) if source.subfolder is not None else source_root
)
indexed_keys += self._add_safetensors_source(
checkpoint_dir,
source.prefix,
remap_fn,
model_to_ckpt,
)
logger.info("model_path %s is not local, downloading from HF", model_path)
model_path = download_weights_from_hf(
model_name_or_path=model_path,
cache_dir=None,
allow_patterns=["*.safetensors", "*.safetensors.index.json"],
)
if not sources:
model_path = self.config.model_path
if not model_path:
raise RuntimeError("No model_path or weights_sources for mmap weight loading")
if not os.path.isdir(model_path):
from vllm.model_executor.model_loader.weight_utils import (
download_weights_from_hf,
)
# Build {checkpoint_key: file_path} from safetensors index
weight_map: dict[str, str] = {}
for idx_file in glob.glob(os.path.join(model_path, "**", "*.safetensors.index.json"), recursive=True):
idx_dir = os.path.dirname(idx_file)
with open(idx_file) as f:
idx = json.load(f)
for key, filename in idx.get("weight_map", {}).items():
weight_map[key] = os.path.join(idx_dir, filename)
logger.info("model_path %s is not local, downloading from HF", model_path)
model_path = download_weights_from_hf(
model_name_or_path=model_path,
cache_dir=None,
allow_patterns=["*.safetensors", "*.safetensors.index.json"],
)
if not weight_map:
single_files = glob.glob(os.path.join(model_path, "**", "model.safetensors"), recursive=True)
for sf in single_files:
from safetensors import safe_open as _safe_open
# Legacy mmap-enabled pipelines may keep the transformer one level
# below model_path, so preserve recursive discovery in this fallback.
import glob
f = _safe_open(sf, framework="pt", device="cpu")
for key in f.keys():
weight_map[key] = sf
logger.info("Found single-file safetensors: %s (%d keys)", sf, len(f.keys()))
checkpoint_dirs = {
os.path.dirname(path)
for pattern in ("*.safetensors.index.json", "*.safetensors")
for path in glob.glob(os.path.join(model_path, "**", pattern), recursive=True)
}
for checkpoint_dir in sorted(checkpoint_dirs):
indexed_keys += self._add_safetensors_source(
checkpoint_dir,
"",
remap_fn,
model_to_ckpt,
)
if not weight_map:
if not model_to_ckpt:
raise RuntimeError(
"Distributed layerwise offload could not find safetensors "
f"weights at {model_path}. Expected *.safetensors.index.json "
"or model.safetensors. Ensure the checkpoint path is correct."
"Distributed layerwise offload could not find compatible "
"safetensors weights in the configured model sources."
)
logger.info(
"Built mmap source map: %d model tensors from %d checkpoint keys",
len(model_to_ckpt),
indexed_keys,
)
return model_to_ckpt
logger.info("Loading DiT weights via mmap (meta → page cache, no RSS): %d keys", len(weight_map))
def _load_weights_via_mmap(self, pipeline: nn.Module, modules) -> None:
"""Load DiT checkpoint tensors as file-backed safetensors views.
When the transformer is created on meta device, this method
replaces meta params with mmap views of the checkpoint files.
The views point to OS page cache shared across ranks. AllGather mode
copies only the rank's 1/dp_size shard to a persistent private buffer;
rank-local mode retains the views and packs one block at a time into
bounded staging storage.
Non-DiT modules (VAE, encoders) are NOT affected — they were
created on CPU with real weights via from_pretrained.
"""
from safetensors import safe_open
model_to_ckpt = self._build_mmap_model_map(pipeline)
logger.info(
"Loading DiT weights via mmap (meta -> shared page cache): %d tensors",
len(model_to_ckpt),
)
# --- Convert DiT modules to meta device ---
# The transformer was created normally (with random weights and
@@ -812,25 +1076,6 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
len(saved_buffers.get(id(dit_module), {})),
)
# Build reverse mapping {model_param_name: (ckpt_key, file_path)} using
# the pipeline's _remap_ckpt_key (if available). This handles the
# model-specific checkpoint key remapping (e.g., Cosmos3's
# layers.0.mlp_moe_gen.gate_proj → transformer.gen_layers.0.mlp.gate_proj).
remap_fn = getattr(type(pipeline), "_remap_ckpt_key", None)
model_to_ckpt: dict[str, tuple[str, str]] = {}
if remap_fn is not None:
for ckpt_key, file_path in weight_map.items():
model_name = remap_fn(ckpt_key)
if model_name is not None:
model_to_ckpt[model_name] = (ckpt_key, file_path)
logger.info(
"Built reverse remap: %d model params from %d checkpoint keys", len(model_to_ckpt), len(weight_map)
)
else:
# No remap function — use checkpoint keys directly
for ckpt_key, file_path in weight_map.items():
model_to_ckpt[ckpt_key] = (ckpt_key, file_path)
# Collect all meta params in DiT modules
dit_ids = set()
for dit_module in modules.dits:
@@ -850,8 +1095,8 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
for block in blocks:
block_module_ids.update(id(m) for m in block.modules())
for name, param in pipeline.named_parameters():
if not (hasattr(param, "is_meta") and param.is_meta):
for name, target in chain(pipeline.named_parameters(), pipeline.named_buffers()):
if not (hasattr(target, "is_meta") and target.is_meta):
continue
# Check if this param is inside a block (will be handled by
@@ -895,13 +1140,17 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
parent = parent[int(part)]
else:
parent = getattr(parent, part)
replacement = torch.nn.Parameter(tensor, requires_grad=param.requires_grad)
self._attach_mmap_param_attrs(name, replacement, param)
parent._parameters[parts[-1]] = replacement
leaf_name = parts[-1]
if leaf_name in parent._parameters:
replacement = torch.nn.Parameter(tensor, requires_grad=target.requires_grad)
self._attach_mmap_param_attrs(name, replacement, target)
parent._parameters[leaf_name] = replacement
else:
parent._buffers[leaf_name] = tensor
loaded += 1
loaded_names.add(name)
logger.info("Mmap weight loading: %d non-block params loaded, %d skipped", loaded, skipped)
logger.info("Mmap weight loading: %d non-block tensors loaded, %d skipped", loaded, skipped)
# Now load DiT block params from safetensors using mmap views.
# Each block param is assigned an mmap view (no RSS).
@@ -927,8 +1176,8 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
# e.g. "transformer.language_model.layers.0" or "transformer.gen_layers.0"
block_full_prefix = f"{dit_name}.{blocks_attr}.{block_idx}"
for bname, bparam in block.named_parameters():
if not (hasattr(bparam, "is_meta") and bparam.is_meta):
for bname, block_target in chain(block.named_parameters(), block.named_buffers()):
if not (hasattr(block_target, "is_meta") and block_target.is_meta):
continue
# Use reverse remap to find checkpoint key
@@ -952,29 +1201,43 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
parent = parent[int(part)]
else:
parent = getattr(parent, part)
replacement = torch.nn.Parameter(tensor, requires_grad=bparam.requires_grad)
self._attach_mmap_param_attrs(full_param_name, replacement, bparam)
parent._parameters[bparts[-1]] = replacement
leaf_name = bparts[-1]
if leaf_name in parent._parameters:
replacement = torch.nn.Parameter(tensor, requires_grad=block_target.requires_grad)
self._attach_mmap_param_attrs(full_param_name, replacement, block_target)
parent._parameters[leaf_name] = replacement
else:
parent._buffers[leaf_name] = tensor
block_loaded += 1
loaded_names.add(full_param_name)
logger.info("Mmap block loading: %d params loaded, %d skipped", block_loaded, skipped)
logger.info("Mmap block loading: %d tensors loaded, %d skipped", block_loaded, skipped)
# Strict validation: check that all meta params were loaded.
# This replaces the validation that AutoWeightsLoader.load_weights()
# performs in the regular load path.
remaining_meta = [
name for name, param in pipeline.named_parameters() if hasattr(param, "is_meta") and param.is_meta
name
for name, tensor in chain(pipeline.named_parameters(), pipeline.named_buffers())
if hasattr(tensor, "is_meta") and tensor.is_meta
]
if remaining_meta:
# Filter out params inside submodules that will be loaded later
# by _load_module_weights_from_mmap (non-block submodules)
dit_param_names = set()
for dit_module in modules.dits:
for pname, _ in dit_module.named_parameters():
dit_param_names.add(pname)
for dit_name, dit_module in zip(modules.dit_names, modules.dits):
for pname, _ in chain(dit_module.named_parameters(), dit_module.named_buffers()):
dit_param_names.add(f"{dit_name}.{pname}")
truly_missing = [n for n in remaining_meta if n in dit_param_names]
if truly_missing:
if getattr(pipeline, "_supports_mmap_loading", None) is True:
raise RuntimeError(
"Explicit mmap loading left "
f"{len(truly_missing)} DiT tensors on the meta device "
f"(first 5: {truly_missing[:5]}). Check that every "
"runtime tensor has a matching weights_sources prefix "
"and checkpoint key."
)
logger.warning(
"Mmap loading: %d params still on meta device after "
"loading (first 5: %s). These may be loaded later by "
@@ -1021,10 +1284,11 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
parent = parent[int(part)]
else:
parent = getattr(parent, part)
# Place on the same device as the module's parameters
# Place on the offloader's target device (not the module's
# current device, which may still be meta at this point).
parent._buffers[parts[-1]] = buf.to(self.device)
# Rank-local hooks collect every block buffer as a CPU source.
# AllGather preserves the existing behavior of restoring these
# generated buffers on the target device before shard packing.
restore_device = torch.device("cpu") if self._using_rank_local_mmap else self.device
parent._buffers[parts[-1]] = buf.to(restore_device)
def _load_module_weights_from_mmap(self, module: nn.Module, dit_name: str, child_name: str) -> None:
"""Load a non-block DiT submodule's weights from safetensors via mmap."""
@@ -1037,8 +1301,8 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
model_to_ckpt = getattr(self, "_mmap_model_to_ckpt", {})
loaded = 0
for pname, param in module.named_parameters():
if not (hasattr(param, "is_meta") and param.is_meta):
for pname, target in chain(module.named_parameters(), module.named_buffers()):
if not (hasattr(target, "is_meta") and target.is_meta):
continue
# Build full model param name and look up in reverse remap
@@ -1065,9 +1329,13 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
parent = parent[int(part)]
else:
parent = getattr(parent, part)
replacement = torch.nn.Parameter(tensor, requires_grad=param.requires_grad)
self._attach_mmap_param_attrs(full_name, replacement, param)
parent._parameters[parts[-1]] = replacement
leaf_name = parts[-1]
if leaf_name in parent._parameters:
replacement = torch.nn.Parameter(tensor, requires_grad=target.requires_grad)
self._attach_mmap_param_attrs(full_name, replacement, target)
parent._parameters[leaf_name] = replacement
else:
parent._buffers[leaf_name] = tensor
loaded += 1
if loaded > 0:
@@ -1265,6 +1533,7 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self.comm_stream,
self.config.pin_cpu_memory,
shared_buffers=[None, None],
rank_local_mmap=self._using_rank_local_mmap,
)
sub_hooks = [last_hook]
for i, block in enumerate(blocks[:-1]):
@@ -1280,6 +1549,7 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self.comm_stream,
self.config.pin_cpu_memory,
shared_buffers=[None, None],
rank_local_mmap=self._using_rank_local_mmap,
)
sub_hooks.append(hook)
@@ -1402,33 +1672,49 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self.config.dlo_resident_layers,
)
# Load weights via mmap for DLO+AllGather.
# TODO(offload): Add a rank-local mmap path for dlo_no_use_allgather.
# It must apply declarative checkpoint-to-runtime adapters before the
# standard TP loader shards each block, and must cover staged encoders
# and VAEs as well as DiT blocks. The current AllGather mmap path is
# deliberately not reused because it changes the weight layout.
# Gate condition MUST match diffusers_loader.py:
# supports_mmap_loading(pipeline) and not _has_online_quant
# When the gate is False, the loader has already loaded weights via
# Select checkpoint mmap independently from the DLO transfer protocol.
# AllGather consumes rank shards copied from mmap. Without AllGather,
# TP1/no-HSDP workers retain the same file-backed tensors and pack only
# the next block into bounded, rank-private pinned staging buffers.
# TP/HSDP rank-local layouts still come from the standard loader.
# The loader and backend share should_select_dlo_mmap(), so this
# decision stays aligned with whether load_weights() was skipped.
# When selection is False, the loader has already loaded weights via
# regular load_weights() + _process_weights_after_loading().
if self.config.dlo_use_allgather and self.dp_size > 1:
_has_online_quant = any(
getattr(getattr(module, "quant_method", None), "uses_meta_device", False)
for module in pipeline.modules()
_has_online_quant = has_online_quantization(pipeline)
if self.config.dlo_use_allgather and _has_online_quant:
raise ValueError(
"Online quantization is incompatible with DLO+AllGather: "
"the sharding + AllGather mechanism flattens weights by "
"dtype, which breaks quantized weight/scale layouts. "
"Please use --dlo-no-use-allgather or disable online "
"quantization."
)
if _has_online_quant:
raise ValueError(
"Online quantization is incompatible with DLO+AllGather: "
"the sharding + AllGather mechanism flattens weights by "
"dtype, which breaks quantized weight/scale layouts. "
"Please use --dlo-no-use-allgather or disable online "
"quantization."
mmap_supported = supports_mmap_loading(pipeline)
self._using_mmap = should_select_dlo_mmap(
mmap_supported=mmap_supported,
dlo_use_allgather=self.config.dlo_use_allgather,
has_online_quant=_has_online_quant,
tensor_parallel_size=self.config.tensor_parallel_size,
use_hsdp=self.config.use_hsdp,
)
self._using_rank_local_mmap = self._using_mmap and self.dp_size <= 1
if self._using_mmap:
self._load_weights_via_mmap(pipeline, modules)
if self._using_rank_local_mmap:
logger.info(
"DLO rank-local mmap storage enabled: checkpoint pages are "
"node-shared; each worker owns only two bounded host staging slots"
)
if supports_mmap_loading(pipeline):
self._load_weights_via_mmap(pipeline, modules)
else:
logger.info("Weights loaded via regular loader — skipping mmap (model does not support mmap)")
elif not mmap_supported:
logger.info("Weights loaded via regular loader - model does not support mmap")
elif not self.config.dlo_use_allgather and (self.config.tensor_parallel_size != 1 or self.config.use_hsdp):
logger.info(
"Rank-local mmap disabled for TP=%d/HSDP=%s; using standard-loader tensors",
self.config.tensor_parallel_size,
self.config.use_hsdp,
)
# Keep VAE/encoders on CPU; move to GPU on-demand via hooks.
# This saves ~4.3 GB HBM per card (VAE 1.3 + encoder 1.1 + sound 1.9)
@@ -1528,6 +1814,7 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self.comm_stream,
self.config.pin_cpu_memory,
shared_buffers=[None, None],
rank_local_mmap=self._using_rank_local_mmap,
)
block_hooks: list[DistributedLayerwiseOffloadHook] = [last_hook]
@@ -1544,6 +1831,7 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self.comm_stream,
self.config.pin_cpu_memory,
shared_buffers=[None, None],
rank_local_mmap=self._using_rank_local_mmap,
)
block_hooks.append(hook)
@@ -1572,11 +1860,15 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self.device,
self.copy_stream,
self.config.pin_cpu_memory,
rank_local_mmap=self._using_rank_local_mmap,
defer_staging=bool(self._all_hook_groups),
)
pipeline._dlo_residency_controller = self
if not self._all_hook_groups:
self.enabled = bool(self._resident_blocks)
if self._using_mmap and not self.enabled:
self._release_mmap_handles()
return
# Unified allocation: 2 shared output buffers + 2 shared shard buffers
@@ -1590,6 +1882,21 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
unified_shard_buffers = None
if self.dp_size > 1:
unified_shard_buffers = self._allocate_shared_shard_buffers(all_hooks)
unified_cpu_staging = None
cpu_staging_events = None
if self._using_rank_local_mmap:
unified_cpu_staging = self._allocate_shared_cpu_staging_buffers(
all_hooks,
self._resident_layer_group,
)
cpu_staging_events = [None, None]
if self._resident_layer_group is not None:
# Resident and streamed layers execute in the same stage and
# reuse the same host slots. Events serialize slot reuse.
self._resident_layer_group._cpu_staging_buffers = [
buffers for buffers in unified_cpu_staging if buffers is not None
]
self._resident_layer_group._cpu_staging_events = cpu_staging_events
# Shared slot-group tracker: _shared_slot_group[slot] = group_id
# that last wrote to that slot. Group-first hooks use this to
@@ -1602,6 +1909,9 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
hook._owns_buffers = False
if unified_shard_buffers is not None:
hook.gpu_shard_buffers = unified_shard_buffers
if unified_cpu_staging is not None and cpu_staging_events is not None:
hook.cpu_staging_buffers = unified_cpu_staging
hook.cpu_staging_events = cpu_staging_events
hook._group_id = group_idx
hook._shared_slot_group = shared_slot_group
@@ -1628,9 +1938,11 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self.enabled = True
# Release mmap file handles — _shard_and_pin has copied all shards,
# params now point to offload placeholders (not mmap views).
self._release_mmap_handles()
if self._using_mmap and not self._using_rank_local_mmap:
# AllGather mode copied each rank's persistent shard, so the source
# mappings are no longer needed. Rank-local mode retains them as
# the node-shared host master until disable().
self._release_mmap_handles()
# Assign GPU buffers to sharded on-demand modules (VAE/encoders).
# Each module gets a dedicated input (shard-sized) and output
@@ -1674,9 +1986,12 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
pass
def disable(self) -> None:
if not self.enabled:
if not self.enabled and not hasattr(self, "_mmap_file_cache"):
return
if self._using_rank_local_mmap:
current_omni_platform.synchronize()
for blocks in self._blocks:
for block in blocks:
remove_distributed_block_hook(block)
@@ -1691,6 +2006,9 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self._all_hook_groups.clear()
self._resident_blocks.clear()
self._resident_layer_group = None
self._release_mmap_handles()
self._using_mmap = False
self._using_rank_local_mmap = False
self.enabled = False
logger.info("Distributed layer-wise offloading disabled")
@@ -1730,6 +2048,41 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
)
return shared_buffers
@staticmethod
def _allocate_shared_cpu_staging_buffers(
hooks: list[DistributedLayerwiseOffloadHook],
resident_group: PinnedResidentLayerGroup | None = None,
) -> list[dict[torch.dtype, torch.Tensor] | None]:
"""Allocate two bounded host slots for rank-local mmap -> device copies."""
max_sizes: dict[torch.dtype, int] = {}
for hook in hooks:
for dtype, metas in hook.metadata.items():
total = sum(meta["numel"] for meta in metas)
max_sizes[dtype] = max(max_sizes.get(dtype, 0), total)
if resident_group is not None:
for state in resident_group._states:
for dtype, metas in state["metadata"].items():
total = sum(meta["numel"] for meta in metas)
max_sizes[dtype] = max(max_sizes.get(dtype, 0), total)
pin_memory = hooks[0].pin_memory if hooks else bool(resident_group and resident_group.pin_memory)
shared_staging: list[dict[torch.dtype, torch.Tensor] | None] = [None, None]
for slot in range(2):
buffers: dict[torch.dtype, torch.Tensor] = {}
for dtype, total_numel in max_sizes.items():
buffer = torch.empty(total_numel, dtype=dtype, device="cpu")
if pin_memory:
buffer = buffer.pin_memory()
buffers[dtype] = buffer
shared_staging[slot] = buffers
logger.info(
"Allocated 2 shared host staging buffers for rank-local mmap (max block size: %s, pinned=%s)",
{str(k): f"{v * _dtype_size(k) / 1024 / 1024:.1f}MB" for k, v in max_sizes.items()},
pin_memory,
)
return shared_staging
@staticmethod
def _allocate_shared_shard_buffers(
hooks: list[DistributedLayerwiseOffloadHook],
+42 -6
View File
@@ -60,15 +60,51 @@ def get_offload_plan(pipeline: nn.Module) -> OffloadPlan | None:
def supports_mmap_loading(pipeline: nn.Module) -> bool:
"""Whether the pipeline supports mmap weight loading.
A pipeline supports mmap if any module defines ``_remap_ckpt_key``,
which maps checkpoint keys to model parameter names. Models without
this method must use the regular ``load_weights()`` path.
A pipeline supports mmap when either:
* a module defines ``_remap_ckpt_key``, which maps checkpoint keys to
model parameter names; or
* the pipeline explicitly opts in with ``_supports_mmap_loading=True``
and declares ``weights_sources``. In that case each source prefix is
used as the model namespace.
The explicit opt-in is important: many pipelines declare
``weights_sources`` but still require arbitrary loader callbacks and must
stay on the regular ``load_weights()`` path.
This check is shared between ``diffusers_loader.py`` (to decide whether
to skip ``load_weights``) and ``DistributedLayerwiseOffloadBackend.enable()``
(to decide whether to call ``_load_weights_via_mmap``), ensuring both
paths use the same gate condition.
"""
return bool(getattr(pipeline, "_supports_mmap_loading", True)) and any(
callable(getattr(type(m), "_remap_ckpt_key", None)) for m in pipeline.modules()
)
declared_support = getattr(pipeline, "_supports_mmap_loading", None)
if declared_support is False:
return False
has_remap = any(callable(getattr(type(m), "_remap_ckpt_key", None)) for m in pipeline.modules())
if has_remap:
return True
return declared_support is True and bool(getattr(pipeline, "weights_sources", ()))
def has_online_quantization(model: nn.Module) -> bool:
"""Whether any module defers online-quantized weights on ``meta``."""
return any(getattr(getattr(module, "quant_method", None), "uses_meta_device", False) for module in model.modules())
def should_select_dlo_mmap(
*,
mmap_supported: bool,
dlo_use_allgather: bool,
has_online_quant: bool,
tensor_parallel_size: int,
use_hsdp: bool,
) -> bool:
"""Whether loader and backend should enter DLO's mmap path.
AllGather preserves its existing fail-closed topology validation inside
the mmap loader. The extra TP/HSDP gate applies to rank-local mmap only.
"""
rank_local_compatible = tensor_parallel_size == 1 and not use_hsdp
return mmap_supported and not has_online_quant and (dlo_use_allgather or rank_local_compatible)