Compare commits

...
Author SHA1 Message Date
Hongsheng Liu 65c86dec44 [Docs] Clarify TP1 DLO mmap support scope
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-16 10:54:24 +08:00
Hongsheng Liu 5ce549bc57 Merge branch 'main' into codex-dlo-host-weight-plan 2026-08-16 00:13:33 +08:00
Hongsheng Liu fa2f5b4571 [Diffusion] Preserve non-DiT loading with DLO plans
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-15 23:57:03 +08:00
Hongsheng Liu 11687f73bd [Diffusion] Rebase DLO plan and generalize lifecycle example
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-15 23:40:24 +08:00
Hongsheng Liu 88614175c0 [Diffusion] Address DLO host-weight review feedback
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-15 23:32:27 +08:00
Hongsheng Liu e2796b6168 [Diffusion] Add loader-owned host-weight plans for DLO
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-15 23:32:27 +08:00
17 changed files with 2009 additions and 615 deletions

No files matched your search

@@ -4,7 +4,7 @@ This document describes distributed layerwise offload (DLO) for diffusion
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.
complete rank-local block sources and avoid an additional collective.
For user-facing commands, see the
[distributed layerwise offloading guide](../../../user_guide/diffusion/offloader/distributed_layerwise_offload.md)
@@ -14,10 +14,20 @@ and the [Cosmos3 recipe](https://github.com/vllm-project/vllm-omni/blob/main/rec
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.
`--dlo-no-use-allgather` path streams complete blocks independently and adds no
DLO weight collective.
Host storage is selected separately from the transfer protocol. The loader can
produce a direct-checkpoint mmap plan for a proven-compatible runtime layout;
otherwise it uses the ordinary loader. Consequently, no-AllGather replicas on
the same node can share immutable checkpoint pages when direct mmap is
selected, while the ordinary-loader fallback still keeps a private runtime
copy per process.
The Phase A shared-mmap support boundary is TP1. TP greater than one is an
ordinary-loader compatibility path: DLO can consume the resulting TP-local
tensors, but those configurations do not use checkpoint mmap and must not be
used to claim shared-mmap host-memory savings.
The compatibility matrix below describes the current implementation. The
unit-level guards are covered, but not every parallelism combination has a
@@ -41,6 +51,36 @@ TP is deliberately not used as DLO's AllGather group. HSDP has its own
parameter-sharding lifecycle and is not allowed to be sharded a second time by
DLO's AllGather path.
### The loader owns host-weight planning
Before it decides whether ordinary weight materialization can be skipped, the
diffusion loader builds one `HostWeightPlan`. A direct-checkpoint mmap plan is
accepted only when preflight proves all of the following:
- every required DiT parameter and persistent buffer has exactly one source;
- runtime names, checkpoint keys, shapes, and dtypes match;
- the runtime topology is TP1 without HSDP or online quantization; and
- every custom loader operation is represented by a loader-owned checkpoint
adapter.
The exact plan object is handed to DLO. The backend does not rescan checkpoint
files, repeat the capability decision, or reconstruct names from its block
topology. If preflight fails, the loader materializes weights normally and DLO
consumes those runtime tensors.
The plan owns only dedicated DiT component sources. If a pipeline also exposes
ordinary sources for a text encoder or another non-DiT component, the loader
still consumes those sources and includes their loaded names in its strict
coverage check. Only the source prefixes covered by the plan skip ordinary
materialization. A source that mixes DiT and non-DiT weights fails closed to
the complete ordinary-loader path because it cannot be skipped safely as a
unit.
This boundary keeps checkpoint semantics out of DLO and avoids model-pipeline
flags such as `_supports_mmap_loading` or parameter attributes for mmap-only
transforms. Model-specific direct-layout knowledge, when required, lives in a
checkpoint adapter beside the ordinary loader.
### AllGather path
With the default `dlo_use_allgather=True`, each rank stores approximately
@@ -61,6 +101,15 @@ Buffers: [current slot] [prefetch slot] [current slot]
The backend uses two shared device buffers, so accelerator weight residency is
bounded by the largest streamed blocks rather than the complete model.
When direct checkpoint mmap is selected, the checkpoint mappings are only the
source used to prepare each rank's persistent shard. They can be closed after
shard preparation. Across the AllGather group, those private shards total
approximately one runtime model copy.
An effective DLO group size of one performs no collective, even when
`dlo_use_allgather=True`; it follows the rank-local transfer path described
below.
When DP is greater than one, the engine can process one request per DP rank in
the same denoising wave. Because AllGather is a collective, all participating
requests must take the same execution path at every denoising step.
@@ -68,9 +117,20 @@ 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 blocks using H2D copies only. The host backing may be
either a loader-approved checkpoint mapping or ordinary runtime tensors.
For direct mmap, each process retains immutable safetensors views and uses two
bounded pinned host staging slots. Processes on the same node that map the same
files share physical checkpoint pages through the OS page cache. This removes
the persistent private full-model copy per pure-DP process, but each process
still packs and transfers every complete block. Sharing is node-local; each
node has its own page cache.
When direct mmap preflight fails, the regular model loader remains responsible
for preparing each rank's weights, including TP-local tensors or HSDP-managed
parameters. In that fallback, each pure-DP process keeps a private full runtime
copy.
This mode means:
@@ -80,21 +140,18 @@ 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 share one checkpoint-backed copy per node when direct
mmap is selected; the ordinary-loader fallback keeps one private runtime
copy per rank.
- 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.
## 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. Compatible TP1 replicas can share checkpoint pages on each node; fallback runtime tensors remain private. |
| **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. |
| **TP > 1** | Outside the Phase A shared-mmap support scope. The loader falls back before mutation, preserves TP-local layouts, and DLO may apply DP/SP host sharding to those ordinary runtime tensors. | Outside the Phase A shared-mmap support scope. The ordinary TP-aware loader produces rank-local tensors, which DLO streams without an additional weight collective; DP replicas retain private runtime storage. |
| **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. |
### Combined dimensions
@@ -122,11 +179,15 @@ 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.
Direct checkpoint mmap can back either transfer path. It is currently limited
to proven TP1, non-HSDP, non-online-quantized layouts. Other layouts use the
ordinary loader. Online quantization remains incompatible with DLO AllGather;
use `--dlo-no-use-allgather` or disable online quantization.
A normalized runtime mmap cache, built through the ordinary loader, is the
proposed general mechanism for sharing transformed TP or quantized layouts.
That cache and its publication/lifecycle protocol are intentionally outside
this phase; see [RFC #6195](https://github.com/vllm-project/vllm-omni/issues/6195).
## Validation coverage
@@ -134,15 +195,75 @@ Current source-level validation includes:
- HSDP + DLO + AllGather rejection;
- HSDP + DLO without AllGather acceptance at configuration level;
- TP rejection in the DLO+AllGather mmap path;
- loader preflight fallback for TP, HSDP, online quantization, unknown custom
loaders, missing keys, and shape/dtype mismatches;
- exact loader-to-backend plan transfer and ordinary-loader fallback;
- rank-local mmap source retention, bounded two-slot staging, and adapter
transforms without parameter-side flags;
- resident-layer requests requiring no-AllGather;
- DP request-wave validation for denoising-step compatibility;
- sharding, double-buffer, AllGather-size, and heterogeneous-block regression
tests.
The highest-value missing coverage is end-to-end numerical comparison against
ordinary layerwise offload for DP+SP, TP+no-AllGather, and HSDP+SP+no-AllGather
on the target CUDA/NCCL or CANN/HCCL hardware.
### B300 parallel-topology smoke matrix
A four-GPU B300 smoke test covered MiniMax-H3 FL2VA with the same prompt, seed,
CUDNN attention backend, 256x256 output, two denoising steps, and
`dlo_resident_layers=0`. The TP2 rows used DiT DP2xTP2 with the text encoder and
VAEs at TP1. They validate the ordinary-loader fallback only, not direct mmap
or shared-mmap host-memory savings.
| Configuration | Result | Warm E2E | Peak device memory | Host PSS |
|---|---:|---:|---:|---:|
| DP4xTP1 AllGather | Passed, 4 concurrent requests | 2.87 s / 4 requests | 13.84 GiB | 211.99 GiB |
| DP4xTP1 no-AllGather | Passed, 1 request | 15.02 s | 13.23 GiB | 187.77 GiB |
| DP2xTP2 AllGather | Passed, 2 concurrent requests | 4.16 s / 2 requests | 12.50 GiB | 211.97 GiB |
| DP2xTP2 no-AllGather | Passed, 1 request | 3.51 s | 11.88 GiB | 314.01 GiB |
Within each topology, the AllGather and no-AllGather video and audio outputs
were byte-identical. All four runs completed without an `ERROR` or traceback
and released their device allocations. For DP4xTP1, no-AllGather direct mmap
reduced total PSS by 24.22 GiB (11.4%) and `Private_Dirty` from 211.33 to
125.32 GiB (40.7%) relative to AllGather. For DP2xTP2, preflight selected the
ordinary loader as designed; no-AllGather PSS was 314.01 GiB, about 48% above
AllGather, because DP replicas did not share checkpoint-backed runtime
weights. This is a functional and memory smoke test, not a production-quality
performance or output-quality benchmark.
### Host-memory measurement
A two-worker MiniMax-H3 FL2VA measurement on one L20X node compared the
ordinary-loader fallback with direct mmap. Both runs used
DP=2, TP=1, no DLO AllGather, BF16 weights, two denoising steps, and a
256x256 four-second request. The ordinary-loader workers were sampled after
initialization. The mmap workers were sampled after one completed request, so
the checkpoint working set had been faulted into the page cache; this is the
more conservative point for mmap.
The values below come from `/proc/<worker>/smaps_rollup` and include the whole
worker, not only the DiT. The stable rank-to-rank difference comes from other
pipeline components, so each worker should be compared with the same worker in
the other storage mode.
| Worker | Ordinary RSS | mmap RSS | Ordinary PSS | mmap PSS | PSS reduction |
|---|---:|---:|---:|---:|---:|
| DP worker 0 | 168.27 GiB | 132.76 GiB | 167.84 GiB | 101.43 GiB | 66.40 GiB |
| DP worker 1 | 116.19 GiB | 79.97 GiB | 115.73 GiB | 48.64 GiB | 67.09 GiB |
| **Two-worker total** | — | — | **283.56 GiB** | **150.08 GiB** | **133.48 GiB (47.1%)** |
The direct-mmap workers each reported 62.45 GiB `Shared_Clean` but only
31.20 GiB `Pss_File`, which is the proportional charge expected when the same
resident checkpoint pages are mapped by two workers. `Private_Dirty` also fell
from 167.53 to 70.24 GiB for worker 0 and from 115.40 to 17.44 GiB for worker
1, a reduction of about 97–98 GiB per worker. RSS understates this benefit
because it counts a shared physical page in every process that maps it; summed
PSS is the appropriate node-memory comparison.
The highest-value missing coverage is broader end-to-end numerical and
lifecycle comparison against ordinary layerwise offload for DP+SP,
HSDP+SP+no-AllGather, and TP greater than one across additional models and
target CUDA/NCCL or CANN/HCCL hardware. That broader TP coverage does not
change the Phase A direct-mmap TP1 support boundary.
## Recommendations
@@ -150,7 +271,9 @@ 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 independent replica execution is required. TP1
direct-mmap deployments can share checkpoint pages per node; other layouts
retain the ordinary loader's private host memory behavior and are outside
the Phase A shared-mmap support scope.
- Prefer **HSDP alone** for production HSDP deployments until the combined
HSDP + DLO no-AllGather path has broader end-to-end coverage.
@@ -3,7 +3,9 @@
Distributed layerwise offloading (DLO) extends block streaming to multi-device
deployments. With AllGather enabled, each rank stores roughly `1 / dp_size` of
the host weights and reconstructs each layer at runtime. Without AllGather,
each rank streams its standard-loader rank-local weights independently.
each rank streams a complete block independently. Compatible TP1 deployments
can share checkpoint-backed host pages among processes on the same node;
otherwise DLO streams the ordinary loader's rank-local tensors.
See the [DLO feature design](../../../design/feature/offloader/distributed_layerwise_offload.md)
for the implementation contract and compatibility matrix.
@@ -59,21 +61,48 @@ omni = Omni(
| `--enable-distributed-layerwise-offload` | Enable DLO | `false` |
| `--data-parallel-size N` | DP ranks and AllGather weight-sharding group | `1` |
| `--dlo-use-allgather` | Shard host weights and reconstruct with AllGather | `true` |
| `--dlo-no-use-allgather` | Keep standard-loader rank-local weights | `false` |
| `--dlo-no-use-allgather` | Stream complete rank-local blocks without a DLO weight collective | `false` |
| `--dlo-resident-layers N` | Keep N leading main-DiT blocks on device; requires no-AllGather and model-declared resident paths | `0` |
## mmap weight loading
## Host-weight loading
The DLO plus AllGather path:
The diffusion loader chooses host storage before DLO is enabled. It first
attempts to build a complete, validated direct-checkpoint mmap plan. If names,
coverage, shape, dtype, topology, or loader-callback compatibility cannot be
proven, it runs the ordinary model loader instead. DLO consumes that result and
does not make a second checkpoint-compatibility decision.
The shared-mmap optimization in this phase is supported only with TP1. TP
greater than one falls back before model mutation to ordinary TP-aware loading.
DLO may still consume those TP-local tensors, but this is a compatibility path:
it does not share checkpoint-backed runtime weights across DP replicas and
provides no shared-mmap host-memory guarantee.
The mmap plan skips only dedicated DiT weight sources. Other component sources,
such as a text encoder loaded through the shared diffusion loader, continue to
use their ordinary component loader. A checkpoint source that mixes DiT and
non-DiT weights falls back completely rather than leaving an unplanned
component uninitialized.
With direct checkpoint mmap, the loader:
1. saves non-persistent buffers such as RoPE frequencies;
2. moves the normally created transformer to the meta device;
3. loads checkpoint tensors as mmap views backed by the shared OS page cache;
4. calls model-specific `post_load_weights()` conversions; and
5. restores the saved non-persistent buffers.
4. applies any loader-owned bounded layout adapters while packing blocks;
5. restores saved non-persistent buffers; and
6. preserves `post_load_weights()` and `validate_loaded_weights()` lifecycle
hooks.
This avoids one full checkpoint RSS copy per rank and does not require
model-specific loading code.
For AllGather with a group larger than one, each process copies only its
persistent shard and then releases the source mapping. For no-AllGather, each
process keeps the mapping open and packs complete blocks through two bounded
pinned staging slots. Processes mapping the same files on one node share the
immutable pages; no-AllGather still performs a complete-block H2D copy in each
process.
When the effective DLO group size is one, `dlo_use_allgather=True` does not
perform a collective and uses the same rank-local transfer behavior.
## Declarative topology
@@ -103,16 +132,22 @@ must enter each collective.
## Limitations
- Online FP8 quantization is rejected with the DLO plus AllGather mmap path.
Use `--dlo-no-use-allgather` or disable online quantization.
- Tensor parallel size greater than one is rejected in the mmap path because
it bypasses TP-aware loader callbacks. The no-AllGather path retains the
standard loader and remains experimental with TP.
- Direct checkpoint mmap currently requires TP1. TP greater than one is
outside the Phase A shared-mmap support scope and falls back before model
mutation to the ordinary TP-aware loader. DLO can stream that runtime layout,
but it provides no shared-mmap host-memory benefit or guarantee.
- HSDP plus AllGather is rejected to avoid double sharding. HSDP without
AllGather has limited end-to-end validation.
- Online quantization uses the ordinary loader with no-AllGather. It remains
incompatible with DLO AllGather.
- Resident leading layers require `--dlo-no-use-allgather` and a model
`OffloadPlan` that declares eligible `resident_dit_paths`.
- DP concurrency requires an explicit, identical inference-step count.
Sharing transformed TP or quantized runtime layouts through a normalized mmap
cache is a follow-up design in
[RFC #6195](https://github.com/vllm-project/vllm-omni/issues/6195), not part of
the direct-checkpoint path.
See the [Cosmos3 DistOffload recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/cosmos3/Cosmos3-DistOffload.md)
for an end-to-end example.
@@ -1,21 +1,32 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Reproduce MiniMax-H3 request-mode and DLO/DP2 lifecycle behavior.
"""Validate MiniMax-H3 request-mode and DLO lifecycle behavior.
The DLO modes accept any positive data-parallel size. With AllGather enabled,
the script submits one complete DP wave and verifies recovery after an invalid
wave. It also checks that every mode shuts down all worker processes cleanly.
Examples:
VLLM_WORKER_MULTIPROC_METHOD=spawn \
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=14400 \
VLLM_OMNI_DLO_DP_WAVE_TIMEOUT=600 \
CUDA_VISIBLE_DEVICES=0,1,2,3 \
python examples/offline_inference/minimax_h3/dlo_lifecycle.py \
--model /path/to/MiniMax-H3/FL2VA --mode dlo --dp-size 4
# 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
python examples/offline_inference/minimax_h3/dlo_lifecycle.py \
--model /path/to/MiniMax-H3/FL2VA --mode dlo-no-allgather --dp-size 2
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 request
python examples/offline_inference/minimax_h3/dlo_lifecycle.py \
--model /path/to/MiniMax-H3/FL2VA --mode request --tp-size 2
"""
from __future__ import annotations
@@ -40,14 +51,33 @@ DEFAULT_PROMPTS = (
)
def positive_int(value: str) -> int:
parsed = int(value)
if parsed < 1:
raise argparse.ArgumentTypeError("must be a positive integer")
return parsed
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--model", required=True, help="Path to MiniMax-H3/FL2VA")
parser.add_argument(
"--mode",
choices=("dlo-dp2", "request"),
choices=("dlo", "dlo-no-allgather", "request"),
required=True,
help="DLO AllGather DP2 or ordinary non-DLO TP2 request mode",
help="DLO (with or without AllGather) or ordinary non-DLO request mode",
)
parser.add_argument(
"--dp-size",
type=positive_int,
default=2,
help="DLO data-parallel size (for example 1, 2, 4, or 8; default: 2)",
)
parser.add_argument(
"--tp-size",
type=positive_int,
default=2,
help="Tensor-parallel size used only by request mode (default: 2)",
)
parser.add_argument("--steps", type=int, default=2)
parser.add_argument("--duration", type=float, default=5.0)
@@ -60,10 +90,11 @@ def parse_args() -> argparse.Namespace:
def engine_kwargs(args: argparse.Namespace) -> dict[str, Any]:
is_dlo = args.mode in {"dlo", "dlo-no-allgather"}
common: dict[str, Any] = {
"model": args.model,
"trust_remote_code": True,
"num_gpus": 2,
"num_gpus": args.dp_size if is_dlo else args.tp_size,
"usp": 1,
"ring": 1,
"vae_parallel_mode": "tile",
@@ -74,22 +105,22 @@ 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 is_dlo:
common.update(
tensor_parallel_size=1,
data_parallel_size=2,
data_parallel_size=args.dp_size,
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",
dlo_resident_layers=0,
)
else:
common.update(
tensor_parallel_size=2,
tensor_parallel_size=args.tp_size,
data_parallel_size=1,
text_encoder_tp_size=2,
vae_patch_parallel_size=2,
text_encoder_tp_size=args.tp_size,
vae_patch_parallel_size=args.tp_size,
enable_distributed_layerwise_offload=False,
)
return common
@@ -172,7 +203,8 @@ async def run(args: argparse.Namespace) -> dict[str, Any]:
"engine_kwargs": engine_kwargs(args),
}
try:
if args.mode == "dlo-dp2":
uses_dp_wave = args.mode == "dlo" and args.dp_size > 1
if uses_dp_wave:
started = time.perf_counter()
asymmetric = await asyncio.gather(
generate_one(
@@ -182,12 +214,15 @@ async def run(args: argparse.Namespace) -> dict[str, Any]:
prompt="",
seed=1001,
),
generate_one(
engine,
args,
request_id="invalid-peer",
prompt=DEFAULT_PROMPTS[0],
seed=1002,
*(
generate_one(
engine,
args,
request_id=f"invalid-peer-{index}",
prompt=DEFAULT_PROMPTS[index % len(DEFAULT_PROMPTS)],
seed=1001 + index,
)
for index in range(1, args.dp_size)
),
return_exceptions=True,
)
@@ -195,18 +230,18 @@ async def run(args: argparse.Namespace) -> dict[str, Any]:
summary["asymmetric_errors"] = [
f"{type(result).__name__}: {result}" for result in asymmetric if isinstance(result, BaseException)
]
if len(summary["asymmetric_errors"]) != 2:
raise RuntimeError("Both requests in the asymmetric DP wave must fail before dispatch")
if len(summary["asymmetric_errors"]) != args.dp_size:
raise RuntimeError("Every request in the asymmetric DP wave must fail before dispatch")
started = time.perf_counter()
request_count = 2 if args.mode == "dlo-dp2" else 1
request_count = args.dp_size if uses_dp_wave else 1
outputs = await asyncio.gather(
*(
generate_one(
engine,
args,
request_id=f"recovery-{index}",
prompt=DEFAULT_PROMPTS[index],
prompt=DEFAULT_PROMPTS[index % len(DEFAULT_PROMPTS)],
seed=2000 + index,
)
for index in range(request_count)
@@ -16,6 +16,11 @@ from vllm.config.load import LoadConfig
from vllm_omni.diffusion.config import get_current_diffusion_config, get_current_diffusion_config_or_none
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.model_loader.host_weight_plan import (
HostWeightPlan,
HostWeightPlanResult,
TensorBinding,
)
from vllm_omni.diffusion.models.helios import HeliosPipeline
from vllm_omni.diffusion.registry import initialize_model
@@ -258,6 +263,137 @@ def test_load_model_custom_pipeline_sets_current_diffusion_config(monkeypatch):
assert get_current_diffusion_config_or_none() is None
def test_dlo_transfers_loader_plan_and_skips_ordinary_weight_loading(monkeypatch):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
od_config = SimpleNamespace(
dtype=torch.float32,
parallel_config=SimpleNamespace(use_hsdp=False, tensor_parallel_size=1),
quantization_config=None,
enable_distributed_layerwise_offload=True,
dlo_use_allgather=False,
model="unused",
)
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
model = nn.Module()
model.transformer = nn.Linear(2, 2, bias=False)
plan = HostWeightPlan(
backing_kind="checkpoint_mmap",
bindings={},
)
loaded_ordinary_weights = False
def load_weights(_model):
nonlocal loaded_ordinary_weights
loaded_ordinary_weights = True
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
loader.load_weights = load_weights # type: ignore[method-assign]
loader._apply_skip_softmax_calibration = lambda _model: None # type: ignore[method-assign]
monkeypatch.setattr(
loader_mod,
"build_checkpoint_mmap_plan",
lambda *_args, **_kwargs: HostWeightPlanResult(plan),
)
assert loader.load_model(load_device="cpu") is model
assert not loaded_ordinary_weights
assert loader.take_host_weight_plan() is plan
assert loader.take_host_weight_plan() is None
def test_dlo_plan_loads_component_sources_outside_planned_dit(monkeypatch):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
od_config = SimpleNamespace(
dtype=torch.float32,
parallel_config=SimpleNamespace(use_hsdp=False, tensor_parallel_size=1),
quantization_config=None,
enable_distributed_layerwise_offload=True,
dlo_use_allgather=False,
model="unused",
)
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
class MixedSourceModel(nn.Module):
def __init__(self):
super().__init__()
self.transformer = nn.Linear(2, 2, bias=False)
self.text_encoder = nn.Linear(2, 2, bias=False)
self.weights_sources = (
DiffusersPipelineLoader.ComponentSource("unused", None, None, prefix="transformer."),
DiffusersPipelineLoader.ComponentSource("unused", None, None, prefix="text_encoder."),
)
self.loaded_weight_names: list[str] = []
def load_weights(self, weights):
self.loaded_weight_names = [name for name, _ in weights]
return set(self.loaded_weight_names)
model = MixedSourceModel()
plan = HostWeightPlan(
backing_kind="checkpoint_mmap",
bindings={
"transformer.weight": TensorBinding(
checkpoint_key="weight",
file_path="unused",
)
},
planned_source_prefixes=frozenset({"transformer."}),
)
requested_prefixes: list[str] = []
def get_weights(source, model=None):
del model
requested_prefixes.append(source.prefix)
yield source.prefix + "weight", torch.ones(2, 2)
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
loader._get_weights_iterator = get_weights # type: ignore[method-assign]
loader._apply_skip_softmax_calibration = lambda _model: None # type: ignore[method-assign]
monkeypatch.setattr(
loader_mod,
"build_checkpoint_mmap_plan",
lambda *_args, **_kwargs: HostWeightPlanResult(plan),
)
assert loader.load_model(load_device="cpu") is model
assert requested_prefixes == ["text_encoder."]
assert model.loaded_weight_names == ["text_encoder.weight"]
assert loader.take_host_weight_plan() is plan
def test_dlo_plan_fallback_runs_ordinary_loader(monkeypatch):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
od_config = SimpleNamespace(
dtype=torch.float32,
parallel_config=SimpleNamespace(use_hsdp=False, tensor_parallel_size=1),
quantization_config=None,
enable_distributed_layerwise_offload=True,
dlo_use_allgather=False,
model="unused",
)
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
model = nn.Module()
model.transformer = nn.Linear(2, 2, bias=False)
calls: list[str] = []
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
loader.load_weights = lambda _model: calls.append("load") # type: ignore[method-assign]
loader._process_weights_after_loading = lambda *_args: calls.append("process") # type: ignore[method-assign]
loader._apply_skip_softmax_calibration = lambda _model: None # type: ignore[method-assign]
monkeypatch.setattr(
loader_mod,
"build_checkpoint_mmap_plan",
lambda *_args, **_kwargs: HostWeightPlanResult(None, "not direct-compatible"),
)
assert loader.load_model(load_device="cpu") is model
assert calls == ["load", "process"]
assert loader.take_host_weight_plan() is None
def test_hsdp_processes_quantized_weights_before_sharding(mocker):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
from vllm_omni.diffusion.offloader.module_collector import PipelineModules
@@ -191,6 +191,49 @@ def test_model_load_weights_transforms_before_calling_vllm_loader():
]
def test_loader_adapter_declares_equivalent_direct_mmap_transform(monkeypatch):
from vllm_omni.diffusion.model_loader.checkpoint_adapters import (
get_direct_mmap_adapter,
)
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",
)
transformer = object.__new__(h3.MiniMaxH3DiTModel)
nn.Module.__init__(transformer)
transformer.blocks = nn.ModuleList([nn.Module()])
transformer.blocks[0].attn = attention
pipeline = nn.Module()
pipeline.transformer = transformer
adapter = get_direct_mmap_adapter(pipeline)
assert adapter is not None
policy = adapter.policy_for(
"transformer.blocks.0.attn.qkv_proj.weight",
attention.qkv_proj.weight,
)
assert policy is not None
assert policy.allow_custom_loader
assert policy.transform is not None
checkpoint_weight = torch.arange(6, dtype=torch.float32).reshape(6, 1)
assert policy.transform(checkpoint_weight)[:, 0].tolist() == [0, 3, 1, 4, 2, 5]
assert not hasattr(attention.qkv_proj.weight, "mmap_weight_transform")
def test_pipeline_resolves_transformer_component_quant_config():
from vllm_omni.diffusion.models.minimax_h3.pipeline_minimax_h3 import (
_resolve_component_quant_config,
@@ -18,6 +18,11 @@ from torch import nn
from torch.distributed.tensor import DeviceMesh, DTensor, Replicate
import vllm_omni.diffusion.offloader.distributed_layerwise_backend as dist_backend_module
from vllm_omni.diffusion.model_loader.host_weight_plan import (
HostWeightPlan,
TensorBinding,
build_checkpoint_mmap_plan,
)
from vllm_omni.diffusion.offloader.base import OffloadConfig, OffloadStrategy
from vllm_omni.diffusion.offloader.block_discovery import (
get_blocks_attr_names,
@@ -30,7 +35,10 @@ 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,
)
from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
@@ -48,6 +56,9 @@ class DummyEvent:
def record(self, _stream) -> None:
return None
def synchronize(self) -> None:
return None
@contextmanager
def dummy_stream(_stream):
@@ -351,8 +362,9 @@ class TestDistributedLayerwiseOffloadHook:
block.weight = nn.Parameter(torch.zeros(4, dtype=torch.float32))
next_block = nn.Module()
next_block.weight = nn.Parameter(torch.arange(4, dtype=torch.float32))
next_block.weight.mmap_weight_transform = lambda tensor: tensor.flip(0)
next_block.weight.mmap_weight_transform_pending = True
def transform(tensor):
return tensor.flip(0)
hook = DistributedLayerwiseOffloadHook(
next_block=next_block,
@@ -361,17 +373,17 @@ class TestDistributedLayerwiseOffloadHook:
dp_size=2,
rank=0,
pin_memory=False,
tensor_transforms={id(next_block.weight): transform},
)
hook.initialize_hook(block)
assert torch.equal(hook.cpu_shards[torch.float32], torch.tensor([3.0, 2.0]))
def test_regular_loader_weight_does_not_reapply_mmap_transform(self, dist_group, patched_offload_runtime):
def test_transform_is_not_applied_without_a_loader_plan(self, dist_group, patched_offload_runtime):
block = nn.Module()
block.weight = nn.Parameter(torch.zeros(4, dtype=torch.float32))
next_block = nn.Module()
next_block.weight = nn.Parameter(torch.arange(4, dtype=torch.float32))
next_block.weight.mmap_weight_transform = lambda tensor: tensor.flip(0)
hook = DistributedLayerwiseOffloadHook(
next_block=next_block,
@@ -385,6 +397,82 @@ 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()
def transform(tensor):
return tensor.t()
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,
tensor_transforms={id(next_block.weight): transform},
)
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([[0.0, 2.0], [1.0, 3.0]]),
)
assert next_block.weight.stride() == (1, 2)
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):
@@ -439,23 +527,41 @@ class TestPinnedResidentLayerGroup:
assert block.weight.stride() == expected_stride
group.offload()
def test_mmap_loader_attrs_survive_to_empty_parameter_replacement(self):
module = nn.Linear(2, 2, bias=False)
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())
def transform(tensor):
return tensor.flip(0)
group = PinnedResidentLayerGroup(
blocks,
device=torch.device("cpu"),
copy_stream=DummyStream(),
pin_memory=False,
rank_local_mmap=True,
)
module.weight.mmap_weight_transform = transform
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
backend = object.__new__(DistributedLayerwiseOffloadBackend)
backend._remember_mmap_param_attrs(module)
module.to_empty(device="meta")
group.load()
for block, weight in zip(blocks, expected):
assert torch.equal(block.weight, weight)
replacement = nn.Parameter(torch.arange(4).reshape(2, 2).float())
backend._attach_mmap_param_attrs("weight", replacement, module.weight)
blocks[0].weight.data.zero_()
group.offload()
group.load()
for block, weight in zip(blocks, expected):
assert torch.equal(block.weight, weight)
assert replacement.mmap_weight_transform is transform
assert replacement.mmap_weight_transform_pending is True
group.offload()
assert all(block.weight.numel() == 0 for block in blocks)
def test_shared_allgather_output_is_narrowed_to_current_block(
self, dist_group, patched_offload_runtime, monkeypatch
@@ -598,7 +704,6 @@ class TestMmapWeightLoading:
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
model_path=str(tmp_path),
),
torch.device("cpu"),
)
@@ -606,13 +711,81 @@ class TestMmapWeightLoading:
dits=[pipeline.transformer],
dit_names=["transformer"],
)
plan = HostWeightPlan(
backing_kind="checkpoint_mmap",
bindings={
name: TensorBinding(
checkpoint_key=name,
file_path=str(weight_file),
)
for name in weights
},
)
backend._load_weights_via_mmap(pipeline, modules)
backend._load_weights_via_mmap(pipeline, modules, plan)
assert pipeline.transformer.post_load_calls == 1
assert pipeline.transformer.time_embedder.weight.dtype == torch.float32
assert pipeline.transformer.blocks[0].weight.dtype == torch.bfloat16
def test_allgather_flag_with_group_size_one_uses_rank_local_mmap(
self,
tmp_path,
patched_offload_runtime,
):
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):
def __init__(self):
super().__init__()
self.transformer = Transformer()
@staticmethod
def _remap_ckpt_key(key):
return key
pipeline = Pipeline()
weights = {name: torch.ones_like(param) for name, param in pipeline.named_parameters()}
checkpoint_file = tmp_path / "model.safetensors"
save_file(weights, str(checkpoint_file))
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=(),
model_path=str(tmp_path),
tensor_parallel_size=1,
use_hsdp=False,
online_quantization=False,
)
assert result.plan is not None
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
dp_size=1,
dlo_use_allgather=True,
),
torch.device("cpu"),
host_weight_plan=result.plan,
)
backend.enable(pipeline)
assert backend._using_rank_local_mmap
assert backend.dp_group is None
assert all(hook.rank_local_mmap for group in backend._all_hook_groups for hook in group)
backend.disable()
class TestGetBlocksFromDit:
def test_get_blocks_from_dit_single_block_attr(self):
@@ -878,23 +1051,123 @@ class TestOffloadPlan:
class TestMmapValidation:
"""Tests for mmap loader validation: TP rejection, strict check, validate_loaded_weights."""
"""Tests for loader preflight and backend plan realization."""
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
from types import SimpleNamespace
from safetensors.torch import save_file
from vllm_omni.diffusion.offloader.base import OffloadConfig, OffloadStrategy
# Mock TP world size = 2
monkeypatch.setattr(
"vllm.distributed.parallel_state.get_tensor_model_parallel_world_size",
lambda: 2,
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).reshape(1, 4)},
str(checkpoint_file),
)
class Pipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = nn.Module()
self.transformer.blocks = nn.ModuleList([nn.Linear(4, 1, bias=False)])
self.weights_sources = [
SimpleNamespace(
model_or_path=str(source_root),
subfolder="transformer",
revision=None,
prefix="transformer.",
)
]
pipeline = Pipeline()
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=tuple(pipeline.weights_sources),
model_path=None,
tensor_parallel_size=1,
use_hsdp=False,
online_quantization=False,
)
assert result.fallback_reason is None
assert result.plan is not None
assert result.plan.bindings["transformer.blocks.0.weight"] == TensorBinding(
checkpoint_key="blocks.0.weight",
file_path=str(checkpoint_file),
)
assert result.plan.planned_source_prefixes == frozenset({"transformer."})
def test_non_dedicated_component_source_falls_back_before_discovery(self):
pipeline = nn.Module()
pipeline.transformer = nn.Linear(2, 2, bias=False)
sources = (
SimpleNamespace(
model_or_path="unused",
subfolder=None,
revision=None,
prefix="",
),
)
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=sources,
model_path=None,
tensor_parallel_size=1,
use_hsdp=False,
online_quantization=False,
)
assert result.plan is None
assert "dedicated component weight source" in result.fallback_reason
def test_preflight_falls_back_before_mutation_for_missing_model_tensors(self, tmp_path):
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):
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()
original_weight = pipeline.transformer.blocks[0].weight
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=tuple(pipeline.weights_sources),
model_path=None,
tensor_parallel_size=1,
use_hsdp=False,
online_quantization=False,
)
assert result.plan is None
assert "1 required DiT tensors" in result.fallback_reason
assert pipeline.transformer.blocks[0].weight is original_weight
assert not original_weight.is_meta
def test_tp_falls_back_before_checkpoint_discovery(self):
class SimpleModel(nn.Module):
_layerwise_offload_blocks_attrs = ["blocks"]
@@ -912,41 +1185,118 @@ class TestMmapValidation:
return key
pipeline = SimplePipeline()
weights = {name: torch.ones(p.shape, dtype=p.dtype) for name, p in pipeline.named_parameters() if not p.is_meta}
save_file(weights, str(tmp_path / "model.safetensors"))
(tmp_path / "model.safetensors.index.json").write_text(
json.dumps({"weight_map": {k: "model.safetensors" for k in weights}})
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=(),
model_path="unused",
tensor_parallel_size=2,
use_hsdp=False,
online_quantization=False,
)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
model_path=str(tmp_path),
),
torch.device("cpu"),
)
modules = SimpleNamespace(dits=[pipeline.transformer], dit_names=["transformer"])
assert result.plan is None
assert result.fallback_reason == "TP=2 requires the ordinary loader"
with pytest.raises(ValueError, match="Tensor Parallel"):
backend._load_weights_via_mmap(pipeline, modules)
@pytest.mark.parametrize(
("use_hsdp", "online_quantization", "expected_reason"),
[
(True, False, "HSDP requires the ordinary loader"),
(False, True, "online quantization requires the ordinary loader"),
],
)
def test_runtime_layouts_that_need_loader_callbacks_fall_back(
self,
use_hsdp,
online_quantization,
expected_reason,
):
pipeline = nn.Module()
pipeline.transformer = nn.Linear(2, 2, bias=False)
def test_tp_allowed_when_tp_world_size_eq_1(self, tmp_path, patched_offload_runtime, monkeypatch):
"""DLO+AllGather should work when TP world size = 1, even if params
have custom weight_loader attributes (e.g. QKVParallelLinear at TP=1)."""
import json
from types import SimpleNamespace
from safetensors.torch import save_file
from vllm_omni.diffusion.offloader.base import OffloadConfig, OffloadStrategy
# Mock TP world size = 1 (default)
monkeypatch.setattr(
"vllm.distributed.parallel_state.get_tensor_model_parallel_world_size",
lambda: 1,
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=(),
model_path="unused",
tensor_parallel_size=1,
use_hsdp=use_hsdp,
online_quantization=online_quantization,
)
assert result.plan is None
assert result.fallback_reason == expected_reason
@pytest.mark.parametrize(
("checkpoint_tensor", "expected_reason"),
[
(torch.ones((3, 2), dtype=torch.float32), "shape mismatch"),
(torch.ones((2, 2), dtype=torch.bfloat16), "dtype mismatch"),
],
)
def test_source_metadata_mismatch_falls_back(self, tmp_path, checkpoint_tensor, expected_reason):
checkpoint_file = tmp_path / "model.safetensors"
save_file({"transformer.weight": checkpoint_tensor}, str(checkpoint_file))
class Pipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = nn.Linear(2, 2, bias=False)
@staticmethod
def _remap_ckpt_key(key):
return key
pipeline = Pipeline()
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=(),
model_path=str(tmp_path),
tensor_parallel_size=1,
use_hsdp=False,
online_quantization=False,
)
assert result.plan is None
assert expected_reason in result.fallback_reason
def test_non_persistent_buffers_do_not_need_checkpoint_bindings(self, tmp_path):
class Transformer(nn.Module):
def __init__(self):
super().__init__()
self.weight = nn.Parameter(torch.ones(2, 2))
self.register_buffer("derived", torch.ones(2), persistent=False)
checkpoint_file = tmp_path / "model.safetensors"
save_file({"transformer.weight": torch.ones(2, 2)}, str(checkpoint_file))
class Pipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = Transformer()
@staticmethod
def _remap_ckpt_key(key):
return key
pipeline = Pipeline()
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=(),
model_path=str(tmp_path),
tensor_parallel_size=1,
use_hsdp=False,
online_quantization=False,
)
assert result.plan is not None
assert set(result.plan.bindings) == {"transformer.weight"}
def test_unadapted_custom_weight_loader_falls_back_at_tp1(self, tmp_path):
def custom_weight_loader(param, weight):
param.data.copy_(weight)
@@ -976,27 +1326,67 @@ class TestMmapValidation:
json.dumps({"weight_map": {k: "model.safetensors" for k in weights}})
)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
model_path=str(tmp_path),
),
torch.device("cpu"),
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=(),
model_path=str(tmp_path),
tensor_parallel_size=1,
use_hsdp=False,
online_quantization=False,
)
modules = SimpleNamespace(dits=[pipeline.transformer], dit_names=["transformer"])
# Should NOT raise — TP=1 is allowed even with custom weight_loader
backend._load_weights_via_mmap(pipeline, modules)
assert result.plan is None
assert "custom weight_loader" in result.fallback_reason
def test_cosmos3_adapter_preserves_existing_tp1_direct_mmap_contract(self, tmp_path):
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import (
Cosmos3OmniDiffusersPipeline,
)
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import (
Cosmos3VFMTransformer,
)
transformer = object.__new__(Cosmos3VFMTransformer)
nn.Module.__init__(transformer)
transformer.proj_in = nn.Linear(2, 2, bias=False)
transformer.proj_in.weight.weight_loader = lambda *_args: None
pipeline = object.__new__(Cosmos3OmniDiffusersPipeline)
nn.Module.__init__(pipeline)
pipeline.transformer = transformer
source_root = tmp_path / "partition"
checkpoint_dir = source_root / "transformer"
checkpoint_dir.mkdir(parents=True)
checkpoint_file = checkpoint_dir / "model.safetensors"
save_file({"proj_in.weight": torch.ones(2, 2)}, str(checkpoint_file))
sources = (
SimpleNamespace(
model_or_path=str(source_root),
subfolder="transformer",
revision=None,
prefix="transformer.",
),
)
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", transformer),),
sources=sources,
model_path=None,
tensor_parallel_size=1,
use_hsdp=False,
online_quantization=False,
)
assert result.plan is not None
assert result.plan.bindings["transformer.proj_in.weight"] == TensorBinding(
checkpoint_key="proj_in.weight",
file_path=str(checkpoint_file),
)
def test_validate_loaded_weights_called(self, tmp_path, patched_offload_runtime):
"""validate_loaded_weights should be called after mmap loading."""
import json
from types import SimpleNamespace
from safetensors.torch import save_file
from vllm_omni.diffusion.offloader.base import OffloadConfig, OffloadStrategy
class ModelWithValidate(nn.Module):
_layerwise_offload_blocks_attrs = ["blocks"]
@@ -1038,19 +1428,64 @@ class TestMmapValidation:
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE, pin_cpu_memory=False, model_path=str(tmp_path)
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
),
torch.device("cpu"),
)
modules = SimpleNamespace(dits=[pipeline.transformer], dit_names=["transformer"])
result = build_checkpoint_mmap_plan(
pipeline,
dit_modules=(("transformer", pipeline.transformer),),
sources=(),
model_path=str(tmp_path),
tensor_parallel_size=1,
use_hsdp=False,
online_quantization=False,
)
backend._load_weights_via_mmap(pipeline, modules)
assert result.plan is not None
backend._load_weights_via_mmap(pipeline, modules, result.plan)
assert pipeline.transformer.validate_called, "validate_loaded_weights should be called"
class TestConfigValidation:
"""Tests for configuration validation in OffloadConfig / execute_request."""
def test_loader_plan_cannot_be_silently_dropped_on_unsupported_platform(self, monkeypatch):
import vllm_omni.diffusion.offloader as offloader_module
od_config = SimpleNamespace(
enable_cpu_offload=False,
enable_layerwise_offload=False,
enable_distributed_layerwise_offload=True,
dlo_use_allgather=False,
dlo_resident_layers=0,
pin_cpu_memory=False,
model="unused",
parallel_config=SimpleNamespace(
use_hsdp=False,
data_parallel_size=1,
sequence_parallel_size=1,
),
)
plan = HostWeightPlan(
backing_kind="checkpoint_mmap",
bindings={},
)
monkeypatch.setattr(
offloader_module.current_omni_platform,
"supports_cpu_offload",
lambda: False,
)
with pytest.raises(RuntimeError, match="skipped ordinary weight materialization"):
offloader_module.get_offload_backend(
od_config,
device=torch.device("cpu"),
host_weight_plan=plan,
)
def test_hsdp_with_allgather_rejected(self):
"""HSDP + DLO + AllGather should raise ValueError (double sharding)."""
from vllm_omni.diffusion.offloader.base import OffloadConfig
@@ -740,6 +740,9 @@ def test_load_model_clears_cache_backend_for_unsupported_pipeline(monkeypatch):
del kwargs
return SimpleNamespace(transformer=torch.nn.Identity())
def take_host_weight_plan(self):
return None
class _DummyMemoryProfiler:
consumed_memory = 0
@@ -779,7 +782,11 @@ def test_load_model_clears_cache_backend_for_unsupported_pipeline(monkeypatch):
monkeypatch.setattr(model_runner_module, "LoadConfig", lambda: object())
monkeypatch.setattr(model_runner_module, "DiffusersPipelineLoader", _DummyLoader)
monkeypatch.setattr(model_runner_module, "DeviceMemoryProfiler", _DummyMemoryProfiler)
monkeypatch.setattr(model_runner_module, "get_offload_backend", lambda od_config, device: None)
monkeypatch.setattr(
model_runner_module,
"get_offload_backend",
lambda od_config, device, host_weight_plan: None,
)
monkeypatch.setattr(
model_runner_module, "get_cache_backend", lambda cache_backend, cache_config: dummy_cache_backend
)
@@ -9,6 +9,13 @@ from .modelopt import (
)
def get_direct_mmap_adapter(model: nn.Module):
"""Resolve direct-mmap adapters lazily to avoid model-loader cycles."""
from .direct_mmap import get_direct_mmap_adapter as _get_direct_mmap_adapter
return _get_direct_mmap_adapter(model)
def get_checkpoint_adapter(
model: nn.Module,
source: object,
@@ -29,4 +36,5 @@ __all__ = [
"ModelOptMixedPrecisionCheckpointAdapter",
"ModelOptNvFp4CheckpointAdapter",
"get_checkpoint_adapter",
"get_direct_mmap_adapter",
]
@@ -0,0 +1,108 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Loader-side adapters for proven direct-checkpoint mmap layouts."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from functools import partial
from typing import Protocol
import torch
from torch import nn
@dataclass(frozen=True)
class DirectMmapTensorPolicy:
"""Loader-owned proof for one tensor's direct mmap behavior."""
allow_custom_loader: bool = False
transform: Callable[[torch.Tensor], torch.Tensor] | None = None
class DirectMmapAdapter(Protocol):
def policy_for(
self,
runtime_name: str,
target: torch.Tensor,
) -> DirectMmapTensorPolicy | None: ...
class _MiniMaxH3DirectMmapAdapter:
"""TP1 runtime-layout contract already enforced by MiniMax-H3's loader."""
def __init__(self, pipeline: nn.Module) -> None:
self.pipeline = pipeline
def policy_for(
self,
runtime_name: str,
target: torch.Tensor,
) -> DirectMmapTensorPolicy:
del target
transform = None
suffix = ".qkv_proj.weight"
if runtime_name.endswith(suffix):
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import (
MiniMaxH3Attention,
_reorder_grouped_qkv_to_qkv,
)
attention_path = runtime_name[: -len(suffix)]
attention = self.pipeline.get_submodule(attention_path)
if not isinstance(attention, MiniMaxH3Attention):
raise ValueError(
f"MiniMax-H3 direct-mmap adapter expected attention at {attention_path!r}, "
f"got {type(attention).__name__}"
)
transform = partial(
_reorder_grouped_qkv_to_qkv,
num_query_groups=attention.total_num_heads,
heads_per_group=1,
head_dim=attention.head_dim,
)
return DirectMmapTensorPolicy(
allow_custom_loader=True,
transform=transform,
)
class _Cosmos3DirectMmapAdapter:
"""Preserve Cosmos3's existing TP1 direct-mmap load contract."""
def policy_for(
self,
runtime_name: str,
target: torch.Tensor,
) -> DirectMmapTensorPolicy:
del runtime_name, target
# Cosmos3 has no packed module mapping or TP1 semantic transform after
# its checkpoint-key remap. Its custom vLLM loaders reduce to exact
# copies at TP1; topology and metadata are checked by plan preflight.
return DirectMmapTensorPolicy(allow_custom_loader=True)
def get_direct_mmap_adapter(pipeline: nn.Module) -> DirectMmapAdapter | None:
"""Return a loader-owned adapter without requiring a pipeline flag."""
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import (
MiniMaxH3DiTModel,
)
if any(isinstance(module, MiniMaxH3DiTModel) for module in pipeline.modules()):
return _MiniMaxH3DirectMmapAdapter(pipeline)
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import (
Cosmos3VFMTransformer,
)
if any(isinstance(module, Cosmos3VFMTransformer) for module in pipeline.modules()):
return _Cosmos3DirectMmapAdapter()
return None
__all__ = [
"DirectMmapAdapter",
"DirectMmapTensorPolicy",
"get_direct_mmap_adapter",
]
@@ -6,7 +6,7 @@ import glob
import os
import re
import time
from collections.abc import Generator, Iterable
from collections.abc import Generator, Iterable, Sequence
from pathlib import Path
from typing import cast
@@ -34,6 +34,11 @@ from vllm_omni.diffusion.distributed.hsdp import HSDPInferenceConfig, apply_hsdp
from vllm_omni.diffusion.model_loader.checkpoint_adapters import (
get_checkpoint_adapter,
)
from vllm_omni.diffusion.model_loader.host_weight_plan import (
HostWeightPlan,
build_checkpoint_mmap_plan,
has_online_quantization,
)
from vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter import DiffusersAdapterPipeline
from vllm_omni.diffusion.offloader.module_collector import ModuleDiscovery
from vllm_omni.diffusion.registry import initialize_model
@@ -135,6 +140,13 @@ class DiffusersPipelineLoader:
self.od_config = od_config
self.quant_config = od_config.quantization_config
self.parallel_config = od_config.parallel_config
self.host_weight_plan: HostWeightPlan | None = None
def take_host_weight_plan(self) -> HostWeightPlan | None:
"""Transfer the loader-produced plan to the offload backend."""
plan = self.host_weight_plan
self.host_weight_plan = None
return plan
def _prepare_weights(
self,
@@ -293,8 +305,10 @@ class DiffusersPipelineLoader:
def get_all_weights(
self,
model: nn.Module,
sources: Sequence["ComponentSource"] | None = None,
) -> Generator[tuple[str, torch.Tensor], None, None]:
sources = self._get_weight_sources(model)
if sources is None:
sources = self._get_weight_sources(model)
for source in sources:
yield from self._get_weights_iterator(source, model=model)
@@ -386,6 +400,7 @@ class DiffusersPipelineLoader:
device: torch.device | None = None,
) -> nn.Module:
"""Load a model with the given configurations."""
self.host_weight_plan = None
if load_format is None:
load_format = "default"
# CPU offload + quantization: for offline-quantized models (e.g., AutoRound MXFP8),
@@ -417,22 +432,54 @@ 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
_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))
_dp_size = int(getattr(self.parallel_config, "data_parallel_size", 1))
_sp_size = int(getattr(self.parallel_config, "sequence_parallel_size", 1))
_dlo_group_size = _dp_size if _dp_size > 1 else _sp_size
_skip_load = _dist_offload and _use_ag and _supports_mmap and not _has_online_quant
plan_result = None
weight_sources = self._get_weight_sources(model)
if _dist_offload:
modules = ModuleDiscovery.discover(model)
plan_result = build_checkpoint_mmap_plan(
model,
dit_modules=tuple(zip(modules.dit_names, modules.dits)),
sources=weight_sources,
model_path=str(getattr(self.od_config, "model", "")) or None,
tensor_parallel_size=_tp_size,
use_hsdp=_use_hsdp,
online_quantization=_has_online_quant,
)
self.host_weight_plan = plan_result.plan
_skip_load = self.host_weight_plan is not None
if _skip_load:
logger.info("DLO+AllGather active: skipping load_weights (will load via mmap in enable())")
logger.info(
"DLO host-weight plan active (%s, %s): skipping ordinary materialization for %s",
"AllGather" if _use_ag and _dlo_group_size > 1 else "rank-local",
self.host_weight_plan.backing_kind,
sorted(self.host_weight_plan.planned_source_prefixes) or "legacy DiT sources",
)
ordinary_sources = tuple(
source
for source in weight_sources
if source.prefix not in self.host_weight_plan.planned_source_prefixes
)
if ordinary_sources:
logger.info(
"Loading %d component weight source(s) outside the DLO host-weight plan",
len(ordinary_sources),
)
self.load_weights(
model,
sources=ordinary_sources,
planned_weights=self.host_weight_plan.bindings,
)
else:
if _dist_offload and _use_ag and _has_online_quant:
raise ValueError(
@@ -442,9 +489,10 @@ class DiffusersPipelineLoader:
"Please use --dlo-no-use-allgather or disable online "
"quantization."
)
if _dist_offload and _use_ag and not _supports_mmap:
if _dist_offload and plan_result is not None:
logger.info(
"DLO+AllGather active but model does not support mmap: loading weights via regular loader."
"DLO direct checkpoint mmap unavailable; using ordinary loader: %s",
plan_result.fallback_reason,
)
logger.debug("Loading weights on %s ...", load_device)
if offload_after_quant:
@@ -498,11 +546,7 @@ 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
return has_online_quantization(model)
def _apply_skip_softmax_calibration(self, model: nn.Module) -> None:
from vllm_omni.diffusion.attention.backends.trtllm_calibration import (
@@ -605,12 +649,16 @@ class DiffusersPipelineLoader:
model: nn.Module,
*,
stream_online_quant_to_cpu: bool = False,
sources: Sequence["ComponentSource"] | None = None,
planned_weights: Iterable[str] = (),
) -> None:
weights_to_load = self._get_expected_parameter_names(model)
weights = self.get_all_weights(model)
weights = self.get_all_weights(model) if sources is None else self.get_all_weights(model, sources=sources)
if stream_online_quant_to_cpu:
weights = self._stream_online_quant_weights_to_cpu(model, weights)
loaded_weights = model.load_weights(weights)
if loaded_weights is not None:
loaded_weights = set(loaded_weights).union(planned_weights)
self.counter_after_loading_weights = time.perf_counter()
logger.info_once(
@@ -0,0 +1,355 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Loader-owned host-weight plans shared with diffusion offload backends."""
from __future__ import annotations
import glob
import json
import os
from collections.abc import Callable, Sequence
from dataclasses import dataclass
import torch
from safetensors import safe_open
from torch import nn
from vllm.logger import init_logger
from vllm_omni.diffusion.model_loader.checkpoint_adapters import (
get_direct_mmap_adapter,
)
logger = init_logger(__name__)
TensorTransform = Callable[[torch.Tensor], torch.Tensor]
@dataclass(frozen=True)
class TensorBinding:
"""One runtime tensor backed by one safetensors entry."""
checkpoint_key: str
file_path: str
transform: TensorTransform | None = None
@dataclass(frozen=True)
class HostWeightPlan:
"""Complete, prevalidated host backing consumed by an offload backend."""
backing_kind: str
bindings: dict[str, TensorBinding]
planned_source_prefixes: frozenset[str] = frozenset()
@dataclass(frozen=True)
class HostWeightPlanResult:
"""A complete plan or a fail-closed reason to use the ordinary loader."""
plan: HostWeightPlan | None
fallback_reason: str | None = None
class _PlanIncompatibleError(RuntimeError):
pass
_SAFETENSORS_DTYPES: dict[str, torch.dtype] = {
"BOOL": torch.bool,
"U8": torch.uint8,
"I8": torch.int8,
"I16": torch.int16,
"I32": torch.int32,
"I64": torch.int64,
"F16": torch.float16,
"BF16": torch.bfloat16,
"F32": torch.float32,
"F64": torch.float64,
}
for _safe_name, _torch_name in (
("F8_E4M3", "float8_e4m3fn"),
("F8_E5M2", "float8_e5m2"),
):
if (_dtype := getattr(torch, _torch_name, None)) is not None:
_SAFETENSORS_DTYPES[_safe_name] = _dtype
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 _resolve_source_root(source: object) -> str:
source_root = os.fspath(getattr(source, "model_or_path"))
subfolder = getattr(source, "subfolder", None)
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_root,
cache_dir=None,
allow_patterns=["*.safetensors", "*.safetensors.index.json"],
revision=getattr(source, "revision", None),
subfolder=subfolder,
)
return os.path.join(source_root, subfolder) if subfolder is not None else source_root
def _record_binding(
model_to_ckpt: dict[str, tuple[str, str]],
model_name: str,
checkpoint_key: str,
file_path: str,
) -> None:
existing = model_to_ckpt.get(model_name)
candidate = (checkpoint_key, file_path)
if existing is not None and existing != candidate:
raise _PlanIncompatibleError(
f"multiple checkpoint entries map to runtime tensor {model_name!r}: {existing!r} and {candidate!r}"
)
model_to_ckpt[model_name] = candidate
def _add_safetensors_source(
checkpoint_dir: str,
prefix: str,
remap_fn: Callable[[str], str | None] | None,
model_to_ckpt: dict[str, tuple[str, str]],
) -> int:
indexed_keys = 0
index_files = sorted(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:
_record_binding(
model_to_ckpt,
model_name,
checkpoint_key,
os.path.join(checkpoint_dir, filename),
)
indexed_keys += 1
return indexed_keys
for filename in sorted(glob.glob(os.path.join(checkpoint_dir, "*.safetensors"))):
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:
_record_binding(model_to_ckpt, model_name, checkpoint_key, filename)
indexed_keys += 1
return indexed_keys
def _build_source_map(
*,
sources: Sequence[object],
model_path: str | None,
remap_fn: Callable[[str], str | None] | None,
) -> dict[str, tuple[str, str]]:
model_to_ckpt: dict[str, tuple[str, str]] = {}
indexed_keys = 0
for source in sources:
indexed_keys += _add_safetensors_source(
_resolve_source_root(source),
getattr(source, "prefix", ""),
remap_fn,
model_to_ckpt,
)
if not sources:
if remap_fn is None:
raise _PlanIncompatibleError(
"the loader has no component weight sources and the pipeline has no checkpoint-key remapper"
)
if not model_path:
raise _PlanIncompatibleError("the loader has no model path for legacy checkpoint remapping")
resolved_model_path = model_path
if not os.path.isdir(resolved_model_path):
from vllm.model_executor.model_loader.weight_utils import (
download_weights_from_hf,
)
resolved_model_path = download_weights_from_hf(
model_name_or_path=resolved_model_path,
cache_dir=None,
allow_patterns=["*.safetensors", "*.safetensors.index.json"],
)
checkpoint_dirs = {
os.path.dirname(path)
for pattern in ("*.safetensors.index.json", "*.safetensors")
for path in glob.glob(os.path.join(resolved_model_path, "**", pattern), recursive=True)
}
for checkpoint_dir in sorted(checkpoint_dirs):
indexed_keys += _add_safetensors_source(
checkpoint_dir,
"",
remap_fn,
model_to_ckpt,
)
if not model_to_ckpt:
raise _PlanIncompatibleError("no compatible safetensors entries were found in the loader's model sources")
logger.info(
"Indexed %d runtime tensor names from %d checkpoint keys for DLO preflight",
len(model_to_ckpt),
indexed_keys,
)
return model_to_ckpt
def _is_persistent_buffer(module: nn.Module, local_name: str) -> bool:
parts = local_name.split(".")
owner = module.get_submodule(".".join(parts[:-1])) if len(parts) > 1 else module
return parts[-1] not in owner._non_persistent_buffers_set
def _collect_required_dit_tensors(
dit_modules: Sequence[tuple[str, nn.Module]],
) -> dict[str, torch.Tensor]:
required: dict[str, torch.Tensor] = {}
for dit_name, dit_module in dit_modules:
for local_name, tensor in dit_module.named_parameters():
required[f"{dit_name}.{local_name}"] = tensor
for local_name, tensor in dit_module.named_buffers():
if _is_persistent_buffer(dit_module, local_name):
required[f"{dit_name}.{local_name}"] = tensor
return required
def _planned_source_prefixes(
sources: Sequence[object],
dit_modules: Sequence[tuple[str, nn.Module]],
) -> frozenset[str]:
"""Identify component sources that the DiT-only plan owns completely."""
if not sources:
return frozenset()
expected = frozenset(f"{dit_name}." for dit_name, _ in dit_modules)
available = {getattr(source, "prefix", "") for source in sources}
missing = sorted(expected - available)
if missing:
raise _PlanIncompatibleError(
f"direct mmap requires a dedicated component weight source for each DiT; missing prefixes: {missing}"
)
return expected
def _validate_source_metadata(
required: dict[str, torch.Tensor],
bindings: dict[str, TensorBinding],
) -> None:
by_file: dict[str, list[tuple[str, torch.Tensor, TensorBinding]]] = {}
for runtime_name, target in required.items():
by_file.setdefault(bindings[runtime_name].file_path, []).append((runtime_name, target, bindings[runtime_name]))
for file_path, entries in by_file.items():
if not os.path.isfile(file_path):
raise _PlanIncompatibleError(f"checkpoint file does not exist: {file_path}")
with safe_open(file_path, framework="pt", device="cpu") as handle:
available = set(handle.keys())
for runtime_name, target, binding in entries:
if binding.checkpoint_key not in available:
raise _PlanIncompatibleError(
f"checkpoint key {binding.checkpoint_key!r} for {runtime_name!r} is missing from {file_path}"
)
source = handle.get_slice(binding.checkpoint_key)
source_shape = tuple(source.get_shape())
source_dtype = _SAFETENSORS_DTYPES.get(source.get_dtype())
if source_shape != tuple(target.shape):
raise _PlanIncompatibleError(
f"shape mismatch for {runtime_name!r}: checkpoint={source_shape}, runtime={tuple(target.shape)}"
)
if source_dtype is None or source_dtype != target.dtype:
raise _PlanIncompatibleError(
f"dtype mismatch for {runtime_name!r}: checkpoint={source.get_dtype()}, runtime={target.dtype}"
)
def build_checkpoint_mmap_plan(
pipeline: nn.Module,
*,
dit_modules: Sequence[tuple[str, nn.Module]],
sources: Sequence[object],
model_path: str | None,
tensor_parallel_size: int,
use_hsdp: bool,
online_quantization: bool,
) -> HostWeightPlanResult:
"""Build a complete direct-checkpoint plan or return a fallback reason."""
if tensor_parallel_size != 1:
return HostWeightPlanResult(None, f"TP={tensor_parallel_size} requires the ordinary loader")
if use_hsdp:
return HostWeightPlanResult(None, "HSDP requires the ordinary loader")
if online_quantization:
return HostWeightPlanResult(None, "online quantization requires the ordinary loader")
remap_fn = getattr(type(pipeline), "_remap_ckpt_key", None)
if not callable(remap_fn):
remap_fn = None
adapter = get_direct_mmap_adapter(pipeline)
try:
planned_source_prefixes = _planned_source_prefixes(sources, dit_modules)
planned_sources = tuple(
source for source in sources if getattr(source, "prefix", "") in planned_source_prefixes
)
model_to_ckpt = _build_source_map(
sources=planned_sources,
model_path=model_path,
remap_fn=remap_fn,
)
required = _collect_required_dit_tensors(dit_modules)
if not required:
raise _PlanIncompatibleError("no DiT parameters or persistent buffers were discovered")
missing = sorted(set(required) - set(model_to_ckpt))
if missing:
raise _PlanIncompatibleError(
f"{len(missing)} required DiT tensors have no checkpoint binding (first 5: {missing[:5]})"
)
bindings: dict[str, TensorBinding] = {}
for runtime_name, target in required.items():
policy = adapter.policy_for(runtime_name, target) if adapter is not None else None
has_custom_loader = callable(getattr(target, "weight_loader", None))
if has_custom_loader and not (policy and policy.allow_custom_loader):
raise _PlanIncompatibleError(
f"{runtime_name!r} requires a custom weight_loader with no direct-mmap adapter"
)
checkpoint_key, file_path = model_to_ckpt[runtime_name]
bindings[runtime_name] = TensorBinding(
checkpoint_key=checkpoint_key,
file_path=file_path,
transform=policy.transform if policy is not None else None,
)
_validate_source_metadata(required, bindings)
except (OSError, ValueError, _PlanIncompatibleError) as exc:
return HostWeightPlanResult(None, str(exc))
return HostWeightPlanResult(
HostWeightPlan(
backing_kind="checkpoint_mmap",
bindings=bindings,
planned_source_prefixes=planned_source_prefixes,
)
)
__all__ = [
"HostWeightPlan",
"HostWeightPlanResult",
"TensorBinding",
"build_checkpoint_mmap_plan",
"has_online_quantization",
]
@@ -540,13 +540,6 @@ 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.
_PROFILER_TARGETS: ClassVar[list[str]] = [
"_prepare_reference_videos",
"encode_prompt",
+25 -3
View File
@@ -5,6 +5,7 @@ import torch
from vllm.logger import init_logger
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.model_loader.host_weight_plan import HostWeightPlan
from vllm_omni.platforms import current_omni_platform
from .base import OffloadBackend, OffloadConfig, OffloadStrategy
@@ -17,7 +18,7 @@ from .distributed_layerwise_backend import (
)
from .layerwise_backend import LayerWiseOffloadBackend
from .module_residency import PinnedModuleStager
from .offload_plan import OffloadPlan, get_offload_plan, supports_mmap_loading
from .offload_plan import OffloadPlan, get_offload_plan
from .sequential_backend import (
ModelLevelOffloadBackend,
apply_sequential_offload,
@@ -49,7 +50,6 @@ __all__ = [
"remove_distributed_block_hook",
"get_offload_backend",
"get_offload_plan",
"supports_mmap_loading",
"get_blocks_attr_names",
"get_blocks_from_dit",
"set_blocks_attr_names",
@@ -64,12 +64,15 @@ __all__ = [
def get_offload_backend(
od_config: OmniDiffusionConfig,
device: torch.device | None = None,
host_weight_plan: HostWeightPlan | None = None,
) -> OffloadBackend | None:
"""Create appropriate offload backend based on configuration.
Args:
od_config: OmniDiffusionConfig with offload settings
device: Target device (auto-detected if None)
host_weight_plan: Exact loader-produced backing plan, if ordinary
weight materialization was skipped.
Returns:
OffloadBackend instance or None if offloading disabled
@@ -82,12 +85,22 @@ def get_offload_backend(
# Extract and validate configuration
config = OffloadConfig.from_od_config(od_config)
if host_weight_plan is not None and config.strategy != OffloadStrategy.DISTRIBUTED_LAYER_WISE:
raise RuntimeError(
"A loader-owned DLO host-weight plan was produced, but distributed layerwise offload is not selected"
)
# Return None if no offloading requested
if config.strategy == OffloadStrategy.NONE:
return None
# Validate platform (CUDA required for now)
if not current_omni_platform.supports_cpu_offload() or current_omni_platform.get_device_count() < 1:
if host_weight_plan is not None:
raise RuntimeError(
"The loader skipped ordinary weight materialization for DLO, "
"but this platform cannot create the required offload backend"
)
logger.warning(
"Current device: %s does not support CPU offloading. Skipping offloading.",
current_omni_platform.get_device_name(),
@@ -99,6 +112,11 @@ def get_offload_backend(
try:
device = current_omni_platform.get_torch_device()
except (NotImplementedError, AttributeError) as exc:
if host_weight_plan is not None:
raise RuntimeError(
"The loader skipped ordinary weight materialization for DLO, "
"but the target device could not be resolved"
) from exc
logger.error("Failed to detect device: %s. Skipping offloading.", exc)
return None
@@ -108,7 +126,11 @@ def get_offload_backend(
elif config.strategy == OffloadStrategy.LAYER_WISE:
return LayerWiseOffloadBackend(config, device)
elif config.strategy == OffloadStrategy.DISTRIBUTED_LAYER_WISE:
return DistributedLayerwiseOffloadBackend(config, device)
return DistributedLayerwiseOffloadBackend(
config,
device,
host_weight_plan=host_weight_plan,
)
else:
logger.error("Unknown offload strategy: %s", config.strategy)
return None
+4 -6
View File
@@ -27,11 +27,10 @@ class OffloadConfig:
pin_cpu_memory: bool = True
use_hsdp: bool = False
dp_size: int = 1 # derived from parallel_config, not user-configurable
# True: add DP sharding + AllGather. False: stream the standard loader's
# rank-local tensors (including TP-local shards) with H2D only.
# True: add DP sharding + AllGather. False: stream complete rank-local
# blocks from the loader-selected host backing with H2D only.
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
@classmethod
def from_od_config(cls, od_config: OmniDiffusionConfig) -> "OffloadConfig":
@@ -58,7 +57,6 @@ class OffloadConfig:
parallel_config = getattr(od_config, "parallel_config", None)
use_hsdp = getattr(parallel_config, "use_hsdp", False) if parallel_config else False
# Derive dp_size from parallel_config — not user-configurable.
# The offload adapts to whatever DP/SP is already configured.
dp_size = 1
@@ -116,7 +114,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
@@ -137,7 +136,6 @@ class OffloadConfig:
dp_size=dp_size,
dlo_use_allgather=dlo_use_allgather,
dlo_resident_layers=dlo_resident_layers,
model_path=getattr(od_config, "model", None),
)
File diff suppressed because it is too large. Load diff
@@ -55,20 +55,3 @@ class OffloadPlan:
def get_offload_plan(pipeline: nn.Module) -> OffloadPlan | None:
"""Retrieve the OffloadPlan declared by the pipeline, if any."""
return getattr(pipeline, "_offload_plan", 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.
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()
)
@@ -292,7 +292,11 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
)
# Apply CPU offloading
self.offload_backend = get_offload_backend(self.od_config, device=self.device)
self.offload_backend = get_offload_backend(
self.od_config,
device=self.device,
host_weight_plan=model_loader.take_host_weight_plan(),
)
if self.offload_backend is not None:
logger.info(f" Enabling offloader backend: {self.offload_backend.__class__.__name__}")
self.offload_backend.enable(self.pipeline)