Compare commits

..
Author SHA1 Message Date
Hongsheng Liu 1ae56cfcfb Add normalized online FP8 cache for DLO
Signed-off-by: hsliuustc0106 <hsliuustc0106@users.noreply.github.com>
2026-08-19 11:55:29 +08:00
bjf-frzandHongsheng Liu 14bd8f8772 [REFACT]Refactor diffusion parallel state (#5531)
Signed-off-by: bjf-frz <frz123db@gmail.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-19 11:23:10 +08:00
42 changed files with 1318 additions and 4420 deletions

No files matched your search

@@ -43,291 +43,21 @@ DLO weight collective.
Host storage is selected separately from the transfer protocol. The loader can Host storage is selected separately from the transfer protocol. The loader can
produce a direct-checkpoint mmap plan for a proven-compatible runtime layout; produce a direct-checkpoint mmap plan for a proven-compatible runtime layout;
otherwise it uses the ordinary loader. In no-AllGather mode, the Phase B host otherwise it uses the ordinary loader. Consequently, no-AllGather replicas on
weight cache can explicitly normalize supported final ordinary-loader DiT the same node can share immutable checkpoint pages when direct mmap is
tensors into an immutable node-local mmap entry. Ordinary pinned weights remain selected, while the ordinary-loader fallback still keeps a private runtime
the default after checkpoint mmap fallback when the cache is not selected; cache copy per process.
publication and validation add startup and disk lifecycle costs. Replicas can
share either proven-compatible checkpoint pages or, when selected, equivalent
final runtime layouts. Under the existing
pinned-memory policy, the backend attempts to make the complete final mapping
directly H2D-copyable when the platform provides registration, avoiding the
recurrent host packing otherwise required by mmap. An optional positive limit
adds a per-worker safety ceiling. CUDA is the first implementation.
The Phase A shared-mmap support boundary is TP1. TP greater than one is an 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 ordinary-loader compatibility path: DLO can consume the resulting TP-local
tensors. Direct checkpoint mmap still does not cover TP, but the Phase B cache tensors, but those configurations do not use checkpoint mmap and must not be
can share each final TP coordinate among equivalent processes. used to claim shared-mmap host-memory savings.
The compatibility matrix below defines the current implementation boundary. The compatibility matrix below describes the current implementation. The
Hardware and performance qualification is tracked separately from this unit-level guards are covered, but not every parallelism combination has a
long-lived design contract. full model-and-hardware end-to-end test.
## Architecture ## Design
DLO is divided into four planes: configuration, loader-owned host storage,
backend setup, and the per-block inference hot path. Keeping these decisions
separate is intentional. In particular, an mmap cache is a host-storage choice;
it does not imply a DLO AllGather, and disabling DLO AllGather does not decide
whether the host source is private memory, checkpoint mmap, or host weight cache
mmap. The shared control plane is described once below; the AllGather and
no-AllGather startup and hot paths are then shown separately.
### Component ownership
The map includes optional mode-specific components, but the ownership rules
and loader-to-backend boundary are shared.
```mermaid
flowchart TB
CLI["CLI / EngineArgs"] --> Config["OmniDiffusionConfig<br/>validation and operator policy"]
Config --> Runner["DiffusionModelRunner<br/>startup coordinator"]
subgraph LoaderPlane["Loader-owned host-storage plane"]
Loader["DiffusersPipelineLoader"]
Checkpoint["checkpoint mmap preflight"]
Ordinary["ordinary load + callbacks + calibration"]
HostWeightCache["host_weight_cache<br/>final-layout publication / join"]
Contract["HostWeightPlan<br/>backing + bindings + transforms"]
Loader --> Checkpoint
Loader -->|"registered final layout requested"| Ordinary
Checkpoint -->|"compatible"| Contract
Checkpoint -->|"fallback"| Ordinary
Ordinary -->|"host weight cache enabled"| HostWeightCache
HostWeightCache -->|"enabled and compatible"| Contract
end
Runner -->|"load_model"| Loader
Loader -->|"pipeline + optional HostWeightPlan"| Runner
Config --> Factory["get_offload_backend<br/>OffloadConfig normalization"]
Runner -->|"device + optional plan"| Factory
Factory --> Backend["DistributedLayerwiseOffloadBackend<br/>resource and transfer owner"]
Topology["existing DP / SP process groups"] --> Backend
Discovery["ModuleDiscovery + optional OffloadPlan"] --> Backend
Contract -.->|"passed once by the runner"| Backend
Backend -.->|"host weight cache + pinned-memory policy"| Registration["host_registration<br/>platform-neutral lifecycle"]
Registration --> CUDA["CUDA registration backend"]
Backend --> Hooks["DistributedLayerwiseOffloadHook<br/>one circular hook per streamed block"]
Forward["pipeline.forward"] --> Hooks
Hooks --> DataPath["two device slots<br/>H2D only or H2D + AllGather"]
Shutdown["DiffusionWorker.shutdown"] -->|"disable before process-group teardown"| Backend
```
| Component | Owns | Must not own |
| --- | --- | --- |
| CLI, engine arguments, and `OmniDiffusionConfig` | User policy, defaults, validation, and propagation to every worker | Checkpoint inspection, cache publication, or buffer allocation |
| `OffloadConfig.from_od_config()` | Offload-strategy selection, effective DLO group size, AllGather policy, resident-layer count, pinned-memory policy, and optional registration ceiling | Host-weight compatibility decisions |
| `DiffusersPipelineLoader` | Ordinary loading lifecycle and the decision to return an optional `HostWeightPlan` | DLO process groups or block-stream scheduling |
| Checkpoint mmap planner | Proving that checkpoint tensors can represent the required runtime layout before ordinary DiT materialization is skipped | Host weight cache identity or transfer policy |
| `host_weight_cache` | Normalizing final ordinary-loader tensors into an immutable node-local entry and returning a validated plan | Model mutation, host registration, H2D, or collectives |
| `HostWeightPlan` | The one-way loader-to-offloader contract: backing kind, tensor bindings, source coverage, and any bounded transforms | Cache construction or runtime scheduling |
| `ModuleDiscovery` and model `OffloadPlan` | Discovering DiT/components and declaring block, resident, and on-demand paths | Loader or cache behavior embedded in model pipelines |
| `DistributedLayerwiseOffloadBackend` | Consuming the exact plan, selecting the existing group, mapping/repointing storage, allocating shared buffers and streams, optional host registration, installing hooks, and cleanup | Re-running loader compatibility checks or orchestrating replicas |
| `DistributedLayerwiseOffloadHook` | The block hot path: prefetch, optional DLO AllGather, readiness events, parameter repointing, and replacement with offload placeholders | Cache keys, manifest validation, or request routing |
| `host_registration` and platform backend | All-or-nothing registration lifecycle for existing immutable mappings | Choosing which weights are cached or changing the two-slot device-buffer contract |
| `DiffusionWorker` | Calling backend teardown before destroying the distributed environment | Managing backend-owned mappings or registrations directly |
The configuration fields are routed to the component that can enforce their
contract. The cache root and lock timeout are consumed by the loader.
`dlo_use_host_weight_cache` explicitly selects final-layout cache publication
instead of direct-checkpoint mmap. It defaults to false so the ordinary
pinned-loader path remains available without requiring an opt-out.
With `pin_cpu_memory=True`, selecting the host weight cache deliberately chooses
the transform-complete layout even when direct checkpoint mmap is available,
because only that final layout can be registered without deferred transforms.
The backend consumes `dlo_host_weight_cache_pin_limit_gib` as an optional
registration ceiling; zero applies no additional ceiling and does not select
the cache by itself.
`dlo_use_allgather` controls loader plan routing because the host weight cache
is no-AllGather-only, and it selects the backend transfer protocol. This is
coordination through typed configuration and `HostWeightPlan`, not through
model-specific global state.
### Runtime host-layout identity (v1)
Host weight cache v1 identifies the loader-produced host representation, not
the topology used later to move weights to devices. These are separate
contracts:
- `RuntimeHostLayout` describes tensor ownership and representation after the
ordinary loader has applied its parallel-aware transforms. This is the
layout stored in the host weight cache.
- `DLOTransferLayout` describes whether each worker copies its local weights
directly or participates in the existing DLO AllGather protocol. It is not
part of the host weight cache in v1; AllGather continues to use its existing
process-owned host shards.
The v1 physical-sharing invariant is one cache entry per TP coordinate,
shared by equivalent DP and SP consumers on the same node. The identity
therefore includes TP world size and TP rank, because different TP coordinates
own different tensors. It excludes DP world size/rank and SP rank, because
those dimensions do not change the final tensor ownership supported by v1.
SP world size and implementation details remain conservative compatibility
guards until equivalent layouts across those configurations are proven.
PP, HSDP/DTensor, expert parallelism, and CFG parallelism are rejected rather
than assigned speculative cache identities. Supporting one of these layouts
requires first defining its loader-produced ownership, representation, and
sharing domain; it does not follow automatically from supporting its runtime
collectives. Consequently, this phase implements a TP-coordinate runtime host
layout, not a general parallel-layout cache.
### Shared startup and lifecycle
The shared control path selects host backing and transfers its ownership to
the backend before either transfer mode installs hooks:
```mermaid
sequenceDiagram
participant C as OmniDiffusionConfig
participant R as DiffusionModelRunner
participant L as DiffusersPipelineLoader
participant B as DLO backend
participant W as DiffusionWorker
C->>R: validated offload policy
R->>L: load_model(config)
L->>L: select and validate host backing
L-->>R: pipeline + optional HostWeightPlan
R->>B: enable(config, device, optional plan)
B->>B: discover modules and consume host backing
B-->>R: mode-specific backend is ready
W->>B: shutdown calls disable()
B->>B: synchronize, unregister, remove hooks, close mappings
```
The loader returns a plan only after its contract is valid: checkpoint plans
have preflight-proven source coverage and metadata, while host weight cache
plans also have full manifest, file, and mapped-content validation. The runner
then transfers ownership exactly once with
`model_loader.take_host_weight_plan()`. If no plan is returned, the backend
requires materialized ordinary-loader tensors. If a plan caused ordinary DiT
materialization to be skipped, failure to create or enable its consumer is a
hard startup error rather than a fallback to uninitialized parameters.
At inference time, the model pipeline remains the caller: normal block forward
calls activate the installed hooks. Both modes use one copy stream and two
shared device slots sized for the largest streamed block. The hooks do not
call the loader or cache.
### AllGather startup and hot path
```mermaid
sequenceDiagram
participant B as DLO backend
participant H as block hooks
participant G as existing DP or SP group
B->>B: choose existing group and prepare persistent local shards
B->>B: allocate two device slots and a communication stream
B->>H: install hooks and prime the first block
loop each streamed block
H->>H: copy the next local shard to the alternate slot
H->>G: all_gather_into_tensor(next block)
G-->>H: complete next block is ready
H->>H: execute current block and rotate slots
end
```
AllGather uses the existing DP group when DP is greater than one, otherwise
the existing SP group when SP is greater than one. TP is never selected as
DLO's weight-sharding group. Every participating rank must request blocks in
the same order; fast P2P reduces communication cost but does not remove that
lockstep contract. An effective group size of one issues no collective and
uses rank-local transfer.
### No-AllGather startup and hot path
```mermaid
sequenceDiagram
participant L as DiffusersPipelineLoader
participant C as host_weight_cache
participant B as DLO backend
participant P as platform registration
participant H as block hooks
alt host weight cache explicitly selected
L->>L: load, mutate, validate, and calibrate final tensors
L->>C: publish or join final-layout entry
C-->>L: validated host weight cache plan or fail-closed fallback
L-->>B: cache plan or materialized fallback via runner
else automatic checkpoint selection
L->>L: checkpoint mmap preflight
alt loader selects a compatible checkpoint plan
L-->>B: checkpoint plan via runner
else ordinary loading
L->>L: load, mutate, validate, and calibrate final tensors
L-->>B: materialized tensors via runner
end
end
B->>B: consume complete rank-local block sources
opt host weight cache plan and pinned-memory policy
B->>P: register the complete immutable mapping
P-->>B: direct-H2D source or bounded-staging fallback
end
B->>H: install hooks and prime the first block
loop each streamed block
alt registered host weight cache mapping
H->>H: copy complete next block directly to device
else pageable mmap mapping
H->>H: pack into a bounded staging slot, then copy to device
else ordinary runtime tensors
H->>H: copy complete next block to device
end
H->>H: execute current block and rotate slots
end
```
No-AllGather creates no DLO weight group and issues no DLO collective.
Workers may therefore schedule requests independently. Ordinary TP and SP
collectives remain inside model execution; host registration changes only
the source of the complete-block H2D copy.
### Host backing and transfer composition
| Loader-selected host backing | Backend transfer mode | Setup result | Per-block hot path |
| --- | --- | --- | --- |
| Ordinary runtime tensors | AllGather group larger than one | Each rank retains a pinned local weight shard | shard H2D → DLO AllGather → device slot |
| Checkpoint mmap | AllGather group larger than one | Each rank prepares its persistent shard, then releases the source mapping | shard H2D → DLO AllGather → device slot |
| Ordinary runtime tensors | Rank-local / no-AllGather | Each rank retains the complete loader-produced local layout | complete-block H2D → device slot |
| Checkpoint mmap, or host weight cache with pinned memory disabled/registration unavailable | Rank-local / no-AllGather | Immutable mapping plus two bounded host-staging slots | mmap views → staging slot → device slot |
| Host weight cache mmap with pinned memory enabled and registration available | Rank-local / no-AllGather | Complete mapping is registered by the platform backend | registered mmap views → device slot |
The three axes therefore remain independent:
1. **Offload policy:** whether DLO is enabled and which layers are resident.
2. **Host backing:** ordinary tensors, checkpoint mmap, or final host weight cache
mmap, selected by the loader.
3. **Transfer protocol:** rank-local H2D or H2D plus DLO AllGather, selected by
the backend from validated configuration and existing topology.
### Failure and lifecycle boundaries
- Invalid option combinations fail during configuration validation.
- Checkpoint mmap preflight failure falls back before ordinary loading is
skipped.
- Host weight cache incompatibility, timeout, or publication failure retains
the already-valid ordinary tensors.
- Host-registration failure rolls back and retains bounded staging. A rollback
failure aborts startup because unmapping a range still owned by the platform
would be unsafe.
- During shutdown, DLO synchronizes outstanding device work, unregisters host
mappings, removes hooks, and closes mmap handles only after safe unregister;
the worker destroys its process groups afterward.
DLO deliberately does not own DP replica placement, independent-engine request
routing, cross-engine admission control, or cross-node cache distribution.
Those belong to an external router/orchestrator. It also does not replace TP or
SP model collectives; it only adds an optional weight collective over an
already-existing DP or SP group.
## Shared design
The following topology and loader contracts apply to both transfer modes.
### DLO consumes the existing parallel topology ### DLO consumes the existing parallel topology
@@ -335,10 +65,15 @@ DLO does not create a new DP, TP, or SP topology. It reads the configured
`DiffusionParallelConfig` and attaches offload hooks to the DiT blocks after the `DiffusionParallelConfig` and attaches offload hooks to the DiT blocks after the
standard distributed groups have been initialized. standard distributed groups have been initialized.
AllGather mode selects one existing group for DLO weight sharding; no-AllGather The DLO weight-sharding group is selected as follows:
mode creates no DLO group. Neither mode changes ordinary TP or SP model
collectives. The mode-specific group and scheduling contracts are described 1. Use the existing DP group when `data_parallel_size > 1`.
below. 2. When DP is one and SP is greater than one, use the SP group.
3. Otherwise, run rank-locally without a DLO process group.
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 ### The loader owns host-weight planning
@@ -370,24 +105,32 @@ flags such as `_supports_mmap_loading` or parameter attributes for mmap-only
transforms. Model-specific direct-layout knowledge, when required, lives in a transforms. Model-specific direct-layout knowledge, when required, lives in a
checkpoint adapter beside the ordinary loader. checkpoint adapter beside the ordinary loader.
## AllGather design ### Phase-I normalized online-FP8 cache
### Group selection The direct-checkpoint plan above cannot quantize a BF16 source online. Phase I
adds an explicit, opt-in normalized cache for the narrower case of one TP1
transformer with per-tensor online FP8 and DLO AllGather:
The DLO weight-sharding group is selected in this order: ```text
cache miss: BF16 source -> ordinary online FP8 loader -> atomic FP8 cache
cache hit: FP8 cache -> direct mmap plan -> DLO host shard -> AllGather
```
1. Use the existing DP group when `data_parallel_size > 1`. The cache stores the serialized runtime representation, including finalized FP8
2. When DP is one and SP is greater than one, use the existing SP group. weights, generated scales, and persistent transformer buffers. Its manifest
3. Otherwise, use an effective group size of one and issue no collective. records the schema, component, quantization contract, tensor count, source
files, and a source fingerprint. A missing, stale, incomplete, incompatible, or
invalid artifact is a cache miss; cache publication is best-effort and falls
back to the ordinary loader on failure.
TP is deliberately not used as DLO's AllGather group. HSDP has its own Phase I keeps the cache binding deliberately simple: one source per transformer
parameter-sharding lifecycle and cannot be sharded a second time by DLO. component, with existing per-tensor transforms applied before publication. The
cache is not selected for DLO-disabled, no-AllGather, TP>1, or HSDP deployments,
and it normalizes the transformer only. Cache sharing and compatibility for
other components, transformed TP coordinates, and broader DP/SP layouts remain
in [RFC #6231](https://github.com/vllm-project/vllm-omni/issues/6231).
### Sharded transfer and double buffering ### AllGather path
The loader may provide ordinary runtime tensors or a compatible checkpoint
plan. During backend setup, either source is normalized into persistent local
shards. The final-layout host weight cache is not selected in AllGather mode.
With the default `dlo_use_allgather=True`, each rank stores approximately With the default `dlo_use_allgather=True`, each rank stores approximately
`1 / group_size` of each streamable block in pinned host memory. The next `1 / group_size` of each streamable block in pinned host memory. The next
@@ -395,15 +138,6 @@ 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 `all_gather_into_tensor` on a communication stream while the current block is
executing. executing.
![AllGather DLO sharded host weights and lockstep reconstruction](../../figures/dlo/dlo_allgather_sharded_host.svg)
The figure uses DP2 with TP2 to expose both parallel dimensions. DLO creates
one AllGather group across the DP replicas for each TP coordinate; TP0 workers
reconstruct TP0 blocks together and TP1 workers reconstruct TP1 blocks
together. Ordinary TP collectives remain inside each DP replica. Unlike the
no-AllGather host weight cache path, these persistent pinned host shards are
process-owned, and every participating rank must follow the same block order.
```text ```text
Compute: [Block N] [Block N+1] [Block N+2] Compute: [Block N] [Block N+1] [Block N+2]
H2D: [shard N+1] [shard N+2] H2D: [shard N+1] [shard N+2]
@@ -429,29 +163,11 @@ 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 the same denoising wave. Because AllGather is a collective, all participating
requests must take the same execution path at every denoising step. requests must take the same execution path at every denoising step.
Fast NVLink/NVSwitch changes collective cost, not this execution contract. For ### Rank-local path without DLO AllGather
independently scheduled replicas—even on one NVSwitch baseboard—no-AllGather
plus shared mmap storage avoids coupling their request schedules and failure
domains.
## No-AllGather design
### Rank-local transfer
With `--dlo-no-use-allgather`, DLO forces its internal offload shard size to With `--dlo-no-use-allgather`, DLO forces its internal offload shard size to
one and streams complete blocks using H2D copies only. The host backing may be one and streams complete blocks using H2D copies only. The host backing may be
a loader-approved checkpoint mapping, a normalized host weight cache mapping, or either a loader-approved checkpoint mapping or ordinary runtime tensors.
ordinary runtime tensors.
![No-AllGather DLO shared host weights and independent runtime orchestration](../../figures/dlo/dlo_no_allgather_shared_host.svg)
The figure uses TP2 to make per-coordinate physical sharing explicit. Each
engine contains two rank-local workers and devices. The cache keeps one
immutable entry per TP coordinate, so equivalent TP0 workers across engines
share the TP0 entry and equivalent TP1 workers share the separate TP1 entry.
An external router or orchestrator chooses each engine's requests; DLO
coordinates only the worker-local mapping, hooks, transfer streams, events,
and device slots.
For direct mmap, each process retains immutable safetensors views and uses two 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 bounded pinned host staging slots. Processes on the same node that map the same
@@ -460,17 +176,10 @@ 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 still packs and transfers every complete block. Sharing is node-local; each
node has its own page cache. node has its own page cache.
For a host weight cache mapping, the default pinned-memory policy attempts to When direct mmap preflight fails, the regular model loader remains responsible
replace those host slots with direct copies from the shared final tensor views. for preparing each rank's weights, including TP-local tensors or HSDP-managed
Disabling pinned memory or a failed registration retains staging behavior. parameters. In that fallback, each pure-DP process keeps a private full runtime
copy.
When direct mmap preflight fails or the host weight cache is selected, the
regular model loader remains responsible for preparing each rank's weights,
including TP-local tensors or HSDP-managed parameters. Ordinary pinned tensors
remain the default without cache selection. When the host weight cache is
explicitly selected,
supported CPU layouts enter the cache; unsupported layouts keep one private
runtime copy per process.
This mode means: This mode means:
@@ -481,109 +190,18 @@ This mode means:
- TP/HSDP/SP collectives, if configured, are not disabled by this flag; only - TP/HSDP/SP collectives, if configured, are not disabled by this flag; only
DLO's additional weight AllGather is disabled. DLO's additional weight AllGather is disabled.
- Pure DP deployments share one checkpoint-backed copy per node when direct - Pure DP deployments share one checkpoint-backed copy per node when direct
mmap is selected. The host weight cache can share equivalent ordinary-loader mmap is selected; the ordinary-loader fallback keeps one private runtime
outputs; its fallback keeps one private runtime copy per rank. copy per rank.
- The scheduler does not require a synchronized DP request wave for DLO. - The scheduler does not require a synchronized DP request wave for DLO.
### Post-load host weight cache
The Phase B cache preserves the same ownership boundary. DLO neither derives
cache keys nor interprets model loaders. No-AllGather DLO first attempts a
compatible direct-checkpoint mmap plan. When that plan is unavailable, every
rank completes the ordinary loader, custom callbacks, post-load processing,
strict validation, and calibration. When cache use or registration is
explicitly selected, the loader:
1. discovers final CPU DiT parameters and persistent buffers;
2. rejects unsupported non-contiguous, aliased, quantized, distributed, or
ambiguous layouts before changing the model;
3. hashes tensor names, metadata, and final bytes;
4. builds or joins one content-addressed entry under a strict POSIX lock; and
5. returns a `HostWeightPlan(backing_kind="host_weight_cache")` only after file,
manifest, and remapped-tensor content validation succeeds.
The identity excludes DP size/rank and SP rank. It includes TP world size/rank
and conservative SP implementation/world-size guards. DP replicas and
equivalent SP ranks can therefore share; different TP coordinates cannot.
Final tensor content is authoritative, so an unmodeled loader/config change
that changes bytes creates a different entry instead of risking silent
sharing.
Publication writes sharded safetensors and a JSON manifest into a same-
filesystem temporary directory, flushes them, and atomically renames the
directory. The manifest includes per-file SHA-256 digests and the final tensor
content digest. Kernel `flock` ownership distinguishes a live writer from a
stale lock file: process death releases the lock automatically. Stale temporary
entries are removed only by the next lock owner. A bounded wait or any cache
error retains the already-valid ordinary tensors.
DLO maps this plan without moving the DiT to `meta` and without rerunning any
post-load hook. It changes tensor storage while preserving `Parameter` objects,
then runs the existing `gc.collect()`/`malloc_trim()` cleanup before installing
streaming hooks. The source mappings remain open through DLO disable. With
pinned memory disabled, or when registration is unavailable, they feed the
existing bounded two-slot host-staging path. Every streamed block is first
packed from pageable mmap storage into a staging slot and is then copied to the
device. This removes model-sized private pinned storage, but adds a recurrent
host-to-host copy and can amplify TP synchronization waits when several ranks
compete for host bandwidth.
This version is deliberately steady-state-oriented. It requires roughly one
local-disk model copy per runtime identity, has no eviction, and still performs
ordinary loading plus full content-hash passes in every rank. Cache publication
or validation failure falls back to the already-valid ordinary host tensors.
Skip-load-on-hit is a separate follow-up after loader side effects can be
modeled safely.
### Registered host weight cache transfer
Selecting the host weight cache with `pin_cpu_memory=True` opts a worker into
registration of the complete mapping when its platform supports it. It also
changes loader selection: no-AllGather uses the ordinary loader and final-layout
host weight cache even when a TP1 direct-checkpoint plan is available. A
checkpoint binding can carry a deferred adapter such as
grouped-QKV reordering. Registering that raw source would not remove the
adapter's recurrent CPU allocation/copy, whereas the host weight cache contains
the transform-complete final tensors.
After mapping and validating every final tensor, DLO groups source storages by
backing file, page-aligns their address ranges, and coalesces overlapping or
adjacent ranges within that mapping. When a positive per-worker ceiling is
configured, the complete byte count is checked against it before any platform
mutation. The CUDA backend
first requires `cudaDevAttrHostRegisterReadOnlySupported`, then registers every
range with the read-only host-registration flag and verifies that PyTorch
recognizes every mapped tensor as pinned. A device without that capability
retains bounded staging; vLLM-Omni does not replace immutable mappings with a
writable or copy-on-write CUDA-specific layout. Other registration failures
roll back any ranges already registered and preserve the existing two-slot
staging path. Partial direct transfer is deliberately unsupported. A rollback
failure aborts worker initialization so a still-registered range is retained
until process/context teardown rather than being unsafely unmapped.
On success, the hooks copy each immutable tensor view directly into its offset
in the existing flattened rotating device buffer on the copy stream. Parameter
repointing, two-block HBM residency, and H2D/compute overlap are unchanged. The
direct path trades recurrent host packing for finer-grained H2D operations. A
packed cache schema should be considered only if that additional format and
publication complexity becomes necessary.
Registration is per process and platform context; physical file-cache pages
remain shared and are not copied into one private host allocation per worker.
The operator must nevertheless budget page-locked memory and satisfy the
platform and OS registration limits for every worker. During shutdown, DLO
first synchronizes outstanding device work, unregisters all ranges, and then
closes the safetensors mappings. Worker teardown invokes this cleanup before
destroying the distributed environment and platform context.
## Parallelism compatibility ## Parallelism compatibility
| Parallelism | DLO + AllGather | DLO without AllGather | | 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. Compatible TP1 replicas can share checkpoint pages; the optional host weight cache shares equivalent final layouts while excluding DP rank from its identity. CUDA workers may register the complete final mapping for direct H2D. | | **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 without a DLO weight collective. Host weight cache v1 excludes SP rank and includes conservative SP implementation/world-size guards. Registered transfer does not remove ordinary SP activation/attention collectives. | | **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** | Outside the Phase A direct-mmap scope. The loader preserves TP-local layouts and DLO may apply DP/SP host sharding to ordinary runtime tensors. | The ordinary TP-aware loader produces rank-local tensors. Host weight cache v1 creates one entry per TP coordinate, shareable by equivalent DP/SP processes. Registering those final mappings avoids the recurrent staging copy but does not remove ordinary TP collectives. | | **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 only with ordinary-loader rank-local tensors. HSDP owns parameter sharding and its own gathers; host weight cache v1 rejects HSDP/DTensor layouts. | | **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 ### Combined dimensions
@@ -599,9 +217,7 @@ destroying the distributed environment and platform context.
- **HSDP + DP or TP:** rejected independently by the diffusion parallel - **HSDP + DP or TP:** rejected independently by the diffusion parallel
configuration. configuration.
## Mode constraints ## Request and loading constraints
### AllGather constraints
AllGather DP multi-concurrency requires: AllGather DP multi-concurrency requires:
@@ -609,54 +225,109 @@ AllGather DP multi-concurrency requires:
- the same `num_inference_steps` for all requests in a wave; and - the same `num_inference_steps` for all requests in a wave; and
- identical request arguments that affect the collective execution path. - identical request arguments that affect the collective execution path.
Per-tensor online FP8 linears can use DLO AllGather after the ordinary loader
finalizes their runtime weights and scales. Other online methods must use
`--dlo-no-use-allgather` or disable online quantization until their runtime
layouts are validated.
### No-AllGather constraints
The no-AllGather path does not impose these DLO-specific synchronized-wave The no-AllGather path does not impose these DLO-specific synchronized-wave
requirements. Host weight cache v1 is no-AllGather only and rejects all requirements.
quantization configurations, HSDP/DTensor, expert parallelism, CFG
parallelism, PP, non-contiguous tensors, and tied/shared storage.
### Shared loading constraint
Direct checkpoint mmap can back either transfer path. It is currently limited 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 to proven TP1, non-HSDP, non-online-quantized layouts. Other layouts use the
ordinary loader. ordinary loader. Per-tensor online FP8 linears can use DLO AllGather after the
ordinary loader finalizes their runtime weights and scales; DLO then shards and
reconstructs those tensors with their recorded layouts. Other online methods
must use `--dlo-no-use-allgather` or disable online quantization until their
runtime layouts are validated.
## Validation status The Phase-I normalized online-FP8 cache is the supported TP1 transformer case.
The general cache-compatibility contract for additional components, transformed
TP coordinates, and cross-DP/SP reuse remains in
[RFC #6231](https://github.com/vllm-project/vllm-omni/issues/6231).
Historical test counts and hardware measurements are intentionally not kept ## Validation coverage
in this design document because they can drift as the implementation changes.
Revalidation must run against the final commit and publish commit-pinned Current source-level validation includes:
artifacts before making hardware or performance claims. Track that work in
[issue #6231](https://github.com/vllm-project/vllm-omni/issues/6231). - HSDP + DLO + AllGather rejection;
- HSDP + DLO without AllGather acceptance at configuration level;
- loader preflight fallback for TP, HSDP, online quantization, unknown custom
loaders, missing keys, and shape/dtype mismatches;
- ordinary-loader fallback for per-tensor online FP8 linears followed by DLO
sharding of finalized weights and scales;
- 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.
### 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 ## Recommendations
### AllGather - Use **DP + DLO AllGather** for the supported throughput and host-memory
scaling path.
- Use **DP + DLO AllGather** when ranks already execute the same block path in
lockstep and belong to the same fast P2P domain. It shards persistent host
weights across that group.
- Use **SP + DLO AllGather** for long-sequence workloads when DP concurrency is - Use **SP + DLO AllGather** for long-sequence workloads when DP concurrency is
not the goal. not the goal.
### No-AllGather
- Use **no-AllGather** when independent replica execution is required. TP1 - Use **no-AllGather** when independent replica execution is required. TP1
direct-mmap deployments can share checkpoint pages per node. Enable the direct-mmap deployments can share checkpoint pages per node; other layouts
host weight cache only when independent replicas have repeated final layouts retain the ordinary loader's private host memory behavior and are outside
and benchmark the target CPU/memory topology, especially for matching the Phase A shared-mmap support scope.
coordinates across multiple TP engines. With the default pinned-memory - Prefer **HSDP alone** for production HSDP deployments until the combined
policy, supported CUDA workers prefer registered direct H2D. Configure a HSDP + DLO no-AllGather path has broader end-to-end coverage.
positive ceiling when an explicit page-locking bound is required, or disable
pinned memory when pageable staging is operationally preferable.
### HSDP
- Do not combine HSDP with DLO AllGather or host weight cache v1. HSDP with
no-AllGather remains an ordinary-loader rank-local path.
@@ -1,165 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="850" viewBox="0 0 1280 850" role="img" aria-labelledby="title desc">
<title id="title">AllGather DLO sharded host weights and lockstep device reconstruction</title>
<desc id="desc">An external orchestrator submits one synchronized wave to a DP2 by TP2 diffusion engine. Each DP replica contains TP0 and TP1 workers with private pinned host shards and two rotating GPU weight slots. DLO AllGather groups connect matching TP coordinates across DP replicas, reconstructing full rank-local blocks on each device in lockstep. Ordinary TP groups remain within each DP replica.</desc>
<defs>
<marker id="request-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
</marker>
<marker id="h2d-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
<marker id="collective-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
</marker>
<style>
.canvas { fill: #ffffff; stroke: #e2e8f0; }
.node { fill: #f8fafc; stroke: #cbd5e1; }
.engine { fill: #ffffff; stroke: #cbd5e1; }
.replica { fill: #f8fafc; stroke: #cbd5e1; }
.router { fill: #f5f3ff; stroke: #c4b5fd; }
.scope { fill: #f8fafc; stroke: #cbd5e1; }
.host { fill: #fff7ed; stroke: #fdba74; }
.shard { fill: #d97706; }
.gpu { fill: #f0fdf4; stroke: #86efac; }
.slot-current { fill: #16a34a; }
.slot-next { fill: #bbf7d0; stroke: #22c55e; }
.lockstep { fill: #f5f3ff; stroke: #c4b5fd; }
.title { font: 700 25px system-ui, -apple-system, Segoe UI, sans-serif; fill: #0f172a; }
.subtitle { font: 500 13px system-ui, -apple-system, Segoe UI, sans-serif; fill: #64748b; }
.section { font: 700 15px system-ui, -apple-system, Segoe UI, sans-serif; fill: #1e293b; }
.section-sub { font: 500 11.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #64748b; }
.eyebrow { font: 700 10px system-ui, -apple-system, Segoe UI, sans-serif; letter-spacing: .8px; fill: #64748b; }
.box-title { font: 700 12px system-ui, -apple-system, Segoe UI, sans-serif; fill: #1e293b; }
.box-text { font: 500 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #475569; }
.shard-title { font: 700 13px system-ui, -apple-system, Segoe UI, sans-serif; fill: #ffffff; }
.shard-text { font: 500 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #ffffff; }
.slot-title { font: 700 10px system-ui, -apple-system, Segoe UI, sans-serif; fill: #ffffff; }
.slot-text { font: 500 9px system-ui, -apple-system, Segoe UI, sans-serif; fill: #ffffff; }
.slot-next-title { font: 700 10px system-ui, -apple-system, Segoe UI, sans-serif; fill: #166534; }
.slot-next-text { font: 500 9px system-ui, -apple-system, Segoe UI, sans-serif; fill: #166534; }
.request-label { font: 600 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #6d28d9; }
.h2d-label { font: 600 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #b45309; }
.collective-label { font: 700 10px system-ui, -apple-system, Segoe UI, sans-serif; fill: #6d28d9; }
.lock-title { font: 700 12px system-ui, -apple-system, Segoe UI, sans-serif; fill: #6d28d9; }
.lock-text { font: 500 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #5b21b6; }
.request-path { fill: none; stroke: #7c3aed; stroke-width: 2; marker-end: url(#request-arrow); }
.h2d-path { fill: none; stroke: #d97706; stroke-width: 2; marker-end: url(#h2d-arrow); }
.collective-path { fill: none; stroke: #7c3aed; stroke-width: 2.5; marker-start: url(#collective-arrow); marker-end: url(#collective-arrow); }
</style>
</defs>
<rect x="12" y="12" width="1256" height="826" rx="14" class="canvas"/>
<text id="title-text" x="40" y="50" class="title">AllGather DLO: sharded host weights, lockstep reconstruction</text>
<text x="40" y="74" class="subtitle">DP2 &#215; TP2 shown: DLO groups span DP replicas per TP coordinate; ordinary TP stays within each replica</text>
<!-- The external control plane submits one synchronized DP wave. -->
<rect x="390" y="94" width="500" height="68" rx="10" class="router"/>
<text x="640" y="121" text-anchor="middle" class="section">External router / orchestrator</text>
<text x="640" y="144" text-anchor="middle" class="section-sub">submits one synchronized DP wave &#183; outside vLLM-Omni DLO</text>
<rect x="30" y="184" width="1220" height="634" rx="14" class="node"/>
<text x="52" y="211" class="eyebrow">ONE NODE &#183; ONE SYNCHRONIZED ENGINE, PROCESS-OWNED HOST SHARDS</text>
<path d="M 640 162 L 640 232" class="request-path"/>
<text x="654" y="202" class="request-label">request wave</text>
<!-- One engine contains two DP replicas that execute in lockstep. -->
<rect x="45" y="232" width="1190" height="262" rx="12" class="engine"/>
<text x="68" y="261" class="section">One synchronized diffusion engine &#183; DP2 &#215; TP2</text>
<text x="68" y="281" class="section-sub">two DP requests follow the same block order; DLO reuses existing DP groups at each TP coordinate</text>
<!-- DP replica 0. -->
<rect x="65" y="296" width="515" height="186" rx="10" class="replica"/>
<text x="82" y="318" class="box-title">DP replica 0 &#183; request 0</text>
<text x="563" y="318" text-anchor="end" class="section-sub">ordinary TP group</text>
<rect x="82" y="332" width="473" height="64" rx="8" class="gpu"/>
<text x="98" y="351" class="eyebrow">TP0 &#183; GPU 0</text>
<text x="98" y="376" class="box-text">DLO backend &#183; local shard H2D</text>
<rect x="250" y="340" width="132" height="44" rx="6" class="slot-current"/>
<text x="316" y="357" text-anchor="middle" class="slot-title">SLOT 0 &#183; CURRENT</text>
<text x="316" y="374" text-anchor="middle" class="slot-text">full TP-local N &#183; compute</text>
<rect x="397" y="340" width="142" height="44" rx="6" class="slot-next"/>
<text x="468" y="357" text-anchor="middle" class="slot-next-title">SLOT 1 &#183; PREFETCH</text>
<text x="468" y="374" text-anchor="middle" class="slot-next-text">full TP-local N+1 after AG</text>
<rect x="82" y="410" width="473" height="64" rx="8" class="gpu"/>
<text x="98" y="429" class="eyebrow">TP1 &#183; GPU 1</text>
<text x="98" y="454" class="box-text">DLO backend &#183; local shard H2D</text>
<rect x="250" y="418" width="132" height="44" rx="6" class="slot-current"/>
<text x="316" y="435" text-anchor="middle" class="slot-title">SLOT 0 &#183; CURRENT</text>
<text x="316" y="452" text-anchor="middle" class="slot-text">full TP-local N &#183; compute</text>
<rect x="397" y="418" width="142" height="44" rx="6" class="slot-next"/>
<text x="468" y="435" text-anchor="middle" class="slot-next-title">SLOT 1 &#183; PREFETCH</text>
<text x="468" y="452" text-anchor="middle" class="slot-next-text">full TP-local N+1 after AG</text>
<!-- DP replica 1. -->
<rect x="700" y="296" width="515" height="186" rx="10" class="replica"/>
<text x="717" y="318" class="box-title">DP replica 1 &#183; request 1</text>
<text x="1198" y="318" text-anchor="end" class="section-sub">ordinary TP group</text>
<rect x="725" y="332" width="473" height="64" rx="8" class="gpu"/>
<text x="741" y="351" class="eyebrow">TP0 &#183; GPU 2</text>
<text x="741" y="376" class="box-text">DLO backend &#183; local shard H2D</text>
<rect x="893" y="340" width="132" height="44" rx="6" class="slot-current"/>
<text x="959" y="357" text-anchor="middle" class="slot-title">SLOT 0 &#183; CURRENT</text>
<text x="959" y="374" text-anchor="middle" class="slot-text">full TP-local N &#183; compute</text>
<rect x="1040" y="340" width="142" height="44" rx="6" class="slot-next"/>
<text x="1111" y="357" text-anchor="middle" class="slot-next-title">SLOT 1 &#183; PREFETCH</text>
<text x="1111" y="374" text-anchor="middle" class="slot-next-text">full TP-local N+1 after AG</text>
<rect x="725" y="410" width="473" height="64" rx="8" class="gpu"/>
<text x="741" y="429" class="eyebrow">TP1 &#183; GPU 3</text>
<text x="741" y="454" class="box-text">DLO backend &#183; local shard H2D</text>
<rect x="893" y="418" width="132" height="44" rx="6" class="slot-current"/>
<text x="959" y="435" text-anchor="middle" class="slot-title">SLOT 0 &#183; CURRENT</text>
<text x="959" y="452" text-anchor="middle" class="slot-text">full TP-local N &#183; compute</text>
<rect x="1040" y="418" width="142" height="44" rx="6" class="slot-next"/>
<text x="1111" y="435" text-anchor="middle" class="slot-next-title">SLOT 1 &#183; PREFETCH</text>
<text x="1111" y="452" text-anchor="middle" class="slot-next-text">full TP-local N+1 after AG</text>
<!-- DLO collectives connect matching TP coordinates across DP replicas. -->
<text x="640" y="350" text-anchor="middle" class="collective-label">DLO AG &#183; TP0 GROUP</text>
<path d="M 555 364 L 725 364" class="collective-path"/>
<text x="640" y="428" text-anchor="middle" class="collective-label">DLO AG &#183; TP1 GROUP</text>
<path d="M 555 442 L 725 442" class="collective-path"/>
<!-- All participants must reach the same collective in the same order. -->
<rect x="365" y="516" width="550" height="48" rx="8" class="lockstep"/>
<circle cx="395" cy="540" r="12" fill="#7c3aed"/>
<text x="395" y="545" text-anchor="middle" font-family="system-ui, sans-serif" font-size="15" font-weight="700" fill="#ffffff">&#8644;</text>
<text x="420" y="536" class="lock-title">DLO AllGather requires lockstep block order</text>
<text x="420" y="553" class="lock-text">every participant enters each block collective in the same sequence</text>
<!-- Each replica copies two process-owned host shards before reconstruction. -->
<path d="M 455 650 C 412 625, 340 610, 340 575 L 340 540 C 340 516, 318 500, 300 482" class="h2d-path"/>
<text x="276" y="618" text-anchor="middle" class="h2d-label">local shard H2D &#215;2</text>
<path d="M 825 650 C 868 625, 940 610, 940 575 L 940 540 C 940 516, 962 500, 980 482" class="h2d-path"/>
<text x="1004" y="618" text-anchor="middle" class="h2d-label">local shard H2D &#215;2</text>
<!-- Persistent host shards are private to workers, not mmap-shared. -->
<rect x="58" y="650" width="242" height="140" rx="10" class="scope"/>
<text x="78" y="676" class="eyebrow">GROUP TOPOLOGY</text>
<text x="78" y="702" class="box-title">DP2 &#215; TP2</text>
<text x="78" y="725" class="box-text">DLO groups: same TP coordinate</text>
<text x="78" y="746" class="box-text">across DP replicas</text>
<text x="78" y="767" class="box-text">TP stays the model-parallel group.</text>
<rect x="330" y="650" width="620" height="140" rx="10" class="host"/>
<text x="350" y="676" class="eyebrow">PROCESS-OWNED PINNED HOST SHARDS &#183; NOT HOST WEIGHT CACHE MMAP</text>
<rect x="370" y="692" width="260" height="60" rx="8" class="shard"/>
<text x="500" y="716" text-anchor="middle" class="shard-title">DP replica 0 &#183; private</text>
<text x="500" y="738" text-anchor="middle" class="shard-text">TP0 shard 0/2 &#183; TP1 shard 0/2</text>
<rect x="650" y="692" width="260" height="60" rx="8" class="shard"/>
<text x="780" y="716" text-anchor="middle" class="shard-title">DP replica 1 &#183; private</text>
<text x="780" y="738" text-anchor="middle" class="shard-text">TP0 shard 1/2 &#183; TP1 shard 1/2</text>
<text x="640" y="773" text-anchor="middle" class="box-text">Across each DLO group, shards total approximately one rank-local runtime block copy</text>
<rect x="980" y="650" width="242" height="140" rx="10" class="scope"/>
<text x="1000" y="676" class="eyebrow">HOT PATH &#183; PER WORKER</text>
<text x="1000" y="702" class="box-title">Prefetch next block</text>
<text x="1000" y="725" class="box-text">local pinned shard &#8594; GPU</text>
<text x="1000" y="746" class="box-text">DLO AllGather &#8594; full block</text>
<text x="1000" y="767" class="box-text">rotate slots; overlap compute</text>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -1,202 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="850" viewBox="0 0 1280 850" role="img" aria-labelledby="title desc">
<title id="title">No-AllGather DLO shared host weights and independent runtime orchestration</title>
<desc id="desc">An external orchestrator routes requests to two independently scheduled TP2 diffusion engines on one node. Each engine contains two rank-local workers and devices, with private loaders, mmap views, DLO runtime hooks, streams, events, and rotating GPU slots. Equivalent TP0 workers across engines map one immutable TP0 entry in the host weight cache, while equivalent TP1 workers map a separate TP1 entry, without a cross-engine DLO AllGather.</desc>
<defs>
<marker id="request-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
</marker>
<marker id="mapping-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
<marker id="runtime-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
</marker>
<style>
.canvas { fill: #ffffff; stroke: #e2e8f0; }
.node { fill: #f8fafc; stroke: #cbd5e1; }
.engine { fill: #ffffff; stroke: #cbd5e1; }
.router { fill: #f5f3ff; stroke: #c4b5fd; }
.scope { fill: #f8fafc; stroke: #cbd5e1; }
.cache { fill: #fff7ed; stroke: #fdba74; }
.payload { fill: #d97706; }
.loader { fill: #f1f5f9; stroke: #cbd5e1; }
.mapping { fill: #fffbeb; stroke: #f59e0b; }
.hook { fill: #eff6ff; stroke: #60a5fa; }
.gpu { fill: #f0fdf4; stroke: #86efac; }
.device { fill: #ffffff; stroke: #86efac; }
.slot-current { fill: #16a34a; }
.slot-next { fill: #bbf7d0; stroke: #22c55e; }
.no-collective { fill: #fef2f2; stroke: #fca5a5; }
.title { font: 700 25px system-ui, -apple-system, Segoe UI, sans-serif; fill: #0f172a; }
.subtitle { font: 500 13px system-ui, -apple-system, Segoe UI, sans-serif; fill: #64748b; }
.section { font: 700 15px system-ui, -apple-system, Segoe UI, sans-serif; fill: #1e293b; }
.section-sub { font: 500 11.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #64748b; }
.eyebrow { font: 700 10px system-ui, -apple-system, Segoe UI, sans-serif; letter-spacing: .8px; fill: #64748b; }
.box-title { font: 700 12px system-ui, -apple-system, Segoe UI, sans-serif; fill: #1e293b; }
.box-text { font: 500 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #475569; }
.payload-title { font: 700 14px system-ui, -apple-system, Segoe UI, sans-serif; fill: #ffffff; }
.payload-text { font: 500 11px system-ui, -apple-system, Segoe UI, sans-serif; fill: #ffffff; }
.device-title { font: 700 9px system-ui, -apple-system, Segoe UI, sans-serif; fill: #166534; }
.slot-text { font: 700 8px system-ui, -apple-system, Segoe UI, sans-serif; fill: #ffffff; }
.slot-next-text { font: 700 8px system-ui, -apple-system, Segoe UI, sans-serif; fill: #166534; }
.arrow-label { font: 600 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #475569; }
.request-label { font: 600 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #6d28d9; }
.mapping-label { font: 600 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #b45309; }
.no-title { font: 700 12px system-ui, -apple-system, Segoe UI, sans-serif; fill: #b91c1c; }
.no-text { font: 500 10.5px system-ui, -apple-system, Segoe UI, sans-serif; fill: #7f1d1d; }
.request-path { fill: none; stroke: #7c3aed; stroke-width: 2; marker-end: url(#request-arrow); }
.mapping-path { fill: none; stroke: #d97706; stroke-width: 2; marker-end: url(#mapping-arrow); }
.runtime-path { fill: none; stroke: #2563eb; stroke-width: 2; marker-end: url(#runtime-arrow); }
.startup-path { fill: none; stroke: #94a3b8; stroke-width: 1.5; stroke-dasharray: 4 3; marker-end: url(#runtime-arrow); }
</style>
</defs>
<rect x="12" y="12" width="1256" height="826" rx="14" class="canvas"/>
<text id="title-text" x="40" y="50" class="title">No-AllGather DLO: TP-coordinate host sharing, independent engines</text>
<text x="40" y="74" class="subtitle">TP2 shown: each engine owns two devices; equivalent workers share one host payload per TP coordinate</text>
<!-- The control plane remains outside DLO. -->
<rect x="390" y="94" width="500" height="68" rx="10" class="router"/>
<text x="640" y="121" text-anchor="middle" class="section">External router / orchestrator</text>
<text x="640" y="144" text-anchor="middle" class="section-sub">independent request placement &#183; outside vLLM-Omni DLO</text>
<!-- One node contains independent engine runtimes over one shared physical backing. -->
<rect x="30" y="184" width="1220" height="634" rx="14" class="node"/>
<text x="52" y="211" class="eyebrow">ONE NODE &#183; PROCESS-LOCAL ENGINES OVER SHARED PHYSICAL HOST MEMORY</text>
<!-- Independent request routing from the external orchestrator. -->
<path d="M 500 162 C 456 181, 400 204, 332 232" class="request-path"/>
<text x="416" y="194" class="request-label">request A</text>
<path d="M 780 162 C 824 181, 880 204, 948 232" class="request-path"/>
<text x="864" y="194" class="request-label">request B</text>
<!-- Independent engine A. -->
<rect x="45" y="232" width="575" height="250" rx="12" class="engine"/>
<text x="68" y="261" class="section">Independent engine A</text>
<text x="68" y="281" class="section-sub">TP2 &#183; two rank-local workers &#183; own request schedule and DLO state</text>
<rect x="68" y="310" width="98" height="130" rx="8" class="loader"/>
<text x="117" y="335" text-anchor="middle" class="eyebrow">STARTUP &#215;2</text>
<text x="117" y="359" text-anchor="middle" class="box-title">rank-local</text>
<text x="117" y="377" text-anchor="middle" class="box-title">loaders</text>
<text x="117" y="402" text-anchor="middle" class="box-text">final transforms</text>
<text x="117" y="421" text-anchor="middle" class="box-text">publish / join</text>
<rect x="180" y="310" width="112" height="130" rx="8" class="mapping"/>
<text x="236" y="335" text-anchor="middle" class="eyebrow">HOST VIEWS &#215;2</text>
<text x="236" y="359" text-anchor="middle" class="box-title">TP0 mmap</text>
<text x="236" y="377" text-anchor="middle" class="box-title">TP1 mmap</text>
<text x="236" y="402" text-anchor="middle" class="box-text">process-local</text>
<text x="236" y="421" text-anchor="middle" class="box-text">optional registration</text>
<rect x="306" y="310" width="120" height="130" rx="8" class="hook"/>
<text x="366" y="335" text-anchor="middle" class="eyebrow">RUNTIME &#215;2</text>
<text x="366" y="359" text-anchor="middle" class="box-title">DLO backends</text>
<text x="366" y="377" text-anchor="middle" class="box-title">+ block hooks</text>
<text x="366" y="402" text-anchor="middle" class="box-text">private streams</text>
<text x="366" y="421" text-anchor="middle" class="box-text">events + state</text>
<rect x="440" y="310" width="157" height="130" rx="8" class="gpu"/>
<text x="518" y="330" text-anchor="middle" class="eyebrow">TWO DEVICES</text>
<rect x="450" y="340" width="137" height="42" rx="6" class="device"/>
<text x="518" y="353" text-anchor="middle" class="device-title">TP0 &#183; GPU A0</text>
<rect x="457" y="359" width="58" height="16" rx="4" class="slot-current"/>
<text x="486" y="370" text-anchor="middle" class="slot-text">SLOT 0 NOW</text>
<rect x="521" y="359" width="58" height="16" rx="4" class="slot-next"/>
<text x="550" y="370" text-anchor="middle" class="slot-next-text">SLOT 1 NEXT</text>
<rect x="450" y="389" width="137" height="42" rx="6" class="device"/>
<text x="518" y="402" text-anchor="middle" class="device-title">TP1 &#183; GPU A1</text>
<rect x="457" y="408" width="58" height="16" rx="4" class="slot-current"/>
<text x="486" y="419" text-anchor="middle" class="slot-text">SLOT 0 NOW</text>
<rect x="521" y="408" width="58" height="16" rx="4" class="slot-next"/>
<text x="550" y="419" text-anchor="middle" class="slot-next-text">SLOT 1 NEXT</text>
<path d="M 166 375 L 180 375" class="startup-path"/>
<path d="M 292 375 L 306 375" class="runtime-path"/>
<path d="M 426 375 L 440 375" class="runtime-path"/>
<!-- Independent engine B. -->
<rect x="660" y="232" width="575" height="250" rx="12" class="engine"/>
<text x="683" y="261" class="section">Independent engine B</text>
<text x="683" y="281" class="section-sub">TP2 &#183; two rank-local workers &#183; own request schedule and DLO state</text>
<rect x="683" y="310" width="98" height="130" rx="8" class="loader"/>
<text x="732" y="335" text-anchor="middle" class="eyebrow">STARTUP &#215;2</text>
<text x="732" y="359" text-anchor="middle" class="box-title">rank-local</text>
<text x="732" y="377" text-anchor="middle" class="box-title">loaders</text>
<text x="732" y="402" text-anchor="middle" class="box-text">final transforms</text>
<text x="732" y="421" text-anchor="middle" class="box-text">publish / join</text>
<rect x="795" y="310" width="112" height="130" rx="8" class="mapping"/>
<text x="851" y="335" text-anchor="middle" class="eyebrow">HOST VIEWS &#215;2</text>
<text x="851" y="359" text-anchor="middle" class="box-title">TP0 mmap</text>
<text x="851" y="377" text-anchor="middle" class="box-title">TP1 mmap</text>
<text x="851" y="402" text-anchor="middle" class="box-text">process-local</text>
<text x="851" y="421" text-anchor="middle" class="box-text">optional registration</text>
<rect x="921" y="310" width="120" height="130" rx="8" class="hook"/>
<text x="981" y="335" text-anchor="middle" class="eyebrow">RUNTIME &#215;2</text>
<text x="981" y="359" text-anchor="middle" class="box-title">DLO backends</text>
<text x="981" y="377" text-anchor="middle" class="box-title">+ block hooks</text>
<text x="981" y="402" text-anchor="middle" class="box-text">private streams</text>
<text x="981" y="421" text-anchor="middle" class="box-text">events + state</text>
<rect x="1055" y="310" width="157" height="130" rx="8" class="gpu"/>
<text x="1133" y="330" text-anchor="middle" class="eyebrow">TWO DEVICES</text>
<rect x="1065" y="340" width="137" height="42" rx="6" class="device"/>
<text x="1133" y="353" text-anchor="middle" class="device-title">TP0 &#183; GPU B0</text>
<rect x="1072" y="359" width="58" height="16" rx="4" class="slot-current"/>
<text x="1101" y="370" text-anchor="middle" class="slot-text">SLOT 0 NOW</text>
<rect x="1136" y="359" width="58" height="16" rx="4" class="slot-next"/>
<text x="1165" y="370" text-anchor="middle" class="slot-next-text">SLOT 1 NEXT</text>
<rect x="1065" y="389" width="137" height="42" rx="6" class="device"/>
<text x="1133" y="402" text-anchor="middle" class="device-title">TP1 &#183; GPU B1</text>
<rect x="1072" y="408" width="58" height="16" rx="4" class="slot-current"/>
<text x="1101" y="419" text-anchor="middle" class="slot-text">SLOT 0 NOW</text>
<rect x="1136" y="408" width="58" height="16" rx="4" class="slot-next"/>
<text x="1165" y="419" text-anchor="middle" class="slot-next-text">SLOT 1 NEXT</text>
<path d="M 781 375 L 795 375" class="startup-path"/>
<path d="M 907 375 L 921 375" class="runtime-path"/>
<path d="M 1041 375 L 1055 375" class="runtime-path"/>
<!-- The runtimes are deliberately not coupled by a DLO collective. -->
<rect x="365" y="516" width="550" height="44" rx="8" class="no-collective"/>
<circle cx="395" cy="538" r="12" fill="#dc2626"/>
<path d="M 389 532 L 401 544 M 401 532 L 389 544" stroke="#ffffff" stroke-width="2.2"/>
<text x="420" y="534" class="no-title">No cross-engine DLO AllGather</text>
<text x="420" y="551" class="no-text">shared host pages do not couple request order, progress, or failure domains</text>
<text x="640" y="592" text-anchor="middle" class="section-sub">Ordinary TP / SP model collectives, when configured, remain inside each engine</text>
<!-- Each engine maps both TP-coordinate payloads from the shared host cache. -->
<path d="M 455 650 C 412 625, 340 610, 340 570 L 340 525 C 340 478, 278 454, 236 440" class="mapping-path"/>
<text x="286" y="617" text-anchor="middle" class="mapping-label">TP0 + TP1 mappings</text>
<path d="M 825 650 C 868 625, 940 610, 940 570 L 940 525 C 940 478, 893 454, 851 440" class="mapping-path"/>
<text x="994" y="617" text-anchor="middle" class="mapping-label">TP0 + TP1 mappings</text>
<!-- One node-local physical backing, visually beneath its consumers. -->
<rect x="58" y="650" width="242" height="140" rx="10" class="scope"/>
<text x="78" y="676" class="eyebrow">SHARING SCOPE</text>
<text x="78" y="702" class="box-title">TP2: two coordinate entries</text>
<text x="78" y="725" class="box-text">one physical copy per coordinate</text>
<text x="78" y="746" class="box-text">A/TP0 + B/TP0 &#8594; TP0 entry</text>
<text x="78" y="767" class="box-text">A/TP1 + B/TP1 &#8594; TP1 entry</text>
<rect x="330" y="650" width="620" height="140" rx="10" class="cache"/>
<text x="350" y="676" class="eyebrow">NODE-LOCAL IMMUTABLE HOST WEIGHT CACHE &#183; ONE COPY PER TP COORDINATE</text>
<rect x="370" y="692" width="260" height="60" rx="8" class="payload"/>
<text x="500" y="717" text-anchor="middle" class="payload-title">TP0 physical payload</text>
<text x="500" y="738" text-anchor="middle" class="payload-text">shared by A/TP0 + B/TP0</text>
<rect x="650" y="692" width="260" height="60" rx="8" class="payload"/>
<text x="780" y="717" text-anchor="middle" class="payload-title">TP1 physical payload</text>
<text x="780" y="738" text-anchor="middle" class="payload-text">shared by A/TP1 + B/TP1</text>
<text x="640" y="773" text-anchor="middle" class="box-text">Identity includes final layout + TP coordinate; engine and DP replica are excluded</text>
<rect x="980" y="650" width="242" height="140" rx="10" class="scope"/>
<text x="1000" y="676" class="eyebrow">WHAT STAYS PRIVATE</text>
<text x="1000" y="702" class="box-title">Per worker / process</text>
<text x="1000" y="725" class="box-text">mmap object and validation state</text>
<text x="1000" y="746" class="box-text">optional host registration</text>
<text x="1000" y="767" class="box-text">hooks, streams, events, GPU slots</text>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

@@ -1,127 +1,180 @@
# Distributed Layerwise Offloading # Distributed Layerwise Offloading
Distributed layerwise offloading (DLO) streams DiT blocks through two device Distributed layerwise offloading (DLO) extends block streaming to multi-device
buffers instead of keeping the complete DiT in HBM. It supports sharded host deployments. With AllGather enabled, each rank stores roughly `1 / dp_size` of
weights with AllGather or independently streamed rank-local weights. The the host weights and reconstructs each layer at runtime. Without AllGather,
no-AllGather path can optionally share node-local host weights through mmap. 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 [shared DLO component architecture](../../../design/feature/offloader/distributed_layerwise_offload.md#architecture) See the [DLO feature design](../../../design/feature/offloader/distributed_layerwise_offload.md)
for ownership and lifecycle coordination. The mode-specific internals are for the implementation contract and compatibility matrix.
documented separately in the
[AllGather design](../../../design/feature/offloader/distributed_layerwise_offload.md#allgather-design)
and
[no-AllGather design](../../../design/feature/offloader/distributed_layerwise_offload.md#no-allgather-design).
## Choose a mode ## Execution model
| Mode | Use when | Host weights | Runtime synchronization | DLO overlaps three operations with a fixed two-block device buffer:
| --- | --- | --- | --- |
| DLO AllGather (default) | DP ranks execute the same block path in lockstep | About `1 / dp_size` per rank | DLO weight AllGather |
| no-AllGather | Ranks or engines must schedule independently | Complete rank-local layout | No DLO weight collective |
| no-AllGather + host weight cache | Equivalent independent workers share one node | Shared final mmap layout per TP coordinate | No DLO weight collective |
| host weight cache + host registration | The platform supports registration and recurrent staging is too expensive | Shared registered mmap layout | No DLO weight collective |
AllGather is normally the best choice for synchronized DP. Host weight cache mode ```text
targets independently scheduled replicas, especially repeated TP engines on Compute stream: [Layer N] [Layer N+1] [Layer N+2]
one node. H2D stream: [H2D shard N+1] [H2D shard N+2]
AllGather: [AG N+1] [AG N+2]
Slots: slot 0: Layer N slot 1: Layer N+1
```
AllGather communicates only request-independent weight shards, so data-
parallel ranks may process different requests concurrently.
## Usage ## Usage
```bash ```bash
# Sharded host weights with DLO AllGather # Four ranks with sharded host weights and AllGather
vllm serve /path/to/model --omni \ vllm serve /path/to/model --omni \
--enable-distributed-layerwise-offload \ --enable-distributed-layerwise-offload \
--data-parallel-size 4 --data-parallel-size 4
# Independently streamed rank-local weights # Standard-loader rank-local weights, without DLO AllGather
vllm serve /path/to/model --omni \ vllm serve /path/to/model --omni \
--enable-distributed-layerwise-offload \ --enable-distributed-layerwise-offload \
--data-parallel-size 4 \
--dlo-no-use-allgather --dlo-no-use-allgather
# Share final host weights; prefer registered direct H2D under the pinned-memory policy # Sequence parallel deployment
vllm serve /path/to/model --omni \ vllm serve /path/to/model --omni \
--enable-distributed-layerwise-offload \ --enable-distributed-layerwise-offload \
--dlo-no-use-allgather \ --usp 4
--dlo-use-host-weight-cache
# Apply an 80 GiB per-worker safety ceiling to the same registration attempt # Opt-in normalized online-FP8 cache (TP1 + DLO AllGather)
vllm serve /path/to/model --omni \ vllm serve /path/to/model --omni \
--quantization fp8 \
--enable-distributed-layerwise-offload \ --enable-distributed-layerwise-offload \
--tensor-parallel-size 2 \ --dlo-fp8-cache-dir /path/to/node-local/fp8-cache
--dlo-no-use-allgather \
--dlo-use-host-weight-cache \
--dlo-host-weight-cache-pin-limit-gib 80
``` ```
## Relevant options ```python
from vllm_omni import Omni
omni = Omni(
model="/path/to/model",
enable_distributed_layerwise_offload=True,
dlo_use_allgather=True,
)
```
## Flags
| Flag | Meaning | Default | | Flag | Meaning | Default |
| --- | --- | --- | | --- | --- | --- |
| `--enable-distributed-layerwise-offload` | Enable DLO | `false` | | `--enable-distributed-layerwise-offload` | Enable DLO | `false` |
| `--dlo-no-use-allgather` | Stream complete rank-local blocks independently | `false` | | `--data-parallel-size N` | DP ranks and AllGather weight-sharding group | `1` |
| `--dlo-use-host-weight-cache` | Use the final-layout cache and prefer registration when pinned memory is enabled | `false` | | `--dlo-use-allgather` | Shard host weights and reconstruct with AllGather | `true` |
| `--dlo-host-weight-cache-pin-limit-gib GIB` | Optional per-worker registration ceiling; zero means no additional ceiling | `0` | | `--dlo-no-use-allgather` | Stream complete rank-local blocks without a DLO weight collective | `false` |
| `--dlo-resident-layers N` | Keep eligible leading DiT blocks resident in HBM | `0` | | `--dlo-resident-layers N` | Keep N leading main-DiT blocks on device; requires no-AllGather and model-declared resident paths | `0` |
| `--dlo-fp8-cache-dir PATH` | Opt into the Phase-I normalized online-FP8 cache for the transformer | unset |
No-AllGather DLO automatically uses a compatible checkpoint mmap plan. When ## Host-weight loading
checkpoint mmap is unavailable, it preserves ordinary pinned loader weights by
default. `--dlo-use-host-weight-cache` explicitly selects the transform-complete
final-layout cache. Under the default `pin_cpu_memory=True` policy, DLO attempts
to register the complete immutable mapping for direct H2D. Setting
`pin_cpu_memory=False` through structured or programmatic configuration retains
the staged mmap path instead. A positive pin limit is only an optional safety
ceiling and does not select the cache. The cache uses
`~/.cache/vllm-omni/dlo-host-weights`. Programmatic configuration may override
`dlo_host_weight_cache_dir` and the writer lock timeout; these advanced storage
controls are intentionally not separate CLI flags.
## Operational notes 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.
- All workers that should share must see the same local, disk-backed cache The shared-mmap optimization in this phase is supported only with TP1. TP
directory. Do not use tmpfs or a cross-node filesystem for this version. greater than one falls back before model mutation to ordinary TP-aware loading.
- Cache entries are immutable and validated before use. A cache or registration DLO may still consume those TP-local tensors, but this is a compatibility path:
failure keeps the ordinary loader weights or falls back to bounded staging. it does not share checkpoint-backed runtime weights across DP replicas and
- The bounded-staging fallback is a memory-first mode: recurrent pageable-mmap to provides no shared-mmap host-memory guarantee.
pinned-slot copies can reduce throughput, especially across concurrent TP
engines. Use it intentionally by disabling pinned host memory, or when the
active platform cannot register the immutable mapping.
- When a positive registration ceiling is configured, it must cover the
complete page-aligned mapping reported in the worker log. Registration is
all-or-nothing. Zero applies no additional ceiling.
- CUDA is the first registration backend and requires read-only host
registration support for immutable cache mappings. Platforms or devices
without that capability continue to use bounded staging; pinned-memory policy
requests direct H2D but does not guarantee it.
- Registration is process-local but does not duplicate the underlying file
pages. Each worker must still satisfy its platform and OS page-locking limits.
- Shutdown unregisters host ranges before closing their mmap handles.
- Host weight cache v1 has no automatic eviction. Stop all users of an entry
before deleting it.
## Scheduling constraints 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.
AllGather ranks must request blocks in the same collective order. Concurrent DP With direct checkpoint mmap, the loader:
requests may use different prompts, but they must follow the same denoising and
block-execution path and set the same explicit `num_inference_steps`.
No-AllGather workers do not have this DLO lockstep requirement. Ordinary TP or 1. saves non-persistent buffers such as RoPE frequencies;
SP model collectives still synchronize ranks within each engine. 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. 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.
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.
## Normalized online-FP8 cache
`--dlo-fp8-cache-dir` is an opt-in Phase-I cache for per-tensor online FP8 with
TP1 and DLO AllGather. On a cache miss, the ordinary loader quantizes the
transformer once and publishes finalized FP8 weights and scales as an atomic,
serialized safetensors artifact. On a valid cache hit, the loader selects that
serialized source before DLO planning, so the transformer can be loaded through
the direct checkpoint-mmap path without re-materializing the full BF16 source.
The cache is keyed to the source files through the manifest's schema, component,
quantization contract, and source fingerprint. Incomplete, incompatible, or
stale artifacts are treated as cache misses. Cache creation is best-effort: a
publication failure logs a warning and keeps the ordinary online-FP8 path.
Phase I normalizes the transformer only. Other pipeline components keep their
ordinary loading and quantization behavior. The cache is not selected when DLO
is disabled, AllGather is disabled, TP is greater than one, or HSDP is enabled.
## Declarative topology
Models may declare an `OffloadPlan` instead of embedding offload logic:
```python
from vllm_omni.diffusion.offloader import OffloadPlan
class MyPipeline(nn.Module):
_dit_modules = ["transformer"]
_offload_plan = OffloadPlan(
block_attrs={"transformer": ("blocks",)},
offload_submodules={"context_encoder": "layers"},
)
```
When no plan exists, discovery falls back to
`_layerwise_offload_blocks_attrs` and then heuristic attribute lookup.
## Data-parallel concurrency
With `data_parallel_size > 1` and AllGather enabled, the scheduler can process
up to `dp_size` requests per denoising step. Every concurrent request must set
the same explicit `num_inference_steps`; `None` is rejected because every rank
must enter each collective.
## Limitations ## Limitations
- Direct checkpoint mmap currently requires TP1. The host weight cache can share - Direct checkpoint mmap currently requires TP1. TP greater than one is
final ordinary-loader layouts between matching TP coordinates at TP1 or TP>1. 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.
- Per-tensor online FP8 linears use the ordinary loader and can run with either - Per-tensor online FP8 linears use the ordinary loader and can run with either
DLO transfer path. Other online quantization methods require no-AllGather DLO transfer path. With AllGather, every rank temporarily materializes the
until their runtime layouts are validated. complete FP8 model in host memory before DLO retains only its shard. Other
- Host weight cache v1 rejects quantized, non-contiguous, aliased/tied, online quantization methods require no-AllGather until their runtime layouts
device-only, HSDP, expert-parallel, CFG-parallel, and PP layouts. are validated.
- HSDP plus DLO AllGather is unsupported, and host weight cache v1 rejects HSDP. - Resident leading layers require `--dlo-no-use-allgather` and a model
- The host weight cache changes steady-state host backing, not startup loading: `OffloadPlan` that declares eligible `resident_dit_paths`.
each worker still performs ordinary loading and full content validation. - DP concurrency requires an explicit, identical inference-step count.
- Skip-load-on-hit, cache eviction, and cross-node sharing remain follow-up
work in Sharing transformed TP or quantized runtime layouts beyond this TP1 transformer
[RFC #6195](https://github.com/vllm-project/vllm-omni/issues/6195). cache remains a follow-up design in
[RFC #6231](https://github.com/vllm-project/vllm-omni/issues/6231).
See the [Cosmos3 DistOffload recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/cosmos3/Cosmos3-DistOffload.md) 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. for an end-to-end example.
@@ -152,7 +152,8 @@ In `DiffusionParallelConfig`:
**Constraints:** **Constraints:**
- `hsdp_replicate_size × hsdp_shard_size == world_size` - `hsdp_replicate_size × hsdp_shard_size == world_size`
- HSDP cannot be used with Tensor Parallelism (`tensor_parallel_size` must be 1) - HSDP cannot be used with Tensor Parallelism, ordinary Data Parallelism,
Pipeline Parallelism, or Expert Parallelism.
--- ---
+3 -50
View File
@@ -733,8 +733,7 @@ def test_diffusion_parallel_config_matches_diffusion_parallel_world_size_for_vae
def test_diffusion_parallel_config_supports_diffusion_hsdp_auto_sharding(): def test_diffusion_parallel_config_supports_diffusion_hsdp_auto_sharding():
cfg = OmniStageDiffusionParallelConfig( cfg = OmniStageDiffusionParallelConfig(
pipeline_parallel_size=2, ulysses_degree=4,
ulysses_degree=2,
use_hsdp=True, use_hsdp=True,
hsdp_shard_size=-1, hsdp_shard_size=-1,
hsdp_replicate_size=2, hsdp_replicate_size=2,
@@ -745,10 +744,10 @@ def test_diffusion_parallel_config_supports_diffusion_hsdp_auto_sharding():
def test_diffusion_parallel_config_rejects_hsdp_with_tp_or_dp(): def test_diffusion_parallel_config_rejects_hsdp_with_tp_or_dp():
with pytest.raises(ValueError, match="cannot be used with TP or DP"): with pytest.raises(ValueError, match="not compatible with TP"):
OmniStageDiffusionParallelConfig(tensor_parallel_size=2, use_hsdp=True, hsdp_shard_size=2) OmniStageDiffusionParallelConfig(tensor_parallel_size=2, use_hsdp=True, hsdp_shard_size=2)
with pytest.raises(ValueError, match="cannot be used with TP or DP"): with pytest.raises(ValueError, match="not compatible with DP"):
OmniStageDiffusionParallelConfig(data_parallel_size=2, use_hsdp=True, hsdp_shard_size=2) OmniStageDiffusionParallelConfig(data_parallel_size=2, use_hsdp=True, hsdp_shard_size=2)
@@ -908,52 +907,6 @@ def test_structured_diffusion_config_rejects_invalid_compile_granularity():
omni_config_module._DiffusionConfigProjection(diffusion_compile_granularity="block") omni_config_module._DiffusionConfigProjection(diffusion_compile_granularity="block")
@pytest.mark.parametrize(
("enable_dlo", "use_allgather"),
[(False, False), (True, True)],
)
@pytest.mark.parametrize(
("field_name", "field_value"),
[
("dlo_use_host_weight_cache", True),
("dlo_host_weight_cache_pin_limit_gib", 1.0),
],
)
def test_structured_diffusion_config_restricts_host_weight_cache_to_no_allgather_dlo(
enable_dlo,
use_allgather,
field_name,
field_value,
):
with pytest.raises(ValueError, match="requires no-AllGather distributed layerwise offload"):
omni_config_module._DiffusionConfigProjection(
enable_distributed_layerwise_offload=enable_dlo,
dlo_use_allgather=use_allgather,
**{field_name: field_value},
)
def test_structured_diffusion_config_requires_cache_for_registration_ceiling():
with pytest.raises(ValueError, match="requires dlo_use_host_weight_cache=True"):
omni_config_module._DiffusionConfigProjection(
enable_distributed_layerwise_offload=True,
dlo_use_allgather=False,
dlo_host_weight_cache_pin_limit_gib=1.0,
)
@pytest.mark.parametrize(
"field_name",
[
"dlo_host_weight_cache_lock_timeout",
"dlo_host_weight_cache_pin_limit_gib",
],
)
def test_structured_diffusion_config_rejects_non_finite_host_weight_cache_values(field_name):
with pytest.raises(ValueError, match=rf"{field_name} must be finite"):
omni_config_module._DiffusionConfigProjection(**{field_name: float("inf")})
def test_from_pipeline_config_matches_stage_config_to_omegaconf_behavior_for_representative_stage(): def test_from_pipeline_config_matches_stage_config_to_omegaconf_behavior_for_representative_stage():
pipeline = _resolve_pipeline_or_skip("qwen3_tts") pipeline = _resolve_pipeline_or_skip("qwen3_tts")
legacy_stage = merge_pipeline_deploy(pipeline, _load_default_deploy(pipeline))[0] legacy_stage = merge_pipeline_deploy(pipeline, _load_default_deploy(pipeline))[0]
@@ -74,14 +74,6 @@ def mock_dist(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(dist, "broadcast", lambda *args, **kwargs: None) monkeypatch.setattr(dist, "broadcast", lambda *args, **kwargs: None)
@pytest.fixture(autouse=True)
def mock_dit_group(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor.get_dit_group",
lambda: None,
)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_dist_vae_executor(monkeypatch: pytest.MonkeyPatch): def mock_dist_vae_executor(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(DistributedVaeExecutor, "gather_tensors", lambda self, x: [x]) monkeypatch.setattr(DistributedVaeExecutor, "gather_tensors", lambda self, x: [x])
@@ -92,9 +92,8 @@ def test_moe_ep_maps_diffusion_sp_cfg_dp_to_vllm_groups(monkeypatch):
monkeypatch.setattr(parallel_state, "get_forward_context", lambda: fake_forward_context) monkeypatch.setattr(parallel_state, "get_forward_context", lambda: fake_forward_context)
monkeypatch.setattr(parallel_state, "init_model_parallel_group", fake_init_model_parallel_group) monkeypatch.setattr(parallel_state, "init_model_parallel_group", fake_init_model_parallel_group)
monkeypatch.setattr(parallel_state, "init_vllm_model_parallel_group", fake_init_vllm_model_parallel_group) monkeypatch.setattr(parallel_state, "init_vllm_model_parallel_group", fake_init_vllm_model_parallel_group)
monkeypatch.setattr(parallel_state, "init_dit_group", lambda *_args, **_kwargs: None)
for name in ("_DP", "_CFG", "_SP", "_PP", "_FS", "_EXPERT_PARALLEL_GROUP_RANKS"): for name in ("_DP", "_CFG", "_SP", "_PP", "_EXPERT_PARALLEL_GROUP_RANKS"):
monkeypatch.setattr(parallel_state, name, None) monkeypatch.setattr(parallel_state, name, None)
for name in ("_TP", "_PCP", "_DP", "_EP", "_PP"): for name in ("_TP", "_PCP", "_DP", "_EP", "_PP"):
monkeypatch.setattr(parallel_state.vllm_parallel_state, name, None, raising=False) monkeypatch.setattr(parallel_state.vllm_parallel_state, name, None, raising=False)
@@ -229,9 +228,8 @@ def test_cfg_parallel_keeps_diffusion_dp_without_ep(monkeypatch):
monkeypatch.setattr(parallel_state.torch.distributed, "new_group", lambda ranks: tuple(ranks)) monkeypatch.setattr(parallel_state.torch.distributed, "new_group", lambda ranks: tuple(ranks))
monkeypatch.setattr(parallel_state, "get_world_group", lambda: fake_world_group) monkeypatch.setattr(parallel_state, "get_world_group", lambda: fake_world_group)
monkeypatch.setattr(parallel_state, "init_model_parallel_group", fake_init_model_parallel_group) monkeypatch.setattr(parallel_state, "init_model_parallel_group", fake_init_model_parallel_group)
monkeypatch.setattr(parallel_state, "init_dit_group", lambda *_args, **_kwargs: None)
for name in ("_DP", "_CFG", "_SP", "_PP", "_FS", "_EXPERT_PARALLEL_GROUP_RANKS"): for name in ("_DP", "_CFG", "_SP", "_PP", "_EXPERT_PARALLEL_GROUP_RANKS"):
monkeypatch.setattr(parallel_state, name, None) monkeypatch.setattr(parallel_state, name, None)
for name in ("_TP", "_PCP", "_DP", "_EP", "_PP"): for name in ("_TP", "_PCP", "_DP", "_EP", "_PP"):
monkeypatch.setattr(parallel_state.vllm_parallel_state, name, None, raising=False) monkeypatch.setattr(parallel_state.vllm_parallel_state, name, None, raising=False)
@@ -259,6 +257,33 @@ def test_cfg_parallel_keeps_diffusion_dp_without_ep(monkeypatch):
] ]
@pytest.mark.cpu
@pytest.mark.core_model
def test_destroy_model_parallel_clears_vllm_pipeline_group(monkeypatch):
"""A destroyed diffusion PP group must not block the next initialization."""
class _DestroyableGroup:
def __init__(self) -> None:
self.destroy_calls = 0
def destroy(self) -> None:
self.destroy_calls += 1
pipeline_group = _DestroyableGroup()
for name in ("_DP", "_CFG", "_SP"):
monkeypatch.setattr(parallel_state, name, None)
monkeypatch.setattr(parallel_state, "_PP", pipeline_group)
monkeypatch.setattr(parallel_state, "_EXPERT_PARALLEL_GROUP_RANKS", None)
for name in ("_DP", "_PCP", "_TP", "_EP"):
monkeypatch.setattr(parallel_state.vllm_parallel_state, name, None, raising=False)
monkeypatch.setattr(parallel_state.vllm_parallel_state, "_PP", pipeline_group, raising=False)
parallel_state.destroy_model_parallel()
assert pipeline_group.destroy_calls == 1
assert parallel_state.vllm_parallel_state._PP is None
@pytest.mark.cpu @pytest.mark.cpu
@pytest.mark.core_model @pytest.mark.core_model
def test_non_moe_ep_fails_before_vllm_ep_remap(monkeypatch): def test_non_moe_ep_fails_before_vllm_ep_remap(monkeypatch):
@@ -295,9 +320,8 @@ def test_non_moe_ep_fails_before_vllm_ep_remap(monkeypatch):
monkeypatch.setattr(parallel_state, "get_world_group", lambda: fake_world_group) monkeypatch.setattr(parallel_state, "get_world_group", lambda: fake_world_group)
monkeypatch.setattr(parallel_state, "get_forward_context", lambda: fake_forward_context) monkeypatch.setattr(parallel_state, "get_forward_context", lambda: fake_forward_context)
monkeypatch.setattr(parallel_state, "init_model_parallel_group", fake_init_model_parallel_group) monkeypatch.setattr(parallel_state, "init_model_parallel_group", fake_init_model_parallel_group)
monkeypatch.setattr(parallel_state, "init_dit_group", lambda *_args, **_kwargs: None)
for name in ("_DP", "_CFG", "_SP", "_PP", "_FS", "_EXPERT_PARALLEL_GROUP_RANKS"): for name in ("_DP", "_CFG", "_SP", "_PP", "_EXPERT_PARALLEL_GROUP_RANKS"):
monkeypatch.setattr(parallel_state, name, None) monkeypatch.setattr(parallel_state, name, None)
for name in ("_TP", "_PCP", "_DP", "_EP", "_PP"): for name in ("_TP", "_PCP", "_DP", "_EP", "_PP"):
monkeypatch.setattr(parallel_state.vllm_parallel_state, name, None, raising=False) monkeypatch.setattr(parallel_state.vllm_parallel_state, name, None, raising=False)
+2 -154
View File
@@ -234,7 +234,7 @@ class TestDiffusionParallelConfigHSDP:
def test_hsdp_cannot_use_with_tp(self): def test_hsdp_cannot_use_with_tp(self):
"""Test that HSDP and Tensor Parallelism cannot be used together.""" """Test that HSDP and Tensor Parallelism cannot be used together."""
with pytest.raises(ValueError, match="cannot be used with TP or DP"): with pytest.raises(ValueError, match="not compatible with TP"):
DiffusionParallelConfig( DiffusionParallelConfig(
tensor_parallel_size=2, tensor_parallel_size=2,
use_hsdp=True, use_hsdp=True,
@@ -243,7 +243,7 @@ class TestDiffusionParallelConfigHSDP:
def test_hsdp_cannot_use_with_dp(self): def test_hsdp_cannot_use_with_dp(self):
"""Test that HSDP and Data Parallelism cannot be used together.""" """Test that HSDP and Data Parallelism cannot be used together."""
with pytest.raises(ValueError, match="cannot be used with TP or DP"): with pytest.raises(ValueError, match="not compatible with DP"):
DiffusionParallelConfig( DiffusionParallelConfig(
data_parallel_size=2, data_parallel_size=2,
use_hsdp=True, use_hsdp=True,
@@ -264,158 +264,6 @@ class TestDiffusionParallelConfigHSDP:
assert config.hsdp_shard_size == 2 # auto: 4 // 2 assert config.hsdp_shard_size == 2 # auto: 4 // 2
class TestStandaloneHSDPDetection:
"""Tests for standalone HSDP detection and dit_parallel_size calculation.
These tests verify the logic used in initialize_model_parallel() to detect
standalone HSDP mode and calculate effective parallel sizes.
Standalone HSDP is when all non-HSDP parallelism dimensions are 1.
"""
@staticmethod
def compute_standalone_hsdp_params(
data_parallel_size: int = 1,
cfg_parallel_size: int = 1,
sequence_parallel_size: int = 1,
pipeline_parallel_size: int = 1,
tensor_parallel_size: int = 1,
fully_shard_degree: int = 1,
hsdp_replicate_size: int = 1,
) -> dict:
"""Compute standalone HSDP detection parameters.
This mirrors the logic in initialize_model_parallel().
"""
dit_parallel_size = (
data_parallel_size
* cfg_parallel_size
* sequence_parallel_size
* pipeline_parallel_size
* tensor_parallel_size
)
# Check for standalone HSDP: all non-HSDP parallelism dimensions are 1
is_standalone_hsdp = dit_parallel_size == 1 and fully_shard_degree > 1
# For standalone HSDP: use (fully_shard_degree * hsdp_replicate_size)
if is_standalone_hsdp:
effective_dit_parallel_size = fully_shard_degree * hsdp_replicate_size
else:
effective_dit_parallel_size = dit_parallel_size
effective_dp_size = (fully_shard_degree * hsdp_replicate_size) if is_standalone_hsdp else data_parallel_size
return {
"original_dit_parallel_size": dit_parallel_size,
"is_standalone_hsdp": is_standalone_hsdp,
"effective_dit_parallel_size": effective_dit_parallel_size,
"effective_dp_size": effective_dp_size,
}
def test_standalone_hsdp_basic(self):
"""Test basic standalone HSDP detection (shard_size=4, replicate=1)."""
result = self.compute_standalone_hsdp_params(
fully_shard_degree=4,
hsdp_replicate_size=1,
)
assert result["original_dit_parallel_size"] == 1
assert result["is_standalone_hsdp"] is True
assert result["effective_dit_parallel_size"] == 4
assert result["effective_dp_size"] == 4
def test_standalone_hsdp_with_replicate(self):
"""Test standalone HSDP with replication (shard_size=4, replicate=2)."""
result = self.compute_standalone_hsdp_params(
fully_shard_degree=4,
hsdp_replicate_size=2,
)
assert result["original_dit_parallel_size"] == 1
assert result["is_standalone_hsdp"] is True
assert result["effective_dit_parallel_size"] == 8 # 4 * 2
assert result["effective_dp_size"] == 8
def test_combined_hsdp_sp_not_standalone(self):
"""Test HSDP combined with SP is NOT detected as standalone.
This is a regression test for the bug where the condition
`dit_parallel_size == fully_shard_degree` incorrectly matched
combined modes like SP=4 + HSDP=4.
"""
result = self.compute_standalone_hsdp_params(
sequence_parallel_size=4,
fully_shard_degree=4,
hsdp_replicate_size=1,
)
assert result["original_dit_parallel_size"] == 4
assert result["is_standalone_hsdp"] is False
# Should NOT override dp_size for combined mode
assert result["effective_dp_size"] == 1 # original data_parallel_size
def test_combined_hsdp_cfg_not_standalone(self):
"""Test HSDP combined with CFG is NOT detected as standalone."""
result = self.compute_standalone_hsdp_params(
cfg_parallel_size=2,
fully_shard_degree=4,
hsdp_replicate_size=1,
)
assert result["original_dit_parallel_size"] == 2
assert result["is_standalone_hsdp"] is False
assert result["effective_dp_size"] == 1
def test_combined_hsdp_pp_not_standalone(self):
"""Test HSDP combined with PP is NOT detected as standalone."""
result = self.compute_standalone_hsdp_params(
pipeline_parallel_size=2,
fully_shard_degree=4,
hsdp_replicate_size=1,
)
assert result["original_dit_parallel_size"] == 2
assert result["is_standalone_hsdp"] is False
assert result["effective_dp_size"] == 1
def test_no_hsdp_not_standalone(self):
"""Test that no HSDP (fully_shard_degree=1) is NOT standalone."""
result = self.compute_standalone_hsdp_params(
fully_shard_degree=1,
)
assert result["original_dit_parallel_size"] == 1
assert result["is_standalone_hsdp"] is False
assert result["effective_dp_size"] == 1
def test_combined_multiple_parallelism_not_standalone(self):
"""Test HSDP combined with multiple parallelism is NOT standalone."""
result = self.compute_standalone_hsdp_params(
sequence_parallel_size=2,
cfg_parallel_size=2,
fully_shard_degree=4,
hsdp_replicate_size=1,
)
assert result["original_dit_parallel_size"] == 4 # 2 * 2
assert result["is_standalone_hsdp"] is False
assert result["effective_dp_size"] == 1
def test_standalone_hsdp_large_shard(self):
"""Test standalone HSDP with large shard size."""
result = self.compute_standalone_hsdp_params(
fully_shard_degree=8,
hsdp_replicate_size=1,
)
assert result["is_standalone_hsdp"] is True
assert result["effective_dit_parallel_size"] == 8
assert result["effective_dp_size"] == 8
def test_standalone_hsdp_large_replicate(self):
"""Test standalone HSDP with large replicate size."""
result = self.compute_standalone_hsdp_params(
fully_shard_degree=4,
hsdp_replicate_size=4,
)
assert result["is_standalone_hsdp"] is True
assert result["effective_dit_parallel_size"] == 16 # 4 * 4
assert result["effective_dp_size"] == 16
class TestHSDPShardConditions: class TestHSDPShardConditions:
"""Tests for _hsdp_shard_conditions matching logic.""" """Tests for _hsdp_shard_conditions matching logic."""
@@ -12,7 +12,6 @@ import torch
import torch.nn as nn import torch.nn as nn
from huggingface_hub import snapshot_download from huggingface_hub import snapshot_download
from vllm.config.load import LoadConfig from vllm.config.load import LoadConfig
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
from vllm_omni.diffusion.config import get_current_diffusion_config, get_current_diffusion_config_or_none from vllm_omni.diffusion.config import get_current_diffusion_config, get_current_diffusion_config_or_none
from vllm_omni.diffusion.data import DiffusionParallelConfig, OmniDiffusionConfig from vllm_omni.diffusion.data import DiffusionParallelConfig, OmniDiffusionConfig
@@ -310,7 +309,6 @@ def test_dlo_transfers_loader_plan_and_skips_ordinary_weight_loading(monkeypatch
assert loader.load_model(load_device="cpu") is model assert loader.load_model(load_device="cpu") is model
assert not loaded_ordinary_weights assert not loaded_ordinary_weights
assert plan.requires_backend
assert loader.take_host_weight_plan() is plan assert loader.take_host_weight_plan() is plan
assert loader.take_host_weight_plan() is None assert loader.take_host_weight_plan() is None
@@ -348,7 +346,7 @@ def test_dlo_plan_loads_component_sources_outside_planned_dit(monkeypatch):
backing_kind="checkpoint_mmap", backing_kind="checkpoint_mmap",
bindings={ bindings={
"transformer.weight": TensorBinding( "transformer.weight": TensorBinding(
storage_key="weight", checkpoint_key="weight",
file_path="unused", file_path="unused",
) )
}, },
@@ -376,9 +374,8 @@ def test_dlo_plan_loads_component_sources_outside_planned_dit(monkeypatch):
assert loader.take_host_weight_plan() is plan assert loader.take_host_weight_plan() is plan
def test_dlo_plan_fallback_retains_ordinary_loader_weights_by_default(monkeypatch): def test_dlo_plan_fallback_runs_ordinary_loader(monkeypatch):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
import vllm_omni.diffusion.model_loader.host_weight_cache as cache_mod
od_config = SimpleNamespace( od_config = SimpleNamespace(
dtype=torch.float32, dtype=torch.float32,
@@ -403,202 +400,11 @@ def test_dlo_plan_fallback_retains_ordinary_loader_weights_by_default(monkeypatc
lambda *_args, **_kwargs: HostWeightPlanResult(None, "not direct-compatible"), lambda *_args, **_kwargs: HostWeightPlanResult(None, "not direct-compatible"),
) )
def build_cache(*_args, **_kwargs):
pytest.fail("host weight cache must require explicit selection")
monkeypatch.setattr(cache_mod, "build_host_weight_cache_plan", build_cache)
assert loader.load_model(load_device="cpu") is model assert loader.load_model(load_device="cpu") is model
assert calls == ["load", "process"] assert calls == ["load", "process"]
assert loader.take_host_weight_plan() is None assert loader.take_host_weight_plan() is None
def test_dlo_host_weight_cache_is_built_after_final_post_load_mutation(monkeypatch, tmp_path):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
import vllm_omni.diffusion.model_loader.host_weight_cache as cache_mod
od_config = SimpleNamespace(
dtype=torch.float32,
parallel_config=SimpleNamespace(
use_hsdp=False,
tensor_parallel_size=1,
sequence_parallel_size=1,
data_parallel_size=2,
cfg_parallel_size=1,
pipeline_parallel_size=1,
ulysses_degree=1,
ring_degree=1,
allgather_degree=1,
ulysses_mode="strict",
),
quantization_config=None,
enable_distributed_layerwise_offload=True,
dlo_use_allgather=False,
dlo_use_host_weight_cache=True,
dlo_host_weight_cache_dir=str(tmp_path),
dlo_host_weight_cache_lock_timeout=1.0,
model="unused",
revision=None,
model_config={},
)
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
model = nn.Module()
model.transformer = nn.Linear(2, 2, bias=False)
model.transformer.quant_method = UnquantizedLinearMethod()
events: list[str] = []
runtime_plan = HostWeightPlan(
backing_kind="host_weight_cache",
bindings={},
runtime_layout_key="layout-key",
)
def load_weights(_model):
events.append("load")
with torch.no_grad():
model.transformer.weight.fill_(1)
def process_weights(_model, _device):
events.append("process")
with torch.no_grad():
model.transformer.weight.add_(1)
def calibrate(_model):
events.append("calibrate")
with torch.no_grad():
model.transformer.weight.add_(1)
original_eval = model.eval
def eval_model():
events.append("eval")
return original_eval()
def build_cache(_pipeline, **kwargs):
events.append("cache")
assert torch.equal(model.transformer.weight, torch.full((2, 2), 3.0))
assert kwargs["tensor_parallel_rank"] == 0
assert "data_parallel_size" not in kwargs["sequence_parallel_guard"]
assert kwargs["quantization_config"] is None
return HostWeightPlanResult(runtime_plan)
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
loader.load_weights = load_weights # type: ignore[method-assign]
loader._process_weights_after_loading = process_weights # type: ignore[method-assign]
loader._apply_skip_softmax_calibration = calibrate # type: ignore[method-assign]
model.eval = eval_model # type: ignore[method-assign]
monkeypatch.setattr(
loader_mod,
"build_checkpoint_mmap_plan",
lambda *_args, **_kwargs: HostWeightPlanResult(None, "requires normalization"),
)
monkeypatch.setattr(cache_mod, "build_host_weight_cache_plan", build_cache)
assert loader.load_model(load_device="cpu") is model
assert events == ["load", "process", "calibrate", "eval", "cache"]
assert loader.take_host_weight_plan() is runtime_plan
@pytest.mark.parametrize("pin_cpu_memory", [True, False])
def test_dlo_host_weight_cache_prefers_final_layout_over_checkpoint_plan(monkeypatch, tmp_path, pin_cpu_memory):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
import vllm_omni.diffusion.model_loader.host_weight_cache as cache_mod
od_config = SimpleNamespace(
dtype=torch.float32,
parallel_config=SimpleNamespace(
use_hsdp=False,
tensor_parallel_size=1,
sequence_parallel_size=1,
data_parallel_size=1,
cfg_parallel_size=1,
pipeline_parallel_size=1,
),
quantization_config=None,
enable_distributed_layerwise_offload=True,
dlo_use_allgather=False,
dlo_use_host_weight_cache=True,
pin_cpu_memory=pin_cpu_memory,
dlo_host_weight_cache_dir=str(tmp_path),
dlo_host_weight_cache_pin_limit_gib=0.0,
model="unused",
)
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
model = nn.Module()
model.transformer = nn.Linear(2, 2, bias=False)
events: list[str] = []
runtime_plan = HostWeightPlan(
backing_kind="host_weight_cache",
bindings={},
runtime_layout_key="layout-key",
)
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
loader.load_weights = lambda _model: events.append("load") # type: ignore[method-assign]
loader._process_weights_after_loading = lambda *_args: events.append("process") # type: ignore[method-assign]
loader._apply_skip_softmax_calibration = lambda _model: None # type: ignore[method-assign]
def unexpected_checkpoint_plan(*_args, **_kwargs):
pytest.fail("selected host weight cache mode must not select the raw checkpoint mmap plan")
def build_cache(_pipeline, **_kwargs):
events.append("cache")
return HostWeightPlanResult(runtime_plan)
monkeypatch.setattr(loader_mod, "build_checkpoint_mmap_plan", unexpected_checkpoint_plan)
monkeypatch.setattr(cache_mod, "build_host_weight_cache_plan", build_cache)
assert loader.load_model(load_device="cpu") is model
assert events == ["load", "process", "cache"]
assert loader.take_host_weight_plan() is runtime_plan
def test_dlo_host_weight_cache_failure_retains_ordinary_weights(monkeypatch, tmp_path):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
import vllm_omni.diffusion.model_loader.host_weight_cache as cache_mod
od_config = SimpleNamespace(
dtype=torch.float32,
parallel_config=SimpleNamespace(
use_hsdp=False,
tensor_parallel_size=1,
sequence_parallel_size=1,
data_parallel_size=2,
cfg_parallel_size=1,
pipeline_parallel_size=1,
),
quantization_config=None,
enable_distributed_layerwise_offload=True,
dlo_use_allgather=False,
dlo_use_host_weight_cache=True,
dlo_host_weight_cache_dir=str(tmp_path),
model="unused",
)
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
model = nn.Module()
model.transformer = nn.Linear(2, 2, bias=False)
model.transformer.quant_method = object()
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
loader.load_weights = lambda _model: None # type: ignore[method-assign]
loader._process_weights_after_loading = lambda *_args: None # 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, "requires normalization"),
)
def build_cache(_pipeline, **kwargs):
assert kwargs["quantization_config"] == "module_quant_method"
return HostWeightPlanResult(None, "writer timed out", "lock_timeout")
monkeypatch.setattr(cache_mod, "build_host_weight_cache_plan", build_cache)
original_weight = model.transformer.weight.detach().clone()
assert loader.load_model(load_device="cpu") is model
assert torch.equal(model.transformer.weight, original_weight)
assert loader.take_host_weight_plan() is None
def test_dlo_allgather_online_fp8_uses_ordinary_loader(monkeypatch): def test_dlo_allgather_online_fp8_uses_ordinary_loader(monkeypatch):
from vllm.model_executor.layers.quantization.online.fp8 import ( from vllm.model_executor.layers.quantization.online.fp8 import (
Fp8PerTensorOnlineLinearMethod, Fp8PerTensorOnlineLinearMethod,
@@ -0,0 +1,153 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for the Phase-I normalized online-FP8 cache contract."""
import json
from types import SimpleNamespace
import pytest
import torch
from safetensors.torch import load_file, save_file
from torch import nn
from vllm_omni.diffusion.model_loader.fp8_cache import (
CACHE_COMPLETE_NAME,
CACHE_COMPONENT_SUBFOLDER,
CACHE_MANIFEST_NAME,
_source_fingerprint,
_transformer_quant_config,
build_normalized_fp8_cache,
is_online_fp8_config,
load_fp8_cache_manifest,
)
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
def _write_valid_cache(tmp_path):
source = tmp_path / "source.safetensors"
save_file({"weight": torch.ones(2, 2)}, str(source))
cache = tmp_path / "cache"
component = cache / CACHE_COMPONENT_SUBFOLDER
component.mkdir(parents=True)
shard = component / "part-00000.safetensors"
save_file({"weight": torch.ones(2, 2, dtype=torch.float8_e4m3fn)}, str(shard))
(component / "model.safetensors.index.json").write_text(
json.dumps(
{
"metadata": {"total_size": 4},
"weight_map": {"weight": shard.name},
}
)
)
manifest = {
"schema_version": 1,
"cache_kind": "normalized_fp8_runtime",
"component": "transformer",
"component_subfolder": CACHE_COMPONENT_SUBFOLDER,
"source_prefix": "transformer.",
"quantization": {"method": "fp8", "serialized": True, "activation_scheme": "dynamic"},
"tensor_count": 1,
"source_files": [str(source)],
"source_fingerprint": _source_fingerprint({str(source)}),
}
cache.mkdir(exist_ok=True)
(cache / CACHE_MANIFEST_NAME).write_text(json.dumps(manifest))
(cache / CACHE_COMPLETE_NAME).write_text("phase1\n")
return cache, source, manifest
def test_online_and_serialized_fp8_configs_are_classified():
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
assert is_online_fp8_config(Fp8Config())
assert not is_online_fp8_config(Fp8Config(is_checkpoint_fp8_serialized=True))
cache_config = _transformer_quant_config()
assert not is_online_fp8_config(cache_config)
assert cache_config.resolve("transformer").is_checkpoint_fp8_serialized
assert cache_config.resolve("text_encoder") is None
def test_load_fp8_cache_manifest_validates_artifact_and_sources(tmp_path):
cache, source, expected = _write_valid_cache(tmp_path)
assert load_fp8_cache_manifest(cache) == expected
source.write_bytes(source.read_bytes() + b"changed")
assert load_fp8_cache_manifest(cache) is None
def test_load_fp8_cache_manifest_rejects_wrong_serialization_contract(tmp_path):
cache, _, manifest = _write_valid_cache(tmp_path)
manifest["quantization"]["serialized"] = False
(cache / CACHE_MANIFEST_NAME).write_text(json.dumps(manifest))
assert load_fp8_cache_manifest(cache) is None
def test_load_fp8_cache_manifest_rejects_path_traversal(tmp_path):
cache, _, manifest = _write_valid_cache(tmp_path)
index_path = cache / CACHE_COMPONENT_SUBFOLDER / "model.safetensors.index.json"
index_path.write_text(json.dumps({"weight_map": {"weight": "../outside.safetensors"}}))
assert load_fp8_cache_manifest(cache) is None
def test_load_fp8_cache_manifest_rejects_non_object_json(tmp_path):
cache, _, _ = _write_valid_cache(tmp_path)
(cache / CACHE_MANIFEST_NAME).write_text("[]")
assert load_fp8_cache_manifest(cache) is None
def test_build_normalized_fp8_cache_serializes_weight_and_scale(tmp_path):
from vllm.model_executor.layers.quantization.online.fp8 import (
Fp8PerTensorOnlineLinearMethod,
)
source_dir = tmp_path / "source"
source_dir.mkdir()
source_file = source_dir / "model.safetensors"
save_file({"block.weight": torch.ones((3, 2), dtype=torch.bfloat16)}, str(source_file))
model = nn.Module()
model._dit_modules = ("transformer",)
model.transformer = nn.Module()
model.transformer.block = nn.Module()
model.transformer.block.weight = nn.Parameter(torch.ones((2, 3), dtype=torch.float8_e4m3fn), requires_grad=False)
model.transformer.block.weight_scale = nn.Parameter(torch.tensor([0.5]), requires_grad=False)
model.transformer.block.quant_method = object.__new__(Fp8PerTensorOnlineLinearMethod)
source = SimpleNamespace(
model_or_path=str(source_dir),
subfolder=None,
revision=None,
prefix="transformer.",
)
cache_dir = tmp_path / "cache"
manifest = build_normalized_fp8_cache(
model,
sources=(source,),
cache_dir=cache_dir,
)
assert manifest["cache_kind"] == "normalized_fp8_runtime"
tensors = load_file(str(cache_dir / CACHE_COMPONENT_SUBFOLDER / "part-00000.safetensors"))
scale_tensors = load_file(str(cache_dir / CACHE_COMPONENT_SUBFOLDER / "part-00001.safetensors"))
assert tensors["block.weight"].dtype == torch.float8_e4m3fn
assert tuple(tensors["block.weight"].shape) == (3, 2)
assert torch.equal(scale_tensors["block.weight_scale"], torch.tensor([0.5]))
@pytest.mark.parametrize("missing", [CACHE_COMPLETE_NAME, CACHE_MANIFEST_NAME])
def test_load_fp8_cache_manifest_requires_completion_marker(tmp_path, missing):
cache, _, _ = _write_valid_cache(tmp_path)
(cache / missing).unlink()
assert load_fp8_cache_manifest(cache) is None
@@ -1,260 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import fcntl
import multiprocessing as mp
import shutil
from pathlib import Path
import pytest
import torch
from torch import nn
from vllm_omni.diffusion.model_loader.host_weight_cache import (
build_host_weight_cache_plan,
default_host_weight_cache_root,
)
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
_SPAWN_TIMEOUT_SECONDS = 120
class _LoaderMarker:
pass
class _Transformer(nn.Module):
def __init__(self):
super().__init__()
self.proj = nn.Linear(3, 2)
self.register_buffer("persistent", torch.arange(2, dtype=torch.float32))
self.register_buffer("temporary", torch.ones(1), persistent=False)
with torch.no_grad():
self.proj.weight.copy_(torch.arange(6, dtype=torch.float32).reshape(2, 3))
self.proj.bias.copy_(torch.tensor([7.0, 8.0]))
class _Pipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = _Transformer()
def test_default_root_is_not_split_by_replica_vllm_cache(monkeypatch):
monkeypatch.setenv("VLLM_CACHE_ROOT", "/tmp/replica-specific-vllm-cache")
assert default_host_weight_cache_root().endswith("/.cache/vllm-omni/dlo-host-weights")
def _build(pipeline: nn.Module, cache_root: Path, **overrides):
kwargs = {
"dit_modules": (("transformer", pipeline.transformer),),
"loader_type": _LoaderMarker,
"cache_root": cache_root,
"lock_timeout_seconds": 2.0,
"max_shard_bytes": 32,
"model_identity": "test/model",
"revision": "revision",
"runtime_dtype": torch.float32,
"load_format": "default",
"loader_inputs": {"model_config": {"hidden_size": 3}},
"tensor_parallel_size": 1,
"tensor_parallel_rank": 0,
"sequence_parallel_guard": {"sequence_parallel_size": 1, "backend": "none"},
"use_hsdp": False,
"enable_expert_parallel": False,
"quantization_config": None,
"cfg_parallel_size": 1,
"pipeline_parallel_size": 1,
}
kwargs.update(overrides)
return build_host_weight_cache_plan(pipeline, **kwargs)
def _multiprocess_builder(cache_root: str, queue) -> None:
result = _build(_Pipeline(), Path(cache_root))
queue.put(
(
result.fallback_code,
result.plan.runtime_layout_key if result.plan is not None else None,
sorted(binding.file_path for binding in result.plan.bindings.values()) if result.plan is not None else [],
)
)
def test_equivalent_process_layouts_reuse_one_entry(tmp_path):
first = _build(_Pipeline(), tmp_path)
second = _build(_Pipeline(), tmp_path)
assert first.plan is not None
assert second.plan is not None
assert first.plan.runtime_layout_key == second.plan.runtime_layout_key
assert not first.plan.requires_backend
assert {binding.file_path for binding in first.plan.bindings.values()} == {
binding.file_path for binding in second.plan.bindings.values()
}
assert set(first.plan.bindings) == {
"transformer.persistent",
"transformer.proj.bias",
"transformer.proj.weight",
}
def test_equivalent_local_model_paths_reuse_one_entry(tmp_path, monkeypatch):
model_dir = tmp_path / "model"
model_dir.mkdir()
model_link = tmp_path / "model-link"
model_link.symlink_to(model_dir, target_is_directory=True)
cache_root = tmp_path / "cache"
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.chdir(tmp_path)
identities = (str(model_dir), "~/model", "model", str(model_link))
results = [_build(_Pipeline(), cache_root, model_identity=identity) for identity in identities]
assert all(result.plan is not None for result in results)
assert len({result.plan.runtime_layout_key for result in results if result.plan is not None}) == 1
assert len(list((cache_root / "v1").iterdir())) == 1
def test_unstable_loader_identity_fails_closed(tmp_path):
result = _build(_Pipeline(), tmp_path, loader_inputs={"opaque": object()})
assert result.plan is None
assert result.fallback_code == "unstable_identity"
assert result.fallback_reason == "host weight cache identity does not support values of type builtins.object"
assert list(tmp_path.rglob("*.safetensors")) == []
def test_tp_coordinate_and_sp_implementation_guard_split_entries(tmp_path):
pipeline = _Pipeline()
tp0 = _build(pipeline, tmp_path, tensor_parallel_size=2, tensor_parallel_rank=0)
tp1 = _build(pipeline, tmp_path, tensor_parallel_size=2, tensor_parallel_rank=1)
sp2 = _build(
pipeline,
tmp_path,
sequence_parallel_guard={"sequence_parallel_size": 2, "backend": "ulysses"},
)
assert tp0.plan is not None and tp1.plan is not None and sp2.plan is not None
assert len({tp0.plan.runtime_layout_key, tp1.plan.runtime_layout_key, sp2.plan.runtime_layout_key}) == 3
def test_final_runtime_content_is_authoritative(tmp_path):
first_pipeline = _Pipeline()
second_pipeline = _Pipeline()
with torch.no_grad():
second_pipeline.transformer.proj.weight[0, 0] += 1
first = _build(first_pipeline, tmp_path)
second = _build(second_pipeline, tmp_path)
assert first.plan is not None and second.plan is not None
assert first.plan.runtime_layout_key != second.plan.runtime_layout_key
def test_expert_parallel_ownership_fails_closed(tmp_path):
result = _build(_Pipeline(), tmp_path, enable_expert_parallel=True)
assert result.plan is None
assert result.fallback_code == "unsupported_expert_parallel"
assert list(tmp_path.rglob("*.safetensors")) == []
def test_pipeline_parallel_ownership_fails_closed(tmp_path):
result = _build(_Pipeline(), tmp_path, pipeline_parallel_size=2)
assert result.plan is None
assert result.fallback_code == "unsupported_pp"
assert list(tmp_path.rglob("*.safetensors")) == []
def test_corrupt_published_entry_is_rebuilt_under_the_key_lock(tmp_path):
first = _build(_Pipeline(), tmp_path)
assert first.plan is not None
shard = Path(next(iter(first.plan.bindings.values())).file_path)
old_inode = shard.stat().st_ino
with shard.open("r+b") as handle:
handle.seek(-1, 2)
byte = handle.read(1)
handle.seek(-1, 2)
handle.write(bytes([byte[0] ^ 0xFF]))
second = _build(_Pipeline(), tmp_path)
assert second.plan is not None
assert second.plan.runtime_layout_key == first.plan.runtime_layout_key
assert Path(next(iter(second.plan.bindings.values())).file_path).stat().st_ino != old_inode
def test_lock_timeout_keeps_the_ordinary_tensors(tmp_path):
initial = _build(_Pipeline(), tmp_path)
assert initial.plan is not None
cache_key = initial.plan.runtime_layout_key
entry_dir = Path(next(iter(initial.plan.bindings.values())).file_path).parent
shutil.rmtree(entry_dir)
lock_path = tmp_path / ".locks" / f"{cache_key}.lock"
with lock_path.open("a+b") as lock_handle:
fcntl.flock(lock_handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
result = _build(_Pipeline(), tmp_path, lock_timeout_seconds=0.01)
assert result.plan is None
assert result.fallback_code == "lock_timeout"
def test_next_writer_removes_stale_temp_after_process_death(tmp_path):
initial = _build(_Pipeline(), tmp_path)
assert initial.plan is not None
cache_key = initial.plan.runtime_layout_key
entry_dir = Path(next(iter(initial.plan.bindings.values())).file_path).parent
shutil.rmtree(entry_dir)
stale_temp = tmp_path / ".tmp" / f"{cache_key}.stale-writer"
stale_temp.mkdir(parents=True)
(stale_temp / "partial").write_bytes(b"partial")
result = _build(_Pipeline(), tmp_path)
assert result.plan is not None
assert not stale_temp.exists()
@pytest.mark.parametrize("case", ["alias", "external_alias", "noncontiguous"])
def test_unsupported_tensor_layouts_fail_closed(tmp_path, case):
pipeline = _Pipeline()
if case == "alias":
pipeline.transformer.tied = pipeline.transformer.proj.weight
expected_code = "unsupported_alias"
elif case == "external_alias":
pipeline.encoder = nn.Module()
pipeline.encoder.shared = pipeline.transformer.proj.weight
expected_code = "unsupported_alias"
else:
pipeline.transformer.proj.weight = nn.Parameter(torch.arange(6, dtype=torch.float32).reshape(3, 2).t())
expected_code = "unsupported_tensor"
result = _build(pipeline, tmp_path)
assert result.plan is None
assert result.fallback_code == expected_code
assert list(tmp_path.rglob("*.safetensors")) == []
def test_concurrent_processes_publish_one_immutable_entry(tmp_path):
context = mp.get_context("spawn")
queue = context.Queue()
processes = [context.Process(target=_multiprocess_builder, args=(str(tmp_path), queue)) for _ in range(2)]
for process in processes:
process.start()
# Spawn workers import the full vLLM stack before entering the target;
# loaded CI hosts can spend more than 30 seconds in that startup path.
results = [queue.get(timeout=_SPAWN_TIMEOUT_SECONDS) for _ in processes]
for process in processes:
process.join(timeout=_SPAWN_TIMEOUT_SECONDS)
assert all(process.exitcode == 0 for process in processes)
assert all(code is None for code, _, _ in results)
assert len({key for _, key, _ in results}) == 1
assert len({tuple(paths) for _, _, paths in results}) == 1
assert len(list((tmp_path / "v1").iterdir())) == 1
@@ -1,346 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import ctypes
from types import SimpleNamespace
import pytest
import torch
import vllm_omni.diffusion.offloader.cuda_host_registration as registration_module
from vllm_omni.diffusion.offloader.cuda_host_registration import (
CudaHostRegistration,
CudaHostRegistrationBudgetError,
CudaHostRegistrationCleanupError,
CudaHostRegistrationError,
_coalesce_ranges,
)
from vllm_omni.diffusion.offloader.host_registration import (
HostRegistrationError,
register_host_mappings,
)
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
class _FakeStorage:
def __init__(self, pointer: int, size: int) -> None:
self._pointer = pointer
self._size = size
def data_ptr(self) -> int:
return self._pointer
def nbytes(self) -> int:
return self._size
class _FakeTensor:
device = SimpleNamespace(type="cpu")
def __init__(self, pointer: int, size: int, *, pinned: bool = True) -> None:
self._storage = _FakeStorage(pointer, size)
self._pinned = pinned
def untyped_storage(self) -> _FakeStorage:
return self._storage
def numel(self) -> int:
return self._storage.nbytes()
def is_pinned(self) -> bool:
return self._pinned
class _FakeRuntime:
def __init__(
self,
register_results: list[int | Exception],
unregister_results: list[int | Exception] | None = None,
*,
read_only_registration_supported: bool = True,
attribute_result: int = 0,
) -> None:
self._register_results = iter(register_results)
self._unregister_results = iter(unregister_results or [])
self._read_only_registration_supported = read_only_registration_supported
self._attribute_result = attribute_result
self.attribute_queries: list[tuple[int, int]] = []
self.registered: list[tuple[int, int, int]] = []
self.unregistered: list[int] = []
def cudaDeviceGetAttribute(self, value, attribute: int, device: int) -> int:
self.attribute_queries.append((attribute, device))
ctypes.cast(value, ctypes.POINTER(ctypes.c_int)).contents.value = int(self._read_only_registration_supported)
return self._attribute_result
def cudaHostRegister(self, pointer: int, size: int, flags: int) -> int:
self.registered.append((pointer, size, flags))
result = next(self._register_results)
if isinstance(result, Exception):
raise result
return result
def cudaHostUnregister(self, pointer: int) -> int:
self.unregistered.append(pointer)
result = next(self._unregister_results, 0)
if isinstance(result, Exception):
raise result
return result
@staticmethod
def cudaGetErrorString(error: int) -> str:
return f"error-{error}"
@pytest.fixture(autouse=True)
def _current_cuda_device(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.accelerator, "current_device_index", lambda: 0)
def test_platform_factory_reports_unsupported_registration() -> None:
with pytest.raises(HostRegistrationError, match="not supported on cpu"):
register_host_mappings({}, device=torch.device("cpu"), max_bytes=4096)
def test_platform_factory_dispatches_cuda(monkeypatch: pytest.MonkeyPatch) -> None:
sentinel = object()
sources = {"weights": []}
def create(actual_sources, *, max_bytes):
assert actual_sources is sources
assert max_bytes == 4096
return sentinel
monkeypatch.setattr(CudaHostRegistration, "create", staticmethod(create))
assert register_host_mappings(sources, device=torch.device("cuda"), max_bytes=4096) is sentinel
def test_platform_factory_allows_registration_without_additional_budget(monkeypatch: pytest.MonkeyPatch) -> None:
sentinel = object()
sources = {"weights": []}
def create(actual_sources, *, max_bytes):
assert actual_sources is sources
assert max_bytes is None
return sentinel
monkeypatch.setattr(CudaHostRegistration, "create", staticmethod(create))
assert register_host_mappings(sources, device=torch.device("cuda"), max_bytes=None) is sentinel
def test_coalesce_ranges_aligns_and_merges_overlapping_pages() -> None:
assert _coalesce_ranges(
[(0x1003, 4096), (0x2800, 1024), (0x9001, 1)],
page_size=4096,
) == (
registration_module._AddressRange(0x1000, 0x3000),
registration_module._AddressRange(0x9000, 0xA000),
)
def test_registration_rejects_over_budget_before_calling_cuda(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0])
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
with pytest.raises(CudaHostRegistrationBudgetError, match="exceeding"):
CudaHostRegistration.create(
{"weights": [_FakeTensor(0x1003, 4096)]}, # type: ignore[list-item]
max_bytes=4096,
)
assert runtime.registered == []
def test_registration_without_additional_budget_registers_complete_mapping(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0])
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
registration = CudaHostRegistration.create(
{"weights": [_FakeTensor(0x1003, 4096)]}, # type: ignore[list-item]
max_bytes=None,
)
assert registration.total_bytes == 8192
assert len(runtime.registered) == 1
assert registration.close() == []
def test_registration_rejects_unsupported_read_only_mapping(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0], read_only_registration_supported=False)
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
with pytest.raises(CudaHostRegistrationError, match="does not support read-only host registration"):
CudaHostRegistration.create(
{"weights": [_FakeTensor(0x1003, 1)]}, # type: ignore[list-item]
max_bytes=4096,
)
assert runtime.attribute_queries == [
(registration_module._CUDA_DEVICE_ATTRIBUTE_HOST_REGISTER_READ_ONLY_SUPPORTED, 0)
]
assert runtime.registered == []
def test_registration_consumes_capability_query_error(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0], attribute_result=7)
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
consumed_errors: list[int] = []
monkeypatch.setattr(registration_module, "_consume_last_cuda_error", consumed_errors.append)
with pytest.raises(CudaHostRegistrationError, match="error-7"):
CudaHostRegistration.create(
{"weights": [_FakeTensor(0x1003, 1)]}, # type: ignore[list-item]
max_bytes=4096,
)
assert consumed_errors == [7]
assert runtime.registered == []
def test_registration_rolls_back_partial_success(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0, 7])
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
consumed_errors: list[int] = []
monkeypatch.setattr(registration_module, "_consume_last_cuda_error", consumed_errors.append)
with pytest.raises(CudaHostRegistrationError, match="error-7"):
CudaHostRegistration.create(
{
"first": [_FakeTensor(0x1003, 1)], # type: ignore[list-item]
"second": [_FakeTensor(0x9003, 1)], # type: ignore[list-item]
},
max_bytes=8192,
)
assert runtime.unregistered == [0x1000]
assert consumed_errors == [7]
def test_registration_wraps_runtime_exception_and_rolls_back(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0, RuntimeError("driver rejected mapping")])
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
with pytest.raises(CudaHostRegistrationError, match="driver rejected mapping"):
CudaHostRegistration.create(
{
"first": [_FakeTensor(0x1003, 1)], # type: ignore[list-item]
"second": [_FakeTensor(0x9003, 1)], # type: ignore[list-item]
},
max_bytes=8192,
)
assert runtime.unregistered == [0x1000]
def test_registration_fails_closed_when_rollback_fails(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0, 7], unregister_results=[9])
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
consumed_errors: list[int] = []
monkeypatch.setattr(registration_module, "_consume_last_cuda_error", consumed_errors.append)
with pytest.raises(CudaHostRegistrationCleanupError, match="rollback errors"):
CudaHostRegistration.create(
{
"first": [_FakeTensor(0x1003, 1)], # type: ignore[list-item]
"second": [_FakeTensor(0x9003, 1)], # type: ignore[list-item]
},
max_bytes=8192,
)
assert consumed_errors == [7, 9]
@pytest.mark.parametrize("pending_error", [0, 801])
def test_consume_last_cuda_error_accepts_cleared_or_matching_state(
monkeypatch: pytest.MonkeyPatch,
pending_error: int,
) -> None:
class _GetLastError:
def __call__(self) -> int:
return pending_error
runtime = SimpleNamespace(cudaGetLastError=_GetLastError())
monkeypatch.setattr(registration_module.ctypes, "CDLL", lambda _name: runtime)
registration_module._consume_last_cuda_error(801)
def test_consume_last_cuda_error_rejects_unrelated_pending_error(monkeypatch: pytest.MonkeyPatch) -> None:
class _GetLastError:
def __call__(self) -> int:
return 700
runtime = SimpleNamespace(cudaGetLastError=_GetLastError())
monkeypatch.setattr(registration_module.ctypes, "CDLL", lambda _name: runtime)
with pytest.raises(CudaHostRegistrationCleanupError, match="cudaGetLastError reported 700"):
registration_module._consume_last_cuda_error(801)
def test_registration_closes_all_coalesced_ranges(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0, 0])
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
registration = CudaHostRegistration.create(
{
"first": [_FakeTensor(0x1003, 4096), _FakeTensor(0x2800, 1024)], # type: ignore[list-item]
"second": [_FakeTensor(0x9003, 1)], # type: ignore[list-item]
},
max_bytes=12288,
)
assert registration.total_bytes == 12288
assert registration.region_count == 2
assert registration.close() == []
assert runtime.unregistered == [0x9000, 0x1000]
assert registration.close() == []
def test_registration_keeps_adjacent_file_mappings_separate(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0, 0])
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
registration = CudaHostRegistration.create(
{
"first.safetensors": [_FakeTensor(0x1000, 4096)], # type: ignore[list-item]
"second.safetensors": [_FakeTensor(0x2000, 4096)], # type: ignore[list-item]
},
max_bytes=8192,
)
assert registration.region_count == 2
assert runtime.registered == [
(0x1000, 4096, registration_module._CUDA_HOST_REGISTER_READ_ONLY),
(0x2000, 4096, registration_module._CUDA_HOST_REGISTER_READ_ONLY),
]
assert registration.close() == []
def test_registration_close_retries_failed_ranges(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(registration_module.torch.cuda, "is_available", lambda: True)
runtime = _FakeRuntime([0], unregister_results=[9, 0])
monkeypatch.setattr(registration_module.torch.cuda, "cudart", lambda: runtime)
consumed_errors: list[int] = []
monkeypatch.setattr(registration_module, "_consume_last_cuda_error", consumed_errors.append)
registration = CudaHostRegistration.create(
{"weights": [_FakeTensor(0x1003, 1)]}, # type: ignore[list-item]
max_bytes=4096,
)
assert registration.close() == ["cudaHostUnregister(0x1000) failed: error-9"]
assert registration.close() == []
assert runtime.unregistered == [0x1000, 0x1000]
assert consumed_errors == [9]
@@ -18,7 +18,6 @@ from torch import nn
from torch.distributed.tensor import DeviceMesh, DTensor, Replicate from torch.distributed.tensor import DeviceMesh, DTensor, Replicate
import vllm_omni.diffusion.offloader.distributed_layerwise_backend as dist_backend_module import vllm_omni.diffusion.offloader.distributed_layerwise_backend as dist_backend_module
from vllm_omni.diffusion.model_loader.host_weight_cache_utils import file_digest
from vllm_omni.diffusion.model_loader.host_weight_plan import ( from vllm_omni.diffusion.model_loader.host_weight_plan import (
HostWeightPlan, HostWeightPlan,
TensorBinding, TensorBinding,
@@ -547,31 +546,6 @@ class TestDistributedLayerwiseOffloadHook:
assert staging[0][torch.float32].numel() == 12 assert staging[0][torch.float32].numel() == 12
assert staging[1][torch.float32].numel() == 12 assert staging[1][torch.float32].numel() == 12
def test_registered_mmap_copies_directly_without_staging(self, patched_offload_runtime):
current_block = nn.Linear(2, 2, bias=False)
next_block = nn.Linear(2, 2, bias=False)
expected = torch.arange(4, dtype=torch.float32).view(2, 2)
next_block.weight.data.copy_(expected)
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)
hook.registered_mmap = True
def fail_staging(_slot):
raise AssertionError("registered mmap must bypass host staging")
hook._stage_mmap_sources = fail_staging # type: ignore[method-assign]
hook.prefetch_layer(slot=0, non_blocking=True)
assert torch.equal(next_block.weight, expected)
class TestPinnedResidentLayerGroup: class TestPinnedResidentLayerGroup:
def test_load_offload_reuses_pinned_master_weights(self, patched_offload_runtime): def test_load_offload_reuses_pinned_master_weights(self, patched_offload_runtime):
@@ -662,24 +636,6 @@ class TestPinnedResidentLayerGroup:
group.offload() group.offload()
assert all(block.weight.numel() == 0 for block in blocks) assert all(block.weight.numel() == 0 for block in blocks)
def test_registered_mmap_resident_layers_bypass_staging(self, patched_offload_runtime):
block = nn.Linear(2, 2, bias=False)
expected = torch.arange(4, dtype=torch.float32).view(2, 2)
block.weight.data.copy_(expected)
group = PinnedResidentLayerGroup(
[block],
device=torch.device("cpu"),
copy_stream=DummyStream(),
pin_memory=False,
rank_local_mmap=True,
)
group.registered_mmap = True
group._cpu_staging_buffers.clear()
group.load()
assert torch.equal(block.weight, expected)
def test_shared_allgather_output_is_narrowed_to_current_block( def test_shared_allgather_output_is_narrowed_to_current_block(
self, dist_group, patched_offload_runtime, monkeypatch self, dist_group, patched_offload_runtime, monkeypatch
): ):
@@ -809,140 +765,6 @@ class _MmapPostLoadPipeline(nn.Module):
class TestMmapWeightLoading: class TestMmapWeightLoading:
def test_host_weight_cache_maps_final_weights_without_rerunning_post_load(
self,
tmp_path,
patched_offload_runtime,
):
class Transformer(nn.Module):
def __init__(self):
super().__init__()
self.proj = nn.Linear(2, 2, bias=False)
self.post_load_calls = 0
def post_load_weights(self):
self.post_load_calls += 1
class Pipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = Transformer()
pipeline = Pipeline()
old_parameter = pipeline.transformer.proj.weight
old_parameter.runtime_marker = "preserved"
weight_file = tmp_path / "runtime.safetensors"
save_file({"transformer.proj.weight": torch.full((2, 2), 9.0)}, str(weight_file))
plan = HostWeightPlan(
backing_kind="host_weight_cache",
bindings={
"transformer.proj.weight": TensorBinding(
storage_key="transformer.proj.weight",
file_path=str(weight_file),
)
},
runtime_layout_key="runtime-layout",
expected_file_digests={str(weight_file): file_digest(weight_file)},
)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
dlo_use_allgather=False,
),
torch.device("cpu"),
host_weight_plan=plan,
)
modules = SimpleNamespace(dits=[pipeline.transformer], dit_names=["transformer"])
backend._load_weights_from_host_weight_cache(pipeline, modules, plan)
assert pipeline.transformer.post_load_calls == 0
assert pipeline.transformer.proj.weight is old_parameter
assert pipeline.transformer.proj.weight.runtime_marker == "preserved"
assert torch.equal(pipeline.transformer.proj.weight, torch.full((2, 2), 9.0))
backend._release_mmap_handles()
def test_host_weight_cache_mapping_failure_leaves_ordinary_tensors_intact(
self,
tmp_path,
patched_offload_runtime,
):
pipeline = nn.Module()
pipeline.transformer = nn.Linear(2, 2, bias=False)
old_parameter = pipeline.transformer.weight
plan = HostWeightPlan(
backing_kind="host_weight_cache",
bindings={
"transformer.weight": TensorBinding(
storage_key="transformer.weight",
file_path=str(tmp_path / "missing.safetensors"),
)
},
runtime_layout_key="runtime-layout",
expected_file_digests={str(tmp_path / "missing.safetensors"): "unused"},
)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
dlo_use_allgather=False,
),
torch.device("cpu"),
host_weight_plan=plan,
)
modules = SimpleNamespace(dits=[pipeline.transformer], dit_names=["transformer"])
with pytest.raises(Exception, match="missing.safetensors"):
backend._load_weights_from_host_weight_cache(pipeline, modules, plan)
assert pipeline.transformer.weight is old_parameter
def test_host_weight_cache_digest_change_leaves_ordinary_tensors_intact(
self,
tmp_path,
patched_offload_runtime,
):
pipeline = nn.Module()
pipeline.transformer = nn.Linear(2, 2, bias=False)
old_parameter = pipeline.transformer.weight
old_value = old_parameter.detach().clone()
weight_file = tmp_path / "runtime.safetensors"
save_file({"transformer.weight": torch.full((2, 2), 9.0)}, str(weight_file))
expected_digest = file_digest(weight_file)
with weight_file.open("r+b") as handle:
handle.seek(-1, os.SEEK_END)
original = handle.read(1)
handle.seek(-1, os.SEEK_END)
handle.write(bytes([original[0] ^ 0xFF]))
plan = HostWeightPlan(
backing_kind="host_weight_cache",
bindings={
"transformer.weight": TensorBinding(
storage_key="transformer.weight",
file_path=str(weight_file),
)
},
runtime_layout_key="runtime-layout",
expected_file_digests={str(weight_file): expected_digest},
)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
dlo_use_allgather=False,
),
torch.device("cpu"),
host_weight_plan=plan,
)
modules = SimpleNamespace(dits=[pipeline.transformer], dit_names=["transformer"])
with pytest.raises(RuntimeError, match="changed after loader validation"):
backend._load_weights_from_host_weight_cache(pipeline, modules, plan)
assert pipeline.transformer.weight is old_parameter
assert torch.equal(pipeline.transformer.weight, old_value)
def test_runs_model_post_load_hook(self, tmp_path, patched_offload_runtime): def test_runs_model_post_load_hook(self, tmp_path, patched_offload_runtime):
pipeline = _MmapPostLoadPipeline() pipeline = _MmapPostLoadPipeline()
weights = {name: torch.ones(param.shape, dtype=torch.bfloat16) for name, param in pipeline.named_parameters()} weights = {name: torch.ones(param.shape, dtype=torch.bfloat16) for name, param in pipeline.named_parameters()}
@@ -966,7 +788,7 @@ class TestMmapWeightLoading:
backing_kind="checkpoint_mmap", backing_kind="checkpoint_mmap",
bindings={ bindings={
name: TensorBinding( name: TensorBinding(
storage_key=name, checkpoint_key=name,
file_path=str(weight_file), file_path=str(weight_file),
) )
for name in weights for name in weights
@@ -1037,292 +859,6 @@ class TestMmapWeightLoading:
assert all(hook.rank_local_mmap for group in backend._all_hook_groups for hook in group) assert all(hook.rank_local_mmap for group in backend._all_hook_groups for hook in group)
backend.disable() backend.disable()
def test_host_weight_cache_enable_reclaims_private_weights_before_hook_setup(
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) for _ in range(2)])
class Pipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = Transformer()
pipeline = Pipeline()
weights = {name: torch.full_like(param, 4) for name, param in pipeline.named_parameters()}
weight_file = tmp_path / "runtime.safetensors"
save_file(weights, str(weight_file))
plan = HostWeightPlan(
backing_kind="host_weight_cache",
bindings={name: TensorBinding(storage_key=name, file_path=str(weight_file)) for name in weights},
runtime_layout_key="runtime-layout",
expected_file_digests={str(weight_file): file_digest(weight_file)},
)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
dlo_use_allgather=False,
),
torch.device("cpu"),
host_weight_plan=plan,
)
cleanup_calls: list[None] = []
backend._cleanup_after_loading = lambda: cleanup_calls.append(None) # type: ignore[method-assign]
backend.enable(pipeline)
assert backend._using_rank_local_mmap
# One reclamation immediately after remap, plus the existing final
# post-hook cleanup.
assert len(cleanup_calls) == 2
backend.disable()
def test_host_weight_cache_enable_falls_back_to_ordinary_tensors(
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) for _ in range(2)])
class Pipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = Transformer()
pipeline = Pipeline()
weight_file = tmp_path / "runtime.safetensors"
weights = {name: torch.full_like(param, 4) for name, param in pipeline.named_parameters()}
save_file(weights, str(weight_file))
plan = HostWeightPlan(
backing_kind="host_weight_cache",
bindings={name: TensorBinding(storage_key=name, file_path=str(weight_file)) for name in weights},
runtime_layout_key="runtime-layout",
expected_file_digests={str(weight_file): "stale-digest"},
)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
dlo_use_allgather=False,
),
torch.device("cpu"),
host_weight_plan=plan,
)
backend.enable(pipeline)
assert backend.enabled
assert backend.host_weight_plan is None
assert not backend._using_mmap
assert not backend._using_rank_local_mmap
backend.disable()
def test_disable_synchronizes_allgather_device_work(
self,
monkeypatch: pytest.MonkeyPatch,
patched_offload_runtime,
):
synchronize_calls: list[None] = []
monkeypatch.setattr(
dist_backend_module.current_omni_platform,
"synchronize",
lambda: synchronize_calls.append(None),
)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
dlo_use_allgather=True,
),
torch.device("cpu"),
)
backend.enabled = True
backend._using_rank_local_mmap = False
backend.disable()
assert synchronize_calls == [None]
def test_host_weight_cache_registration_uses_budget_and_releases_before_mmap(
self,
monkeypatch: pytest.MonkeyPatch,
patched_offload_runtime,
):
events: list[str] = []
class Registration:
total_bytes = 4096
region_count = 1
@staticmethod
def close() -> list[str]:
events.append("unregister")
return []
create_calls: list[tuple[dict[str, list[torch.Tensor]], int]] = []
def register(sources, *, device, max_bytes):
assert device == torch.device("cuda")
create_calls.append((sources, max_bytes))
return Registration()
monkeypatch.setattr(dist_backend_module, "register_host_mappings", register)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=True,
dlo_use_allgather=False,
dlo_host_weight_cache_pin_limit_gib=1.5,
),
torch.device("cuda"),
)
sources = {"runtime.safetensors": [torch.ones(1)]}
backend._host_weight_cache_mapped_sources = sources
assert backend._try_register_host_weight_cache_mmap()
assert create_calls == [(sources, int(1.5 * 1024**3))]
backend.enabled = True
backend._using_rank_local_mmap = True
backend._mmap_file_cache = {}
backend._release_mmap_handles = lambda: events.append("mmap") # type: ignore[method-assign]
backend.disable()
assert events == ["unregister", "mmap"]
def test_host_weight_cache_registration_defaults_to_pinned_memory_policy(
self,
monkeypatch: pytest.MonkeyPatch,
patched_offload_runtime,
):
class Registration:
total_bytes = 4096
region_count = 1
@staticmethod
def close() -> list[str]:
return []
registration = Registration()
create_calls: list[tuple[dict[str, list[torch.Tensor]], int | None]] = []
def register(sources, *, device, max_bytes):
assert device == torch.device("cuda")
create_calls.append((sources, max_bytes))
return registration
monkeypatch.setattr(dist_backend_module, "register_host_mappings", register)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=True,
dlo_use_allgather=False,
dlo_host_weight_cache_pin_limit_gib=0.0,
),
torch.device("cuda"),
)
sources = {"runtime.safetensors": [torch.ones(1)]}
backend._host_weight_cache_mapped_sources = sources
assert backend._try_register_host_weight_cache_mmap()
assert create_calls == [(sources, None)]
assert backend._host_registration is registration
def test_host_weight_cache_registration_respects_disabled_pinned_memory(
self,
monkeypatch: pytest.MonkeyPatch,
patched_offload_runtime,
):
monkeypatch.setattr(
dist_backend_module,
"register_host_mappings",
lambda *_args, **_kwargs: pytest.fail("registration must follow pin_cpu_memory=False"),
)
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=False,
dlo_use_allgather=False,
dlo_host_weight_cache_pin_limit_gib=0.0,
),
torch.device("cuda"),
)
backend._host_weight_cache_mapped_sources = {"runtime.safetensors": [torch.ones(1)]}
assert not backend._try_register_host_weight_cache_mmap()
assert backend._host_registration is None
def test_host_weight_cache_transfer_is_configured_once_for_all_consumers(
self,
monkeypatch: pytest.MonkeyPatch,
patched_offload_runtime,
):
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=True,
dlo_use_allgather=False,
),
torch.device("cuda"),
host_weight_plan=HostWeightPlan(
backing_kind="host_weight_cache",
bindings={},
),
)
backend._using_rank_local_mmap = True
backend._host_weight_cache_mapped_sources = {"runtime.safetensors": [torch.ones(1)]}
streamed_hook = SimpleNamespace(registered_mmap=False)
resident_group = SimpleNamespace(
registered_mmap=False,
_cpu_staging_buffers=[{"buffer": torch.ones(1)}],
)
backend._resident_layer_group = resident_group
register_calls: list[None] = []
def register() -> bool:
register_calls.append(None)
return True
monkeypatch.setattr(backend, "_try_register_host_weight_cache_mmap", register)
backend._configure_host_weight_cache_transfer([streamed_hook]) # type: ignore[list-item]
assert register_calls == [None]
assert streamed_hook.registered_mmap
assert resident_group.registered_mmap
assert resident_group._cpu_staging_buffers == []
assert backend._host_weight_cache_mapped_sources == {}
def test_host_weight_cache_registration_falls_back_on_unsupported_platform(
self,
patched_offload_runtime,
):
backend = DistributedLayerwiseOffloadBackend(
OffloadConfig(
strategy=OffloadStrategy.DISTRIBUTED_LAYER_WISE,
pin_cpu_memory=True,
dlo_use_allgather=False,
dlo_host_weight_cache_pin_limit_gib=1.0,
),
torch.device("cpu"),
)
backend._host_weight_cache_mapped_sources = {"runtime.safetensors": [torch.ones(1)]}
assert not backend._try_register_host_weight_cache_mmap()
assert backend._host_registration is None
class TestGetBlocksFromDit: class TestGetBlocksFromDit:
def test_get_blocks_from_dit_single_block_attr(self): def test_get_blocks_from_dit_single_block_attr(self):
@@ -1628,7 +1164,7 @@ class TestMmapValidation:
assert result.fallback_reason is None assert result.fallback_reason is None
assert result.plan is not None assert result.plan is not None
assert result.plan.bindings["transformer.blocks.0.weight"] == TensorBinding( assert result.plan.bindings["transformer.blocks.0.weight"] == TensorBinding(
storage_key="blocks.0.weight", checkpoint_key="blocks.0.weight",
file_path=str(checkpoint_file), file_path=str(checkpoint_file),
) )
assert result.plan.planned_source_prefixes == frozenset({"transformer."}) assert result.plan.planned_source_prefixes == frozenset({"transformer."})
@@ -1918,7 +1454,7 @@ class TestMmapValidation:
assert result.plan is not None assert result.plan is not None
assert result.plan.bindings["transformer.proj_in.weight"] == TensorBinding( assert result.plan.bindings["transformer.proj_in.weight"] == TensorBinding(
storage_key="proj_in.weight", checkpoint_key="proj_in.weight",
file_path=str(checkpoint_file), file_path=str(checkpoint_file),
) )
@@ -92,6 +92,30 @@ class TestParallelConfigPropagation:
od = _roundtrip_diffusion_config(model="x", parallel_config=pc) od = _roundtrip_diffusion_config(model="x", parallel_config=pc)
assert od.num_gpus == 2 assert od.num_gpus == 2
def test_dp_is_inferred_from_num_gpus(self):
pc = DiffusionParallelConfig(tensor_parallel_size=2, ulysses_degree=2)
od = OmniDiffusionConfig.from_kwargs(model="x", parallel_config=pc, num_gpus=8)
assert od.parallel_config.data_parallel_size == 2
assert od.parallel_config.world_size == 8
def test_explicit_dp_is_validated_against_num_gpus(self):
pc = DiffusionParallelConfig(tensor_parallel_size=2, data_parallel_size=2)
od = OmniDiffusionConfig.from_kwargs(model="x", parallel_config=pc, num_gpus=4)
assert od.parallel_config.data_parallel_size == 2
with pytest.raises(ValueError, match="does not match WORLD-derived value"):
OmniDiffusionConfig.from_kwargs(
model="x",
parallel_config=DiffusionParallelConfig(tensor_parallel_size=2, data_parallel_size=2),
num_gpus=8,
)
def test_hsdp_does_not_infer_ordinary_dp(self):
pc = DiffusionParallelConfig(use_hsdp=True, hsdp_shard_size=4)
od = OmniDiffusionConfig.from_kwargs(model="x", parallel_config=pc, num_gpus=4)
assert od.parallel_config.data_parallel_size == 1
assert od.parallel_config.world_size == 4
class TestCreateDefaultDiffusion: class TestCreateDefaultDiffusion:
"""Verify engine_args structure from create_default_diffusion.""" """Verify engine_args structure from create_default_diffusion."""
@@ -12,13 +12,11 @@ This module tests the WorkerWrapperBase implementation:
- Dynamic worker class extension - Dynamic worker class extension
""" """
from types import SimpleNamespace
from typing import Any from typing import Any
import pytest import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
import vllm_omni.diffusion.worker.diffusion_worker as worker_module
from vllm_omni.diffusion.worker.diffusion_worker import ( from vllm_omni.diffusion.worker.diffusion_worker import (
CustomPipelineWorkerExtension, CustomPipelineWorkerExtension,
DiffusionWorker, DiffusionWorker,
@@ -206,29 +204,6 @@ class TestWorkerWrapperBaseDelegation:
wrapper.worker.shutdown.assert_called_once() wrapper.worker.shutdown.assert_called_once()
assert result is None assert result is None
def test_worker_shutdown_disables_offloader_before_distributed_teardown(
self,
mocker: MockerFixture,
monkeypatch: pytest.MonkeyPatch,
):
worker = object.__new__(DiffusionWorker)
offload_backend = mocker.Mock()
kv_transfer_manager = mocker.Mock()
worker.model_runner = SimpleNamespace(
offload_backend=offload_backend,
kv_transfer_manager=kv_transfer_manager,
)
events: list[str] = []
offload_backend.disable.side_effect = lambda: events.append("offloader")
kv_transfer_manager.shutdown_prefetch.side_effect = lambda: events.append("kv_transfer")
monkeypatch.setattr(worker_module, "destroy_distributed_env", lambda: events.append("distributed"))
worker.shutdown()
offload_backend.disable.assert_called_once_with()
kv_transfer_manager.shutdown_prefetch.assert_called_once_with()
assert events == ["offloader", "kv_transfer", "distributed"]
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Tests: execute_method # Tests: execute_method
@@ -58,6 +58,23 @@ def test_default_stage_devices_from_sequence_parallel():
assert stage_cfg["runtime"]["devices"] == "0,1,2,3" assert stage_cfg["runtime"]["devices"] == "0,1,2,3"
def test_default_stage_devices_and_dp_from_num_gpus():
"""Resolve omitted DP before deriving devices from an explicit GPU count."""
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(
{
"num_gpus": 8,
"tensor_parallel_size": 2,
"ulysses_degree": 2,
}
)[0]
parallel_config = stage_cfg["engine_args"]["parallel_config"]
assert parallel_config.data_parallel_size == 2
assert parallel_config.world_size == 8
assert stage_cfg["engine_args"]["num_gpus"] == 8
assert stage_cfg["runtime"]["devices"] == "0,1,2,3,4,5,6,7"
def test_default_stage_config_uses_parallel_size_kwargs(): def test_default_stage_config_uses_parallel_size_kwargs():
"""Ensure default diffusion parallel_config uses CLI/API parallel sizes.""" """Ensure default diffusion parallel_config uses CLI/API parallel sizes."""
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg( stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(
@@ -76,8 +93,8 @@ def test_default_stage_config_uses_parallel_size_kwargs():
assert parallel_config.enable_expert_parallel is True assert parallel_config.enable_expert_parallel is True
def test_default_stage_config_defaults_nullified_parallel_size_kwargs(): def test_default_stage_config_preserves_omitted_dp_for_runtime_inference():
"""Ensure nullified diffusion parallel-size kwargs fall back to defaults.""" """Keep omitted DP unresolved until the runtime WORLD size is known."""
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg( stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(
{ {
"pipeline_parallel_size": None, "pipeline_parallel_size": None,
@@ -92,7 +109,7 @@ def test_default_stage_config_defaults_nullified_parallel_size_kwargs():
parallel_config = stage_cfg["engine_args"]["parallel_config"] parallel_config = stage_cfg["engine_args"]["parallel_config"]
assert parallel_config.pipeline_parallel_size == 1 assert parallel_config.pipeline_parallel_size == 1
assert parallel_config.data_parallel_size == 1 assert parallel_config.data_parallel_size is None
assert parallel_config.tensor_parallel_size == 1 assert parallel_config.tensor_parallel_size == 1
assert parallel_config.enable_expert_parallel is False assert parallel_config.enable_expert_parallel is False
assert stage_cfg["engine_args"]["enforce_eager"] is False assert stage_cfg["engine_args"]["enforce_eager"] is False
@@ -328,11 +345,10 @@ def test_serve_cli_forwards_distributed_offload_residency():
"--omni", "--omni",
"--enable-distributed-layerwise-offload", "--enable-distributed-layerwise-offload",
"--dlo-no-use-allgather", "--dlo-no-use-allgather",
"--dlo-use-host-weight-cache",
"--dlo-host-weight-cache-pin-limit-gib",
"80",
"--dlo-resident-layers", "--dlo-resident-layers",
"20", "20",
"--dlo-fp8-cache-dir",
"/tmp/dlo-fp8-cache",
] ]
) )
@@ -342,16 +358,12 @@ def test_serve_cli_forwards_distributed_offload_residency():
assert args.enable_distributed_layerwise_offload is True assert args.enable_distributed_layerwise_offload is True
assert args.dlo_use_allgather is False assert args.dlo_use_allgather is False
assert args.dlo_use_host_weight_cache is True
assert args.dlo_host_weight_cache_pin_limit_gib == 80
assert args.dlo_resident_layers == 20 assert args.dlo_resident_layers == 20
assert args.dlo_fp8_cache_dir == "/tmp/dlo-fp8-cache"
assert engine_args["enable_distributed_layerwise_offload"] is True assert engine_args["enable_distributed_layerwise_offload"] is True
assert engine_args["dlo_use_allgather"] is False assert engine_args["dlo_use_allgather"] is False
assert engine_args["dlo_use_host_weight_cache"] is True
assert engine_args["dlo_host_weight_cache_dir"] is None
assert engine_args["dlo_host_weight_cache_lock_timeout"] == 600.0
assert engine_args["dlo_host_weight_cache_pin_limit_gib"] == 80
assert engine_args["dlo_resident_layers"] == 20 assert engine_args["dlo_resident_layers"] == 20
assert engine_args["dlo_fp8_cache_dir"] == "/tmp/dlo-fp8-cache"
def test_serve_cli_accepts_diffusion_compile_controls(): def test_serve_cli_accepts_diffusion_compile_controls():
+12 -25
View File
@@ -11,7 +11,6 @@ later PRs cut consumers over to these classes.
from __future__ import annotations from __future__ import annotations
import copy import copy
import math
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import InitVar, dataclass, field, fields from dataclasses import InitVar, dataclass, field, fields
from functools import wraps from functools import wraps
@@ -601,12 +600,17 @@ class OmniStageDiffusionParallelConfig(OmniStageParallelConfig):
* self.cfg_parallel_size * self.cfg_parallel_size
) )
if self.use_hsdp: if self.use_hsdp:
if self.tensor_parallel_size > 1 or self.data_parallel_size > 1: incompatible = []
raise ValueError( if self.tensor_parallel_size > 1:
"HSDP (use_hsdp=True) cannot be used with TP or DP " incompatible.append("TP")
f"(tensor_parallel_size={self.tensor_parallel_size}, " if self.data_parallel_size > 1:
f"data_parallel_size={self.data_parallel_size})" incompatible.append("DP")
) if self.pipeline_parallel_size > 1:
incompatible.append("PP")
if self.enable_expert_parallel:
incompatible.append("EP")
if incompatible:
raise ValueError("HSDP (FSDP2) is not compatible with " + ", ".join(incompatible))
if self.hsdp_shard_size == -1: if self.hsdp_shard_size == -1:
if other_parallel_world_size == 1: if other_parallel_world_size == 1:
raise ValueError("Cannot auto-calculate hsdp_shard_size when other parallelism is all 1") raise ValueError("Cannot auto-calculate hsdp_shard_size when other parallelism is all 1")
@@ -688,11 +692,8 @@ class _DiffusionConfigProjection:
enable_layerwise_offload: bool = False enable_layerwise_offload: bool = False
enable_distributed_layerwise_offload: bool = False enable_distributed_layerwise_offload: bool = False
dlo_use_allgather: bool = True dlo_use_allgather: bool = True
dlo_use_host_weight_cache: bool = False
dlo_host_weight_cache_dir: str | None = None
dlo_host_weight_cache_lock_timeout: float = Field(default=600.0, gt=0)
dlo_host_weight_cache_pin_limit_gib: float = Field(default=0.0, ge=0)
dlo_resident_layers: int = Field(default=0, ge=0) dlo_resident_layers: int = Field(default=0, ge=0)
dlo_fp8_cache_dir: str | None = None
pin_cpu_memory: bool = True pin_cpu_memory: bool = True
diffusion_compile_granularity: Literal["regional", "full"] = "regional" diffusion_compile_granularity: Literal["regional", "full"] = "regional"
diffusion_compile_dynamic: bool = Field(default=True, strict=True) diffusion_compile_dynamic: bool = Field(default=True, strict=True)
@@ -754,20 +755,6 @@ class _DiffusionConfigProjection:
from vllm_omni.diffusion.diffusion_kv.config import parse_diffusion_kv_cache_mode from vllm_omni.diffusion.diffusion_kv.config import parse_diffusion_kv_cache_mode
from vllm_omni.quantization import build_quant_config from vllm_omni.quantization import build_quant_config
if not math.isfinite(self.dlo_host_weight_cache_lock_timeout):
raise ValueError("dlo_host_weight_cache_lock_timeout must be finite")
if not math.isfinite(self.dlo_host_weight_cache_pin_limit_gib):
raise ValueError("dlo_host_weight_cache_pin_limit_gib must be finite")
if self.dlo_host_weight_cache_pin_limit_gib and (
not self.enable_distributed_layerwise_offload or self.dlo_use_allgather
):
raise ValueError("dlo_host_weight_cache_pin_limit_gib requires no-AllGather distributed layerwise offload")
if self.dlo_use_host_weight_cache and (not self.enable_distributed_layerwise_offload or self.dlo_use_allgather):
raise ValueError("dlo_use_host_weight_cache requires no-AllGather distributed layerwise offload")
if self.dlo_host_weight_cache_pin_limit_gib and not self.dlo_use_host_weight_cache:
raise ValueError("dlo_host_weight_cache_pin_limit_gib requires dlo_use_host_weight_cache=True")
if self.tf_model_config is None: if self.tf_model_config is None:
self.tf_model_config = TransformerConfig() self.tf_model_config = TransformerConfig()
elif isinstance(self.tf_model_config, Mapping): elif isinstance(self.tf_model_config, Mapping):
+1 -4
View File
@@ -382,11 +382,8 @@ class StageDeployConfig:
enable_distributed_layerwise_offload: bool | None = None enable_distributed_layerwise_offload: bool | None = None
dlo_use_allgather: bool | None = None dlo_use_allgather: bool | None = None
dlo_use_host_weight_cache: bool | None = None
dlo_host_weight_cache_dir: str | None = None
dlo_host_weight_cache_lock_timeout: float | None = None
dlo_host_weight_cache_pin_limit_gib: float | None = None
dlo_resident_layers: int | None = None dlo_resident_layers: int | None = None
dlo_fp8_cache_dir: str | None = None
# Diffusion-specific debug and observability knobs. # Diffusion-specific debug and observability knobs.
enable_diffusion_pipeline_profiler: bool | None = None enable_diffusion_pipeline_profiler: bool | None = None
+59 -38
View File
@@ -154,8 +154,8 @@ class DiffusionParallelConfig:
pipeline_parallel_size: int = 1 pipeline_parallel_size: int = 1
"""Number of pipeline parallel stages.""" """Number of pipeline parallel stages."""
data_parallel_size: int = 1 data_parallel_size: int | None = None
"""Number of data parallel groups.""" """Number of data parallel groups. Inferred from WORLD when omitted."""
tensor_parallel_size: int = 1 tensor_parallel_size: int = 1
"""Number of tensor parallel groups.""" """Number of tensor parallel groups."""
@@ -243,7 +243,7 @@ class DiffusionParallelConfig:
def _validate_parallel_config(self) -> Self: def _validate_parallel_config(self) -> Self:
"""Validates the config relationships among the parallel strategies.""" """Validates the config relationships among the parallel strategies."""
assert self.pipeline_parallel_size > 0, "Pipeline parallel size must be > 0" assert self.pipeline_parallel_size > 0, "Pipeline parallel size must be > 0"
assert self.data_parallel_size > 0, "Data parallel size must be > 0" assert self.data_parallel_size is None or self.data_parallel_size > 0, "Data parallel size must be > 0"
assert self.tensor_parallel_size > 0, "Tensor parallel size must be > 0" assert self.tensor_parallel_size > 0, "Tensor parallel size must be > 0"
assert self.sequence_parallel_size > 0, "Sequence parallel size must be > 0" assert self.sequence_parallel_size > 0, "Sequence parallel size must be > 0"
assert self.ulysses_degree > 0, "Ulysses degree must be > 0" assert self.ulysses_degree > 0, "Ulysses degree must be > 0"
@@ -283,10 +283,12 @@ class DiffusionParallelConfig:
self.allgather_degree if self.allgather_degree > 1 else self.ulysses_degree * self.ring_degree self.allgather_degree if self.allgather_degree > 1 else self.ulysses_degree * self.ring_degree
) )
# Calculate world_size from other parallelism dimensions # Until the runtime WORLD size is known, an omitted DP dimension means
# one replica. OmniDiffusionConfig resolves it against num_gpus below.
data_parallel_size = self.data_parallel_size or 1
other_parallel_world_size = ( other_parallel_world_size = (
self.pipeline_parallel_size self.pipeline_parallel_size
* self.data_parallel_size * data_parallel_size
* self.tensor_parallel_size * self.tensor_parallel_size
* self.sequence_parallel_size * self.sequence_parallel_size
* self.cfg_parallel_size * self.cfg_parallel_size
@@ -297,13 +299,17 @@ class DiffusionParallelConfig:
# 1. Standalone: when other parallelism is all 1, HSDP determines world_size # 1. Standalone: when other parallelism is all 1, HSDP determines world_size
# 2. Combined: HSDP overlays on top of other parallelism # 2. Combined: HSDP overlays on top of other parallelism
if self.use_hsdp: if self.use_hsdp:
if self.tensor_parallel_size > 1 or self.data_parallel_size > 1: incompatible = []
raise ValueError( if self.tensor_parallel_size > 1:
"HSDP (use_hsdp=True) cannot be used with TP or DP " incompatible.append("TP")
f"(tensor_parallel_size={self.tensor_parallel_size}, " if data_parallel_size > 1:
f"data_parallel_size={self.data_parallel_size}). " incompatible.append("DP")
"Set tensor_parallel_size=1 and data_parallel_size=1 when using HSDP." if self.pipeline_parallel_size > 1:
) incompatible.append("PP")
if self.enable_expert_parallel:
incompatible.append("EP")
if incompatible:
raise ValueError("HSDP (FSDP2) is not compatible with " + ", ".join(incompatible))
if self.hsdp_shard_size == -1: if self.hsdp_shard_size == -1:
# Auto-calculate: use other_parallel_world_size as shard_size # Auto-calculate: use other_parallel_world_size as shard_size
if self.hsdp_replicate_size <= 0: if self.hsdp_replicate_size <= 0:
@@ -338,6 +344,42 @@ class DiffusionParallelConfig:
else: else:
self.world_size = other_parallel_world_size self.world_size = other_parallel_world_size
def resolve_data_parallel_size(self, world_size: int) -> int:
"""Resolve or validate diffusion DP against the actual WORLD size."""
if world_size <= 0:
raise ValueError(f"WORLD size must be > 0, but got {world_size}")
if self.use_hsdp:
if self.data_parallel_size not in (None, 1):
raise ValueError("HSDP (FSDP2) requires data_parallel_size to be 1")
expected_world_size = self.hsdp_replicate_size * self.hsdp_shard_size
if world_size != expected_world_size:
raise ValueError(
f"WORLD size ({world_size}) must equal HSDP size "
f"({self.hsdp_replicate_size} x {self.hsdp_shard_size} = {expected_world_size})"
)
self.data_parallel_size = 1
self.world_size = world_size
return 1
non_dp_size = (
self.pipeline_parallel_size
* self.tensor_parallel_size
* self.sequence_parallel_size
* self.cfg_parallel_size
)
if world_size % non_dp_size != 0:
raise ValueError(f"WORLD size ({world_size}) must be divisible by non-DP parallel size ({non_dp_size})")
inferred_data_parallel_size = world_size // non_dp_size
if self.data_parallel_size is not None and self.data_parallel_size != inferred_data_parallel_size:
raise ValueError(
f"data_parallel_size ({self.data_parallel_size}) does not match WORLD-derived value "
f"({inferred_data_parallel_size})"
)
self.data_parallel_size = inferred_data_parallel_size
self.world_size = world_size
return inferred_data_parallel_size
@classmethod @classmethod
def from_dict(cls, data: dict[str, Any]) -> "DiffusionParallelConfig": def from_dict(cls, data: dict[str, Any]) -> "DiffusionParallelConfig":
""" """
@@ -708,18 +750,12 @@ class OmniDiffusionConfig:
# This avoids AllGather synchronization, while host memory follows the # This avoids AllGather synchronization, while host memory follows the
# loader's existing rank-local layout instead of adding a second DP shard. # loader's existing rank-local layout instead of adding a second DP shard.
dlo_use_allgather: bool = True dlo_use_allgather: bool = True
# Opt into publishing/joining the transform-complete final-layout host
# weight cache instead of direct-checkpoint mmap.
dlo_use_host_weight_cache: bool = False
# Shared local-disk root. None selects ~/.cache/vllm-omni/dlo-host-weights.
dlo_host_weight_cache_dir: str | None = None
# Maximum wait for the per-layout POSIX writer lock.
dlo_host_weight_cache_lock_timeout: float = 600.0
# Optional maximum file-backed memory a worker may register with CUDA.
# Zero applies no additional ceiling; pin_cpu_memory controls registration.
dlo_host_weight_cache_pin_limit_gib: float = 0.0
# Leading main-DiT blocks kept resident by distributed layerwise offload. # Leading main-DiT blocks kept resident by distributed layerwise offload.
dlo_resident_layers: int = 0 dlo_resident_layers: int = 0
# Optional Phase-I normalized online-FP8 cache. A cache miss builds the
# artifact after the ordinary online loader; a valid cache hit uses the
# serialized FP8 checkpoint-mmap path.
dlo_fp8_cache_dir: str | None = None
pin_cpu_memory: bool = True # Use pinned memory for faster transfers when offloading pin_cpu_memory: bool = True # Use pinned memory for faster transfers when offloading
@@ -946,18 +982,6 @@ class OmniDiffusionConfig:
) )
def __post_init__(self): def __post_init__(self):
if not math.isfinite(self.dlo_host_weight_cache_lock_timeout) or self.dlo_host_weight_cache_lock_timeout <= 0:
raise ValueError("dlo_host_weight_cache_lock_timeout must be positive")
if not math.isfinite(self.dlo_host_weight_cache_pin_limit_gib) or self.dlo_host_weight_cache_pin_limit_gib < 0:
raise ValueError("dlo_host_weight_cache_pin_limit_gib must be non-negative")
if self.dlo_host_weight_cache_pin_limit_gib and (
not self.enable_distributed_layerwise_offload or self.dlo_use_allgather
):
raise ValueError("dlo_host_weight_cache_pin_limit_gib requires no-AllGather distributed layerwise offload")
if self.dlo_use_host_weight_cache and (not self.enable_distributed_layerwise_offload or self.dlo_use_allgather):
raise ValueError("dlo_use_host_weight_cache requires no-AllGather distributed layerwise offload")
if self.dlo_host_weight_cache_pin_limit_gib and not self.dlo_use_host_weight_cache:
raise ValueError("dlo_host_weight_cache_pin_limit_gib requires dlo_use_host_weight_cache=True")
if self.diffusion_compile_granularity not in {"regional", "full"}: if self.diffusion_compile_granularity not in {"regional", "full"}:
raise ValueError( raise ValueError(
"diffusion_compile_granularity must be 'regional' or 'full', " "diffusion_compile_granularity must be 'regional' or 'full', "
@@ -1021,10 +1045,7 @@ class OmniDiffusionConfig:
else: else:
self.num_gpus = 1 self.num_gpus = 1
if self.num_gpus < self.parallel_config.world_size: self.parallel_config.resolve_data_parallel_size(self.num_gpus)
raise ValueError(
f"num_gpus ({self.num_gpus}) < parallel_config.world_size ({self.parallel_config.world_size})"
)
if self.diffusion_compile_granularity == "full": if self.diffusion_compile_granularity == "full":
incompatible_features = [] incompatible_features = []
@@ -6,11 +6,6 @@ from vllm_omni.diffusion.distributed.hsdp import (
HSDPInferenceConfig, HSDPInferenceConfig,
apply_hsdp_to_model, apply_hsdp_to_model,
) )
from vllm_omni.diffusion.distributed.parallel_state import (
get_fs_group,
get_fully_shard_rank,
get_fully_shard_world_size,
)
from vllm_omni.diffusion.distributed.sp_plan import ( from vllm_omni.diffusion.distributed.sp_plan import (
SequenceParallelConfig, SequenceParallelConfig,
SequenceParallelInput, SequenceParallelInput,
@@ -46,8 +41,4 @@ __all__ = [
"get_sharding_validator", "get_sharding_validator",
"HSDPInferenceConfig", "HSDPInferenceConfig",
"apply_hsdp_to_model", "apply_hsdp_to_model",
# FS (Fully Shard) utilities
"get_fs_group",
"get_fully_shard_rank",
"get_fully_shard_world_size",
] ]
@@ -9,8 +9,6 @@ import torch
import torch.distributed as dist import torch.distributed as dist
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm_omni.diffusion.distributed.parallel_state import get_dit_group
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -45,9 +43,10 @@ class DistributedVaeExecutor:
""" """
def __init__(self): def __init__(self):
self.group = get_dit_group() # VAE tiles are scheduled across the complete worker WORLD.
self.world_size = dist.get_world_size(self.group) self.group = None
self.rank = dist.get_rank(self.group) self.world_size = dist.get_world_size()
self.rank = dist.get_rank()
self.parallel_size = 1 self.parallel_size = 1
self.parallel_mode = "tile" self.parallel_mode = "tile"
@@ -189,7 +188,7 @@ class DistributedVaeMixin:
if self.distributed_executor.parallel_size > pp_size: if self.distributed_executor.parallel_size > pp_size:
logger.warning( logger.warning(
f"vae_patch_parallel_size={self.distributed_executor.parallel_size} " f"vae_patch_parallel_size={self.distributed_executor.parallel_size} "
f"is greater than dit_group={world_size};" f"is greater than WORLD={world_size};"
f" using dit_group size={world_size}" f" using WORLD size={world_size}"
) )
return True return True
@@ -991,6 +991,18 @@ class PipelineGroupCoordinator(GroupCoordinator):
def _pipeline_isend_skip(self, tensor: torch.tensor): def _pipeline_isend_skip(self, tensor: torch.tensor):
return torch.distributed.isend(tensor, dst=self.skip_rank, group=self.skip_device_group) return torch.distributed.isend(tensor, dst=self.skip_rank, group=self.skip_device_group)
def destroy(self):
groups = [*self.device_groups, *self.cpu_groups, self.skip_device_group]
seen = {id(self.device_group), id(self.cpu_group)}
for group in groups:
if group is not None and id(group) not in seen:
torch.distributed.destroy_process_group(group)
seen.add(id(group))
self.device_groups = []
self.cpu_groups = []
self.skip_device_group = None
super().destroy()
class SequenceParallelGroupCoordinator(GroupCoordinator): class SequenceParallelGroupCoordinator(GroupCoordinator):
def __init__( def __init__(
@@ -1031,3 +1043,15 @@ class SequenceParallelGroupCoordinator(GroupCoordinator):
self.ring_rank = torch.distributed.get_rank(self.ring_group) self.ring_rank = torch.distributed.get_rank(self.ring_group)
self.allgather_world_size = torch.distributed.get_world_size(self.allgather_group) self.allgather_world_size = torch.distributed.get_world_size(self.allgather_group)
self.allgather_rank = torch.distributed.get_rank(self.allgather_group) self.allgather_rank = torch.distributed.get_rank(self.allgather_group)
def destroy(self):
groups = [self.ulysses_group, self.ring_group, self.allgather_group]
seen = {id(self.device_group), id(self.cpu_group)}
for group in groups:
if group is not None and id(group) not in seen:
torch.distributed.destroy_process_group(group)
seen.add(id(group))
self.ulysses_group = None
self.ring_group = None
self.allgather_group = None
super().destroy()
+15 -30
View File
@@ -15,12 +15,7 @@ from torch.distributed.fsdp import (
) )
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm_omni.diffusion.distributed.parallel_state import ( from vllm_omni.diffusion.distributed.parallel_state import get_world_group
get_fs_group,
get_fully_shard_rank,
get_fully_shard_world_size,
get_world_group,
)
from vllm_omni.platforms import current_omni_platform from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -54,36 +49,36 @@ class HSDPInferenceConfig:
def _create_hsdp_mesh( def _create_hsdp_mesh(
device_type: str, device_type: str,
replicate_size: int, replicate_size: int,
shard_pg: torch.distributed.ProcessGroup, shard_size: int,
) -> DeviceMesh: ) -> DeviceMesh:
"""Create a 2D DeviceMesh for HSDP using an existing ProcessGroup for the shard dimension. """Create the FSDP2 DeviceMesh; it owns the HSDP process groups.
Args: Args:
device_type: The device type (e.g., "cuda", "npu") device_type: The device type (e.g., "cuda", "npu")
replicate_size: Number of replica groups replicate_size: Number of replica groups
shard_pg: The ProcessGroup for the shard dimension (from FS GroupCoordinator) shard_size: Number of ranks in each FSDP shard group
Returns: Returns:
A 2D DeviceMesh with dimensions ("replicate", "shard") A 2D DeviceMesh with dimensions ("replicate", "shard")
""" """
shard_size = torch.distributed.get_world_size(shard_pg)
world_size = replicate_size * shard_size world_size = replicate_size * shard_size
actual_world_size = torch.distributed.get_world_size()
if world_size != actual_world_size:
raise ValueError(
f"HSDP mesh dimensions ({replicate_size} x {shard_size} = {world_size}) "
f"must equal WORLD size ({actual_world_size})"
)
# Build 2D mesh tensor: shape (replicate_size, shard_size) # Build 2D mesh tensor: shape (replicate_size, shard_size)
# Ranks are arranged so that each row is a shard group # Ranks are arranged so that each row is a shard group
mesh_tensor = torch.arange(world_size).reshape(replicate_size, shard_size) mesh_tensor = torch.arange(world_size).reshape(replicate_size, shard_size)
# Create DeviceMesh with the shard ProcessGroup
# For the shard dimension, we reuse the existing FS ProcessGroup
device_mesh = init_device_mesh( device_mesh = init_device_mesh(
device_type, device_type,
mesh_shape=(replicate_size, shard_size), mesh_shape=(replicate_size, shard_size),
mesh_dim_names=("replicate", "shard"), mesh_dim_names=("replicate", "shard"),
) )
# Note: init_device_mesh creates new ProcessGroups internally.
# For consistency, we verify the mesh structure matches our FS group.
# In a future optimization, we could pass the existing ProcessGroups directly.
logger.debug( logger.debug(
"Created HSDP mesh: replicate_size=%d, shard_size=%d, mesh=%s", "Created HSDP mesh: replicate_size=%d, shard_size=%d, mesh=%s",
replicate_size, replicate_size,
@@ -119,34 +114,24 @@ def apply_hsdp_to_model(
if not hsdp_config.enabled: if not hsdp_config.enabled:
raise ValueError("HSDP is not enabled in config") raise ValueError("HSDP is not enabled in config")
# Use GroupCoordinator for distributed info
world_group = get_world_group() world_group = get_world_group()
fs_group = get_fs_group()
world_size = world_group.world_size world_size = world_group.world_size
rank = world_group.rank_in_group rank = world_group.rank_in_group
fs_world_size = get_fully_shard_world_size()
fs_rank = get_fully_shard_rank()
hsdp_replicate_size = hsdp_config.hsdp_replicate_size hsdp_replicate_size = hsdp_config.hsdp_replicate_size
hsdp_shard_size = hsdp_config.hsdp_shard_size hsdp_shard_size = hsdp_config.hsdp_shard_size
# Validate that the FS group matches the HSDP shard size if hsdp_replicate_size * hsdp_shard_size != world_size:
if fs_world_size != hsdp_shard_size:
raise ValueError( raise ValueError(
f"FS group world_size ({fs_world_size}) does not match " f"HSDP mesh dimensions ({hsdp_replicate_size} x {hsdp_shard_size}) must equal WORLD size ({world_size})"
f"HSDP shard_size ({hsdp_shard_size}). "
"Ensure fully_shard_degree is set correctly in initialize_model_parallel."
) )
logger.info( logger.info(
"HSDP Inference: replicate_size=%d, shard_size=%d, world_size=%d, rank=%d, fs_world_size=%d, fs_rank=%d", "HSDP Inference (FSDP2): replicate_size=%d, shard_size=%d, world_size=%d, rank=%d",
hsdp_replicate_size, hsdp_replicate_size,
hsdp_shard_size, hsdp_shard_size,
world_size, world_size,
rank, rank,
fs_world_size,
fs_rank,
) )
# When the model contains FP8 parameters (online quantization), let FSDP # When the model contains FP8 parameters (online quantization), let FSDP
@@ -163,14 +148,14 @@ def apply_hsdp_to_model(
device_type = current_omni_platform.device_type device_type = current_omni_platform.device_type
# Create 2D DeviceMesh for HSDP using the FS group's ProcessGroup for shard dimension # DeviceMesh is the single source of truth for FSDP2 shard/replicate groups
# The mesh shape is (replicate, shard) where: # The mesh shape is (replicate, shard) where:
# - replicate: groups of ranks that hold the same shard (for gradient all-reduce in training) # - replicate: groups of ranks that hold the same shard (for gradient all-reduce in training)
# - shard: groups of ranks that each hold different shards (for parameter all-gather) # - shard: groups of ranks that each hold different shards (for parameter all-gather)
device_mesh = _create_hsdp_mesh( device_mesh = _create_hsdp_mesh(
device_type=device_type, device_type=device_type,
replicate_size=hsdp_replicate_size, replicate_size=hsdp_replicate_size,
shard_pg=fs_group.device_group, shard_size=hsdp_shard_size,
) )
hsdp_shard_conditions = getattr(model, "_hsdp_shard_conditions", None) hsdp_shard_conditions = getattr(model, "_hsdp_shard_conditions", None)
+108 -134
View File
@@ -29,11 +29,11 @@ If you only need to use the distributed environment without model parallelism,
""" """
import inspect import inspect
from math import prod
import torch import torch
import torch.distributed import torch.distributed
import vllm.distributed.parallel_state as vllm_parallel_state import vllm.distributed.parallel_state as vllm_parallel_state
from vllm.distributed.parallel_state import get_tensor_model_parallel_world_size
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm_omni.diffusion import envs from vllm_omni.diffusion import envs
@@ -59,8 +59,6 @@ _SP: SequenceParallelGroupCoordinator | None = None
_PP: PipelineGroupCoordinator | None = None _PP: PipelineGroupCoordinator | None = None
_CFG: GroupCoordinator | None = None _CFG: GroupCoordinator | None = None
_DP: GroupCoordinator | None = None _DP: GroupCoordinator | None = None
_FS: GroupCoordinator | None = None # Fully Sharded (HSDP shard dimension)
_DIT: GroupCoordinator | None = None
# Rank-layout metadata for expert parallelism. This is not a process group; # Rank-layout metadata for expert parallelism. This is not a process group;
# it is reused by platform-specific runtimes that must build companion groups # it is reused by platform-specific runtimes that must build companion groups
@@ -184,7 +182,6 @@ class RankGenerator:
pp: int, pp: int,
cfg: int, cfg: int,
dp: int, dp: int,
fs: int = 1,
order: str = "tp-sp-pp-cfg-dp", order: str = "tp-sp-pp-cfg-dp",
rank_offset: int = 0, rank_offset: int = 0,
) -> None: ) -> None:
@@ -193,9 +190,7 @@ class RankGenerator:
self.pp = pp self.pp = pp
self.cfg = cfg self.cfg = cfg
self.dp = dp self.dp = dp
self.fs = fs
self.rank_offset = rank_offset self.rank_offset = rank_offset
self.world_size = tp * sp * pp * cfg * dp
self.name_to_size = { self.name_to_size = {
"tp": self.tp, "tp": self.tp,
@@ -203,15 +198,10 @@ class RankGenerator:
"pp": self.pp, "pp": self.pp,
"cfg": self.cfg, "cfg": self.cfg,
"dp": self.dp, "dp": self.dp,
"fs": self.fs,
} }
order = order.lower() order = order.lower()
for name in self.name_to_size.keys(): for name in self.name_to_size.keys():
# Skip 'fs' validation - it's handled separately with independent_ranks=True
# and doesn't participate in the main orthogonal rank generation
if name == "fs":
continue
if name not in order and self.name_to_size[name] != 1: if name not in order and self.name_to_size[name] != 1:
raise RuntimeError( raise RuntimeError(
f"The size of ({name}) is ({self.name_to_size[name]}), " f"The size of ({name}) is ({self.name_to_size[name]}), "
@@ -234,7 +224,7 @@ class RankGenerator:
mask[ordered_token.index(t)] = True mask[ordered_token.index(t)] = True
return mask return mask
def get_ranks(self, token, independent_ranks: bool = False): def get_ranks(self, token):
"""Get rank group by input token. """Get rank group by input token.
Arguments: Arguments:
@@ -243,27 +233,11 @@ class RankGenerator:
to obtain multiple parallel types, we can use a hyphen to obtain multiple parallel types, we can use a hyphen
'-' to separate them. For example, if we want to obtain '-' to separate them. For example, if we want to obtain
the TP_DP group, the token should be 'tp-dp'. the TP_DP group, the token should be 'tp-dp'.
independent_ranks (bool):
If True, generate independent rank groups that divide the world
into groups of the specified size. Used for FS (fully shard) groups
which operate independently from the main parallelism hierarchy.
""" """
if independent_ranks and token == "fs":
# FS groups divide world into groups of size fs
# e.g., world_size=8, fs=4 -> [[0,1,2,3], [4,5,6,7]]
ranks = []
num_groups = self.world_size // self.fs
for i in range(num_groups):
group = list(range(i * self.fs + self.rank_offset, (i + 1) * self.fs + self.rank_offset))
ranks.append(group)
return ranks
mask = self.get_mask(self.order, token) mask = self.get_mask(self.order, token)
ranks = generate_masked_orthogonal_rank_groups(self.world_size, self.ordered_size, mask) ranks = generate_masked_orthogonal_rank_groups(prod(self.ordered_size), self.ordered_size, mask)
if self.rank_offset > 0: if self.rank_offset:
for rank_group in ranks: return [[rank + self.rank_offset for rank in rank_group] for rank_group in ranks]
for i in range(len(rank_group)):
rank_group[i] += self.rank_offset
return ranks return ranks
@@ -376,22 +350,6 @@ def get_data_parallel_rank():
return get_dp_group().rank_in_group return get_dp_group().rank_in_group
# FS (Fully Shard / HSDP shard dimension)
def get_fs_group() -> GroupCoordinator:
assert _FS is not None, "fully shard group is not initialized"
return _FS
def get_fully_shard_world_size():
"""Return world size for the fully shard group."""
return get_fs_group().world_size
def get_fully_shard_rank():
"""Return my rank for the fully shard group."""
return get_fs_group().rank_in_group
def is_dp_last_group(): def is_dp_last_group():
"""Return True if in the last data parallel group, False otherwise.""" """Return True if in the last data parallel group, False otherwise."""
return ( return (
@@ -401,17 +359,6 @@ def is_dp_last_group():
) )
def get_dit_world_size():
"""Return world size for the DiT model."""
return (
get_data_parallel_world_size()
* get_classifier_free_guidance_world_size()
* get_sequence_parallel_world_size()
* get_pipeline_parallel_world_size()
* get_tensor_model_parallel_world_size()
)
# * SET # * SET
@@ -498,7 +445,6 @@ def init_model_parallel_group(
"expert", "expert",
"sequence", "sequence",
"classifier_free_guidance", "classifier_free_guidance",
"fully_shard",
], f"parallel_mode {parallel_mode} is not supported" ], f"parallel_mode {parallel_mode} is not supported"
if parallel_mode == "pipeline": if parallel_mode == "pipeline":
return PipelineGroupCoordinator( return PipelineGroupCoordinator(
@@ -546,19 +492,6 @@ def init_vllm_model_parallel_group(
) )
def init_dit_group(
dit_parallel_size: int,
backend: str,
):
global _DIT
_DIT = torch.distributed.new_group(ranks=list(range(dit_parallel_size)), backend=backend)
def get_dit_group():
assert _DIT is not None, "DIT group is not initialized"
return _DIT
# adapted from https://github.com/feifeibear/long-context-attention/blob/main/yunchang/globals.py # adapted from https://github.com/feifeibear/long-context-attention/blob/main/yunchang/globals.py
def set_seq_parallel_pg( def set_seq_parallel_pg(
sp_ulysses_degree: int, sp_ulysses_degree: int,
@@ -737,8 +670,8 @@ def set_seq_parallel_pg(
return ulysses_pg, ring_pg, allgather_pg return ulysses_pg, ring_pg, allgather_pg
def initialize_model_parallel( def _initialize_model_parallel(
data_parallel_size: int = 1, data_parallel_size: int | None = None,
cfg_parallel_size: int = 1, cfg_parallel_size: int = 1,
sequence_parallel_size: int | None = None, sequence_parallel_size: int | None = None,
ulysses_degree: int = 1, ulysses_degree: int = 1,
@@ -746,9 +679,8 @@ def initialize_model_parallel(
allgather_degree: int = 1, allgather_degree: int = 1,
tensor_parallel_size: int = 1, tensor_parallel_size: int = 1,
pipeline_parallel_size: int = 1, pipeline_parallel_size: int = 1,
fully_shard_degree: int = 1,
hsdp_replicate_size: int = 1,
enable_expert_parallel: bool = False, enable_expert_parallel: bool = False,
use_hsdp: bool = False,
backend: str | None = None, backend: str | None = None,
) -> None: ) -> None:
if backend is None: if backend is None:
@@ -768,7 +700,6 @@ def initialize_model_parallel(
(causal=False only). Mutually exclusive with ulysses/ring in v1. (causal=False only). Mutually exclusive with ulysses/ring in v1.
tensor_parallel_size: number of GPUs used for tensor parallelism. tensor_parallel_size: number of GPUs used for tensor parallelism.
pipeline_parallel_size: number of GPUs used for pipeline parallelism. pipeline_parallel_size: number of GPUs used for pipeline parallelism.
fully_shard_degree: number of GPUs used for fully sharded data parallelism (HSDP shard dimension).
backend: distributed backend of pytorch collective comm. backend: distributed backend of pytorch collective comm.
Let's say we have a total of 16 GPUs denoted by g0 ... g15 and we Let's say we have a total of 16 GPUs denoted by g0 ... g15 and we
@@ -798,16 +729,20 @@ def initialize_model_parallel(
ranks 8 to 15 belong to the second box. ranks 8 to 15 belong to the second box.
""" """
# Get world size and rank. Ensure some consistencies. # Get world size and rank. Ensure some consistencies.
assert torch.distributed.is_initialized() if not torch.distributed.is_initialized():
raise RuntimeError("torch.distributed must be initialized before model parallel initialization")
if model_parallel_is_initialized():
raise RuntimeError("model parallel groups are already initialized")
world_size: int = torch.distributed.get_world_size() world_size: int = torch.distributed.get_world_size()
backend = backend or torch.distributed.get_backend(get_world_group().device_group) backend = backend or torch.distributed.get_backend(get_world_group().device_group)
if allgather_degree > 1: if allgather_degree > 1:
assert ulysses_degree == 1 and ring_degree == 1, ( if ulysses_degree != 1 or ring_degree != 1:
"AllGather-KV (allgather_degree>1) is mutually exclusive with Ulysses/Ring in v1. " raise ValueError(
f"Got ulysses_degree={ulysses_degree}, ring_degree={ring_degree}, " "AllGather-KV (allgather_degree>1) is mutually exclusive with Ulysses/Ring in v1. "
f"allgather_degree={allgather_degree}." f"Got ulysses_degree={ulysses_degree}, ring_degree={ring_degree}, "
) f"allgather_degree={allgather_degree}."
)
expected_sequence_parallel_size = allgather_degree if allgather_degree > 1 else ring_degree * ulysses_degree expected_sequence_parallel_size = allgather_degree if allgather_degree > 1 else ring_degree * ulysses_degree
if sequence_parallel_size is None: if sequence_parallel_size is None:
@@ -820,42 +755,38 @@ def initialize_model_parallel(
f"but got {sequence_parallel_size}" f"but got {sequence_parallel_size}"
) )
dit_parallel_size = ( non_dp_size = cfg_parallel_size * sequence_parallel_size * pipeline_parallel_size * tensor_parallel_size
data_parallel_size * cfg_parallel_size * sequence_parallel_size * pipeline_parallel_size * tensor_parallel_size if world_size % non_dp_size != 0:
) raise ValueError(f"WORLD size ({world_size}) must be divisible by non-DP parallel size ({non_dp_size})")
if use_hsdp:
# Check for standalone HSDP: all non-HSDP parallelism dimensions are 1 if data_parallel_size not in (None, 1):
is_standalone_hsdp = dit_parallel_size == 1 and fully_shard_degree > 1 raise ValueError("HSDP (FSDP2) requires data_parallel_size to be 1")
if non_dp_size not in (1, world_size):
# For standalone HSDP: use (fully_shard_degree * hsdp_replicate_size) as dit_parallel_size raise ValueError(f"HSDP non-DP parallel size must be 1 or WORLD size ({world_size}), but got {non_dp_size}")
# This ensures orthogonal rank generation works correctly for all HSDP workers data_parallel_size = 1
if is_standalone_hsdp: else:
dit_parallel_size = fully_shard_degree * hsdp_replicate_size inferred_data_parallel_size = world_size // non_dp_size
if data_parallel_size is not None and data_parallel_size != inferred_data_parallel_size:
if world_size < dit_parallel_size: raise ValueError(
raise RuntimeError( f"data_parallel_size ({data_parallel_size}) does not match WORLD-derived value "
f"world_size ({world_size}) is less than " f"({inferred_data_parallel_size})"
f"tensor_parallel_size ({tensor_parallel_size}) x " )
f"pipeline_parallel_size ({pipeline_parallel_size}) x" data_parallel_size = inferred_data_parallel_size
f"sequence_parallel_size ({sequence_parallel_size}) x"
f"cfg_parallel_size "
f"({cfg_parallel_size}) x"
f"data_parallel_size ({data_parallel_size})"
)
# For standalone HSDP, use (fully_shard_degree * hsdp_replicate_size) as data_parallel_size
# so that RankGenerator.world_size matches the actual number of workers
effective_dp_size = (fully_shard_degree * hsdp_replicate_size) if is_standalone_hsdp else data_parallel_size
rank_generator: RankGenerator = RankGenerator( rank_generator: RankGenerator = RankGenerator(
tensor_parallel_size, tensor_parallel_size,
sequence_parallel_size, sequence_parallel_size,
pipeline_parallel_size, pipeline_parallel_size,
cfg_parallel_size, cfg_parallel_size,
effective_dp_size, data_parallel_size,
fs=fully_shard_degree,
order="tp-sp-pp-cfg-dp", order="tp-sp-pp-cfg-dp",
) )
def get_rank_groups(token: str) -> list[list[int]]:
if use_hsdp and non_dp_size == 1:
return [[rank] for rank in range(world_size)]
return rank_generator.get_ranks(token)
use_moe_parallel_mapping = False use_moe_parallel_mapping = False
if enable_expert_parallel: if enable_expert_parallel:
od_config = get_forward_context().omni_diffusion_config od_config = get_forward_context().omni_diffusion_config
@@ -863,11 +794,11 @@ def initialize_model_parallel(
if not use_moe_parallel_mapping: if not use_moe_parallel_mapping:
raise RuntimeError("Expert parallelism enabled for a non-MoE model") raise RuntimeError("Expert parallelism enabled for a non-MoE model")
sp_group_ranks = rank_generator.get_ranks("sp") sp_group_ranks = get_rank_groups("sp")
global _DP global _DP
assert _DP is None, "data parallel group is already initialized" assert _DP is None, "data parallel group is already initialized"
_DP = init_model_parallel_group( _DP = init_model_parallel_group(
group_ranks=rank_generator.get_ranks("dp"), group_ranks=get_rank_groups("dp"),
local_rank=get_world_group().local_rank, local_rank=get_world_group().local_rank,
backend=backend, backend=backend,
parallel_mode="data", parallel_mode="data",
@@ -877,7 +808,7 @@ def initialize_model_parallel(
global _CFG global _CFG
assert _CFG is None, "classifier_free_guidance group is already initialized" assert _CFG is None, "classifier_free_guidance group is already initialized"
_CFG = init_model_parallel_group( _CFG = init_model_parallel_group(
group_ranks=rank_generator.get_ranks("cfg"), group_ranks=get_rank_groups("cfg"),
local_rank=get_world_group().local_rank, local_rank=get_world_group().local_rank,
backend=backend, backend=backend,
parallel_mode="classifier_free_guidance", parallel_mode="classifier_free_guidance",
@@ -885,7 +816,7 @@ def initialize_model_parallel(
global _PP global _PP
assert _PP is None, "pipeline model parallel group is already initialized" assert _PP is None, "pipeline model parallel group is already initialized"
_PP = init_model_parallel_group( _PP = init_model_parallel_group(
group_ranks=rank_generator.get_ranks("pp"), group_ranks=get_rank_groups("pp"),
local_rank=get_world_group().local_rank, local_rank=get_world_group().local_rank,
backend=backend, backend=backend,
parallel_mode="pipeline", parallel_mode="pipeline",
@@ -899,7 +830,7 @@ def initialize_model_parallel(
sp_ring_degree=ring_degree, sp_ring_degree=ring_degree,
sp_allgather_degree=allgather_degree, sp_allgather_degree=allgather_degree,
rank=get_world_group().rank_in_group, rank=get_world_group().rank_in_group,
world_size=dit_parallel_size, world_size=world_size,
sp_group_ranks=sp_group_ranks, sp_group_ranks=sp_group_ranks,
) )
_SP = init_model_parallel_group( _SP = init_model_parallel_group(
@@ -925,7 +856,7 @@ def initialize_model_parallel(
) )
assert vllm_parallel_state._TP is None, "Tensor parallel group is already initialized" assert vllm_parallel_state._TP is None, "Tensor parallel group is already initialized"
tp_group_ranks = rank_generator.get_ranks("tp") tp_group_ranks = get_rank_groups("tp")
if use_moe_parallel_mapping: if use_moe_parallel_mapping:
vllm_parallel_state._TP = init_vllm_model_parallel_group( vllm_parallel_state._TP = init_vllm_model_parallel_group(
group_ranks=tp_group_ranks, group_ranks=tp_group_ranks,
@@ -944,22 +875,14 @@ def initialize_model_parallel(
# CFG is a diffusion-specific replica dimension. Fold it into vLLM DP # CFG is a diffusion-specific replica dimension. Fold it into vLLM DP
# only when constructing the vLLM EP layout for expert-parallel paths. # only when constructing the vLLM EP layout for expert-parallel paths.
vllm_parallel_state._DP = init_vllm_model_parallel_group( vllm_parallel_state._DP = init_vllm_model_parallel_group(
group_ranks=rank_generator.get_ranks("cfg-dp"), group_ranks=get_rank_groups("cfg-dp"),
local_rank=get_world_group().local_rank, local_rank=get_world_group().local_rank,
backend=backend, backend=backend,
group_name="dp", group_name="dp",
) )
global _FS, _EXPERT_PARALLEL_GROUP_RANKS global _EXPERT_PARALLEL_GROUP_RANKS
assert _FS is None, "fully shard group is already initialized" _EXPERT_PARALLEL_GROUP_RANKS = get_rank_groups("tp-sp-cfg-dp")
_FS = init_model_parallel_group(
group_ranks=rank_generator.get_ranks("fs", independent_ranks=True),
local_rank=get_world_group().local_rank,
backend=backend,
parallel_mode="fully_shard",
)
_EXPERT_PARALLEL_GROUP_RANKS = rank_generator.get_ranks("tp-sp-cfg-dp")
if use_moe_parallel_mapping: if use_moe_parallel_mapping:
vllm_parallel_state._EP = init_vllm_model_parallel_group( vllm_parallel_state._EP = init_vllm_model_parallel_group(
group_ranks=_EXPERT_PARALLEL_GROUP_RANKS, group_ranks=_EXPERT_PARALLEL_GROUP_RANKS,
@@ -969,12 +892,63 @@ def initialize_model_parallel(
use_all2all=True, use_all2all=True,
) )
init_dit_group(dit_parallel_size, backend)
def initialize_model_parallel(
data_parallel_size: int | None = None,
cfg_parallel_size: int = 1,
sequence_parallel_size: int | None = None,
ulysses_degree: int = 1,
ring_degree: int = 1,
allgather_degree: int = 1,
tensor_parallel_size: int = 1,
pipeline_parallel_size: int = 1,
enable_expert_parallel: bool = False,
use_hsdp: bool = False,
backend: str | None = None,
) -> None:
"""Atomically initialize diffusion parallel groups.
Configuration is validated before group creation. Any failure tears down
groups created by this attempt, including vLLM-bound coordinators.
"""
existing_groups = {
"dp": _DP,
"cfg": _CFG,
"sp": _SP,
"pp": _PP,
"vllm_tp": vllm_parallel_state._TP,
"vllm_dp": vllm_parallel_state._DP,
"vllm_pp": vllm_parallel_state._PP,
"vllm_ep": vllm_parallel_state._EP,
"vllm_pcp": vllm_parallel_state._PCP,
}
initialized_names = [name for name, group in existing_groups.items() if group is not None]
if initialized_names:
raise RuntimeError(
"model parallel state must be empty before initialization; found " + ", ".join(initialized_names)
)
try:
_initialize_model_parallel(
data_parallel_size=data_parallel_size,
cfg_parallel_size=cfg_parallel_size,
sequence_parallel_size=sequence_parallel_size,
ulysses_degree=ulysses_degree,
ring_degree=ring_degree,
allgather_degree=allgather_degree,
tensor_parallel_size=tensor_parallel_size,
pipeline_parallel_size=pipeline_parallel_size,
enable_expert_parallel=enable_expert_parallel,
use_hsdp=use_hsdp,
backend=backend,
)
except BaseException:
destroy_model_parallel()
raise
def destroy_model_parallel(): def destroy_model_parallel():
"""Set the groups to none and destroy them.""" """Set the groups to none and destroy them."""
global _DP, _CFG, _SP, _PP, _FS, _EXPERT_PARALLEL_GROUP_RANKS global _DP, _CFG, _SP, _PP, _EXPERT_PARALLEL_GROUP_RANKS
if vllm_parallel_state._DP and vllm_parallel_state._DP is not _DP: if vllm_parallel_state._DP and vllm_parallel_state._DP is not _DP:
vllm_parallel_state._DP.destroy() vllm_parallel_state._DP.destroy()
@@ -1005,14 +979,14 @@ def destroy_model_parallel():
vllm_parallel_state._EP = None vllm_parallel_state._EP = None
_EXPERT_PARALLEL_GROUP_RANKS = None _EXPERT_PARALLEL_GROUP_RANKS = None
if vllm_parallel_state._PP and vllm_parallel_state._PP is not _PP:
vllm_parallel_state._PP.destroy()
vllm_parallel_state._PP = None
if _PP: if _PP:
_PP.destroy() _PP.destroy()
_PP = None _PP = None
if _FS:
_FS.destroy()
_FS = None
def destroy_distributed_environment(): def destroy_distributed_environment():
global _WORLD global _WORLD
@@ -41,6 +41,8 @@ class _MiniMaxH3DirectMmapAdapter:
target: torch.Tensor, target: torch.Tensor,
) -> DirectMmapTensorPolicy: ) -> DirectMmapTensorPolicy:
del target del target
if getattr(self.pipeline, "_dlo_fp8_cache_normalized", False):
return DirectMmapTensorPolicy(allow_custom_loader=True)
transform = None transform = None
suffix = ".qkv_proj.weight" suffix = ".qkv_proj.weight"
if runtime_name.endswith(suffix): if runtime_name.endswith(suffix):
@@ -8,13 +8,12 @@ import re
import time import time
from collections.abc import Generator, Iterable, Sequence from collections.abc import Generator, Iterable, Sequence
from pathlib import Path from pathlib import Path
from typing import cast from typing import Any, cast
import torch import torch
from torch import nn from torch import nn
from vllm.config.load import LoadConfig from vllm.config.load import LoadConfig
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
from vllm.model_executor.layers.quantization.base_config import QuantizeMethodBase from vllm.model_executor.layers.quantization.base_config import QuantizeMethodBase
from vllm.model_executor.model_loader.weight_utils import ( from vllm.model_executor.model_loader.weight_utils import (
download_safetensors_index_file_from_hf, download_safetensors_index_file_from_hf,
@@ -35,6 +34,12 @@ from vllm_omni.diffusion.distributed.hsdp import HSDPInferenceConfig, apply_hsdp
from vllm_omni.diffusion.model_loader.checkpoint_adapters import ( from vllm_omni.diffusion.model_loader.checkpoint_adapters import (
get_checkpoint_adapter, get_checkpoint_adapter,
) )
from vllm_omni.diffusion.model_loader.fp8_cache import (
_transformer_quant_config,
build_normalized_fp8_cache,
is_online_fp8_config,
load_fp8_cache_manifest,
)
from vllm_omni.diffusion.model_loader.host_weight_plan import ( from vllm_omni.diffusion.model_loader.host_weight_plan import (
HostWeightPlan, HostWeightPlan,
build_checkpoint_mmap_plan, build_checkpoint_mmap_plan,
@@ -142,6 +147,10 @@ class DiffusersPipelineLoader:
self.quant_config = od_config.quantization_config self.quant_config = od_config.quantization_config
self.parallel_config = od_config.parallel_config self.parallel_config = od_config.parallel_config
self.host_weight_plan: HostWeightPlan | None = None self.host_weight_plan: HostWeightPlan | None = None
self._fp8_cache_dir: Path | None = None
self._fp8_cache_requested = False
self._fp8_cache_hit = False
self._fp8_cache_manifest: dict[str, Any] | None = None
def take_host_weight_plan(self) -> HostWeightPlan | None: def take_host_weight_plan(self) -> HostWeightPlan | None:
"""Transfer the loader-produced plan to the offload backend.""" """Transfer the loader-produced plan to the offload backend."""
@@ -368,12 +377,61 @@ class DiffusersPipelineLoader:
) )
def _get_weight_sources(self, model: nn.Module) -> tuple["ComponentSource", ...]: def _get_weight_sources(self, model: nn.Module) -> tuple["ComponentSource", ...]:
return tuple( sources = tuple(
cast( cast(
Iterable[DiffusersPipelineLoader.ComponentSource], Iterable[DiffusersPipelineLoader.ComponentSource],
getattr(model, "weights_sources", ()), getattr(model, "weights_sources", ()),
) )
) )
if not self._fp8_cache_hit or self._fp8_cache_dir is None or self._fp8_cache_manifest is None:
return sources
cache_prefix = str(self._fp8_cache_manifest["source_prefix"])
cache_subfolder = str(self._fp8_cache_manifest.get("component_subfolder", "transformer"))
return tuple(
dataclasses.replace(
source,
model_or_path=str(self._fp8_cache_dir),
subfolder=cache_subfolder,
revision=None,
)
if source.prefix == cache_prefix
else source
for source in sources
)
def _prepare_fp8_cache(self) -> None:
"""Select the Phase-I cache path before constructing the pipeline."""
cache_dir = getattr(self.od_config, "dlo_fp8_cache_dir", None)
if not cache_dir or not is_online_fp8_config(self.quant_config):
return
if not getattr(self.od_config, "enable_distributed_layerwise_offload", False):
logger.warning("Ignoring --dlo-fp8-cache-dir because distributed layerwise offload is disabled")
return
if not getattr(self.od_config, "dlo_use_allgather", True):
logger.warning("Ignoring --dlo-fp8-cache-dir because DLO AllGather is disabled")
return
if int(getattr(self.parallel_config, "tensor_parallel_size", 1)) != 1:
logger.warning("Ignoring --dlo-fp8-cache-dir because Phase-I cache requires TP=1")
return
if bool(getattr(self.parallel_config, "use_hsdp", False)):
logger.warning("Ignoring --dlo-fp8-cache-dir because Phase-I cache does not support HSDP")
return
self._fp8_cache_dir = Path(cache_dir)
self._fp8_cache_requested = True
self._fp8_cache_manifest = load_fp8_cache_manifest(self._fp8_cache_dir)
if self._fp8_cache_manifest is None:
logger.info("DLO normalized FP8 cache miss: %s", self._fp8_cache_dir)
return
self._fp8_cache_hit = True
self.quant_config = _transformer_quant_config()
self.od_config.quantization_config = self.quant_config
logger.info(
"DLO normalized FP8 cache hit: %s; selecting serialized FP8 checkpoint path",
self._fp8_cache_dir,
)
def _get_expected_parameter_names(self, model: nn.Module) -> set[str]: def _get_expected_parameter_names(self, model: nn.Module) -> set[str]:
"""Return parameter names that should be covered by strict load checks.""" """Return parameter names that should be covered by strict load checks."""
@@ -404,6 +462,7 @@ class DiffusersPipelineLoader:
self.host_weight_plan = None self.host_weight_plan = None
if load_format is None: if load_format is None:
load_format = "default" load_format = "default"
self._prepare_fp8_cache()
# CPU offload + quantization: for offline-quantized models (e.g., AutoRound MXFP8), # CPU offload + quantization: for offline-quantized models (e.g., AutoRound MXFP8),
# weights are already quantized in the checkpoint — load directly on CPU. # weights are already quantized in the checkpoint — load directly on CPU.
# For online quantization, load on device so quantization can run on accelerator, # For online quantization, load on device so quantization can run on accelerator,
@@ -414,7 +473,9 @@ class DiffusersPipelineLoader:
is_offline = getattr(quant_cfg, "data_type", None) == "mx_fp" or getattr( is_offline = getattr(quant_cfg, "data_type", None) == "mx_fp" or getattr(
quant_cfg, "is_checkpoint_quantized", False quant_cfg, "is_checkpoint_quantized", False
) )
if not is_offline: if self._fp8_cache_hit:
logger.info("Normalized FP8 cache is already serialized; keeping model initialization on CPU")
elif not is_offline:
load_device = device.type load_device = device.type
offload_after_quant = True offload_after_quant = True
logger.info( logger.info(
@@ -432,6 +493,11 @@ class DiffusersPipelineLoader:
) )
else: else:
model = self._init_from_load_format(load_format, target_device, custom_pipeline_name, is_hsdp=False) model = self._init_from_load_format(load_format, target_device, custom_pipeline_name, is_hsdp=False)
if self._fp8_cache_hit:
# The direct-mmap adapter must see this before plan
# construction so normalized QKV tensors bypass the raw
# checkpoint grouped-QKV transform.
setattr(model, "_dlo_fp8_cache_normalized", True)
_dist_offload = getattr(self.od_config, "enable_distributed_layerwise_offload", False) _dist_offload = getattr(self.od_config, "enable_distributed_layerwise_offload", False)
_use_ag = getattr(self.od_config, "dlo_use_allgather", True) _use_ag = getattr(self.od_config, "dlo_use_allgather", True)
@@ -441,13 +507,10 @@ class DiffusersPipelineLoader:
_dp_size = int(getattr(self.parallel_config, "data_parallel_size", 1)) _dp_size = int(getattr(self.parallel_config, "data_parallel_size", 1))
_sp_size = int(getattr(self.parallel_config, "sequence_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 _dlo_group_size = _dp_size if _dp_size > 1 else _sp_size
_select_host_weight_cache = (
_dist_offload and not _use_ag and bool(getattr(self.od_config, "dlo_use_host_weight_cache", False))
)
plan_result = None plan_result = None
weight_sources = self._get_weight_sources(model) weight_sources = self._get_weight_sources(model)
if _dist_offload and not _select_host_weight_cache: if _dist_offload:
modules = ModuleDiscovery.discover(model) modules = ModuleDiscovery.discover(model)
plan_result = build_checkpoint_mmap_plan( plan_result = build_checkpoint_mmap_plan(
model, model,
@@ -459,16 +522,6 @@ class DiffusersPipelineLoader:
online_quantization=_has_online_quant, online_quantization=_has_online_quant,
) )
self.host_weight_plan = plan_result.plan self.host_weight_plan = plan_result.plan
elif _select_host_weight_cache:
# Direct checkpoint bindings can carry deferred transforms
# (for example grouped-QKV reordering), so registering the
# raw checkpoint mapping would not make every recurrent H2D
# source directly copyable. Materialize once, apply every
# post-load mutation, then publish/register the final layout.
logger.info(
"DLO host weight cache selected; using the ordinary loader "
"once to publish a transform-complete final mmap layout"
)
_skip_load = self.host_weight_plan is not None _skip_load = self.host_weight_plan is not None
@@ -510,8 +563,7 @@ class DiffusersPipelineLoader:
) )
if _dist_offload and plan_result is not None: if _dist_offload and plan_result is not None:
logger.info( logger.info(
"DLO direct checkpoint mmap unavailable [%s]; using ordinary loader: %s", "DLO direct checkpoint mmap unavailable; using ordinary loader: %s",
plan_result.fallback_code or "unknown",
plan_result.fallback_reason, plan_result.fallback_reason,
) )
logger.debug("Loading weights on %s ...", load_device) logger.debug("Loading weights on %s ...", load_device)
@@ -535,111 +587,22 @@ class DiffusersPipelineLoader:
model.to("cpu") model.to("cpu")
logger.info("Quantization complete, offloaded model back to CPU") logger.info("Quantization complete, offloaded model back to CPU")
self._apply_skip_softmax_calibration(model) if self._fp8_cache_requested and self._fp8_cache_dir is not None and not self._fp8_cache_hit:
model.eval()
self._maybe_build_host_weight_cache(model, load_format)
return model
def _maybe_build_host_weight_cache(self, model: nn.Module, load_format: str) -> None:
"""Publish final ordinary-loader DiT tensors for no-AllGather DLO."""
if self.host_weight_plan is not None:
return
if not getattr(self.od_config, "enable_distributed_layerwise_offload", False):
return
if getattr(self.od_config, "dlo_use_allgather", True):
return
if not getattr(self.od_config, "dlo_use_host_weight_cache", False):
logger.info(
"DLO host weight cache not selected; retaining ordinary host tensors after checkpoint mmap fallback"
)
return
parallel_config = self.parallel_config
tp_size = int(getattr(parallel_config, "tensor_parallel_size", 1))
tp_rank = 0
if tp_size > 1:
try: try:
from vllm.distributed import get_tensor_model_parallel_rank build_normalized_fp8_cache(
model,
tp_rank = int(get_tensor_model_parallel_rank()) sources=self._get_weight_sources(model),
cache_dir=self._fp8_cache_dir,
)
except Exception as exc: except Exception as exc:
logger.warning( logger.warning(
"DLO host weight cache unavailable [parallel_rank_unavailable]: " "DLO normalized FP8 cache build failed; retaining ordinary-loader path: %s",
"cannot resolve TP rank for TP=%d: %s",
tp_size,
exc, exc,
exc_info=True,
) )
return
modules = ModuleDiscovery.discover(model) self._apply_skip_softmax_calibration(model)
from vllm_omni.diffusion.model_loader.host_weight_cache import ( return model.eval()
DEFAULT_LOCK_TIMEOUT_SECONDS,
DEFAULT_SHARD_SIZE_BYTES,
build_host_weight_cache_plan,
)
loader_inputs = {
"model_class_name": getattr(self.od_config, "model_class_name", None),
"model_config": getattr(self.od_config, "model_config", None),
"diffusers_load_kwargs": getattr(self.od_config, "diffusers_load_kwargs", None),
"custom_pipeline_args": getattr(self.od_config, "custom_pipeline_args", None),
"lora_path": getattr(self.od_config, "lora_path", None),
"lora_scale": getattr(self.od_config, "lora_scale", None),
"lora_backend": getattr(self.od_config, "lora_backend", None),
}
sp_guard = {
"sequence_parallel_size": int(getattr(parallel_config, "sequence_parallel_size", 1)),
"ulysses_degree": int(getattr(parallel_config, "ulysses_degree", 1)),
"ring_degree": int(getattr(parallel_config, "ring_degree", 1)),
"allgather_degree": int(getattr(parallel_config, "allgather_degree", 1)),
"ulysses_mode": getattr(parallel_config, "ulysses_mode", "strict"),
}
model_identity = getattr(self.od_config, "model", None)
runtime_quantization = self.quant_config
if runtime_quantization is None and any(
(quant_method := getattr(module, "quant_method", None)) is not None
and not isinstance(quant_method, UnquantizedLinearMethod)
for module in model.modules()
):
runtime_quantization = "module_quant_method"
result = build_host_weight_cache_plan(
model,
dit_modules=tuple(zip(modules.dit_names, modules.dits)),
loader_type=type(self),
cache_root=getattr(self.od_config, "dlo_host_weight_cache_dir", None),
lock_timeout_seconds=float(
getattr(self.od_config, "dlo_host_weight_cache_lock_timeout", DEFAULT_LOCK_TIMEOUT_SECONDS)
),
max_shard_bytes=DEFAULT_SHARD_SIZE_BYTES,
model_identity=str(model_identity) if model_identity is not None else None,
revision=getattr(self.od_config, "revision", None),
runtime_dtype=getattr(self.od_config, "dtype", None),
load_format=load_format,
loader_inputs=loader_inputs,
tensor_parallel_size=tp_size,
tensor_parallel_rank=tp_rank,
sequence_parallel_guard=sp_guard,
use_hsdp=bool(getattr(parallel_config, "use_hsdp", False)),
enable_expert_parallel=bool(getattr(parallel_config, "enable_expert_parallel", False)),
quantization_config=runtime_quantization,
cfg_parallel_size=int(getattr(parallel_config, "cfg_parallel_size", 1)),
pipeline_parallel_size=int(getattr(parallel_config, "pipeline_parallel_size", 1)),
)
if result.plan is not None:
self.host_weight_plan = result.plan
logger.info(
"DLO host weight cache selected (layout=%s, TP=%d/%d, SP=%d)",
result.plan.runtime_layout_key[:12] if result.plan.runtime_layout_key else "unknown",
tp_rank,
tp_size,
sp_guard["sequence_parallel_size"],
)
else:
logger.warning(
"DLO host weight cache unavailable [%s]; retaining ordinary host tensors: %s",
result.fallback_code or "unknown",
result.fallback_reason or "unspecified reason",
)
@staticmethod @staticmethod
def _request_offload_after_quant(model: nn.Module) -> int: def _request_offload_after_quant(model: nn.Module) -> int:
@@ -0,0 +1,349 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Phase-I normalized FP8 cache support for DLO checkpoint mmap."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import uuid
from pathlib import Path
from typing import Any
import torch
from safetensors import safe_open
from safetensors.torch import save_file
from torch import nn
from vllm.logger import init_logger
from vllm_omni.diffusion.model_loader.host_weight_plan import (
_build_source_map,
_collect_required_dit_tensors,
)
from vllm_omni.quantization.component_config import ComponentQuantizationConfig
logger = init_logger(__name__)
CACHE_SCHEMA_VERSION = 1
CACHE_KIND = "normalized_fp8_runtime"
CACHE_MANIFEST_NAME = "manifest.json"
CACHE_COMPLETE_NAME = "COMPLETE"
CACHE_COMPONENT_SUBFOLDER = "transformer"
def _transformer_quant_config() -> ComponentQuantizationConfig:
"""Build the serialized FP8 config used by a normalized cache."""
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
return ComponentQuantizationConfig(
{
"transformer": Fp8Config(
is_checkpoint_fp8_serialized=True,
activation_scheme="dynamic",
)
}
)
def is_online_fp8_config(quant_config: Any) -> bool:
"""Return whether the transformer component requests online FP8."""
if quant_config is None:
return False
if hasattr(quant_config, "resolve"):
quant_config = quant_config.resolve("transformer")
get_name = getattr(quant_config, "get_name", None)
if not callable(get_name) or str(get_name()).lower() != "fp8":
return False
return not bool(getattr(quant_config, "is_checkpoint_fp8_serialized", False))
def _source_fingerprint(files: set[str]) -> str:
entries: list[dict[str, int | str]] = []
for filename in sorted(files):
stat = os.stat(filename)
entries.append(
{
"path": os.path.abspath(filename),
"size": stat.st_size,
"mtime_ns": stat.st_mtime_ns,
}
)
payload = json.dumps(entries, sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(payload).hexdigest()
def load_fp8_cache_manifest(cache_dir: str | os.PathLike[str] | None) -> dict[str, Any] | None:
"""Return a validated Phase-I manifest, or ``None`` for a cache miss."""
if cache_dir is None:
return None
root = Path(cache_dir)
manifest_path = root / CACHE_MANIFEST_NAME
if not (root / CACHE_COMPLETE_NAME).is_file() or not manifest_path.is_file():
return None
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
if not isinstance(manifest, dict):
return None
if manifest.get("schema_version") != CACHE_SCHEMA_VERSION:
return None
if manifest.get("cache_kind") != CACHE_KIND:
return None
if manifest.get("component") != CACHE_COMPONENT_SUBFOLDER:
return None
quantization = manifest.get("quantization")
if not isinstance(quantization, dict) or quantization != {
"method": "fp8",
"serialized": True,
"activation_scheme": "dynamic",
}:
return None
if manifest.get("component_subfolder") != CACHE_COMPONENT_SUBFOLDER:
return None
if manifest.get("source_prefix") != f"{CACHE_COMPONENT_SUBFOLDER}.":
return None
index_path = root / CACHE_COMPONENT_SUBFOLDER / "model.safetensors.index.json"
if not index_path.is_file():
return None
try:
index = json.loads(index_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
if not isinstance(index, dict):
return None
weight_map = index.get("weight_map")
if not isinstance(weight_map, dict) or not weight_map or not all(isinstance(key, str) for key in weight_map):
return None
if manifest.get("tensor_count") != len(weight_map):
return None
for filename in weight_map.values():
if not isinstance(filename, str):
return None
relative_path = Path(filename)
if (
relative_path.is_absolute()
or relative_path.name != filename
or relative_path.suffix != ".safetensors"
or not (root / CACHE_COMPONENT_SUBFOLDER / relative_path).is_file()
):
return None
source_files = manifest.get("source_files")
source_fingerprint = manifest.get("source_fingerprint")
if not isinstance(source_files, list) or not source_files or not isinstance(source_fingerprint, str):
return None
try:
if _source_fingerprint({str(filename) for filename in source_files}) != source_fingerprint:
return None
except (OSError, TypeError):
return None
return manifest
def _cache_key_for_runtime_name(runtime_name: str, prefix: str) -> str:
if not runtime_name.startswith(prefix):
raise ValueError(f"runtime tensor {runtime_name!r} is outside cache source prefix {prefix!r}")
return runtime_name[len(prefix) :]
def build_normalized_fp8_cache(
model: nn.Module,
*,
sources: tuple[object, ...],
cache_dir: str | os.PathLike[str],
) -> dict[str, Any]:
"""Write a normalized FP8 cache from a completed online-FP8 model.
The builder writes tensors one at a time into individual safetensors shards,
so it does not create a second full-model state dict. The first cache build
still follows the existing online loader; warm launches consume the cache
through the direct DLO mmap path.
"""
cache_root = Path(cache_dir)
existing_manifest = load_fp8_cache_manifest(cache_root)
if existing_manifest is not None:
return existing_manifest
if cache_root.exists() and any(cache_root.iterdir()):
raise FileExistsError(f"FP8 cache directory is not empty and is not valid: {cache_root}")
cache_root.parent.mkdir(parents=True, exist_ok=True)
dit_names = tuple(getattr(model, "_dit_modules", ()))
dit_names = tuple(name for name in dit_names if hasattr(model, name))
if len(dit_names) != 1:
raise ValueError(f"Phase-I FP8 cache requires exactly one DiT module, got {dit_names}")
dit_name = dit_names[0]
if dit_name != CACHE_COMPONENT_SUBFOLDER:
raise ValueError(
f"Phase-I FP8 cache requires a top-level {CACHE_COMPONENT_SUBFOLDER!r} module, got {dit_name!r}"
)
source_prefix = f"{dit_name}."
dit_module = model.get_submodule(dit_name)
planned_sources = tuple(source for source in sources if getattr(source, "prefix", "") == source_prefix)
if len(planned_sources) != 1:
raise ValueError(f"Phase-I FP8 cache requires one source for {source_prefix!r}, got {len(planned_sources)}")
source_map = _build_source_map(
sources=planned_sources,
model_path=None,
remap_fn=getattr(type(model), "_remap_ckpt_key", None),
)
required = _collect_required_dit_tensors(((dit_name, dit_module),))
quantized_modules: dict[str, nn.Module] = {}
try:
from vllm.model_executor.layers.quantization.online.fp8 import Fp8PerTensorOnlineLinearMethod
except ImportError as exc: # pragma: no cover - vLLM 0.27 provides this class
raise RuntimeError("Phase-I FP8 cache requires vLLM's online FP8 implementation") from exc
for module_name, module in model.named_modules():
if not module_name.startswith(source_prefix):
continue
if isinstance(getattr(module, "quant_method", None), Fp8PerTensorOnlineLinearMethod):
if not hasattr(module, "weight_scale"):
raise ValueError(f"online FP8 module {module_name!r} has no generated weight_scale")
quantized_modules[module_name] = module
tmp_root = cache_root.parent / f".{cache_root.name}.tmp-{uuid.uuid4().hex}"
component_root = tmp_root / CACHE_COMPONENT_SUBFOLDER
component_root.mkdir(parents=True, exist_ok=False)
weight_map: dict[str, str] = {}
source_files: set[str] = set()
total_size = 0
part_index = 0
file_cache: dict[str, Any] = {}
def write_tensor(key: str, tensor: torch.Tensor) -> None:
nonlocal part_index, total_size
if key in weight_map:
raise ValueError(f"normalized FP8 cache has duplicate tensor key {key!r}")
tensor = tensor.detach().to("cpu").contiguous()
filename = f"part-{part_index:05d}.safetensors"
part_index += 1
save_file({key: tensor}, str(component_root / filename))
weight_map[key] = filename
total_size += tensor.numel() * tensor.element_size()
try:
for runtime_name, target in required.items():
binding_info = source_map.get(runtime_name)
module_name = runtime_name.removesuffix(".weight")
quant_module = quantized_modules.get(module_name) if runtime_name.endswith(".weight") else None
if runtime_name.endswith(".weight_scale"):
owner_name = runtime_name.removesuffix(".weight_scale")
if owner_name in quantized_modules:
# Generated online scales are published in the dedicated
# pass below; they have no raw checkpoint binding.
continue
if quant_module is not None:
if binding_info is None:
raise ValueError(f"no source binding for quantized weight {runtime_name!r}")
qweight = quant_module.weight.detach()
if qweight.ndim != 2:
raise ValueError(f"Phase-I FP8 cache only supports 2D weights, got {runtime_name!r}")
# Online FP8 stores the final runtime weight as qweight.T. The
# serialized FP8 method expects the pre-process qweight shape.
qweight = qweight.t().contiguous()
# Normalized caches contain the post-checkpoint-adapter QKV
# order. The cache-hit adapter bypasses the raw-checkpoint
# grouped-QKV transform, then the serialized FP8 finalizer
# performs only the standard weight transpose.
write_tensor(binding_info[0], qweight)
source_files.add(binding_info[1])
continue
if binding_info is not None:
checkpoint_key, file_path = binding_info
source_files.add(file_path)
handle = file_cache.get(file_path)
if handle is None:
handle = safe_open(file_path, framework="pt", device="cpu")
file_cache[file_path] = handle
write_tensor(checkpoint_key, handle.get_tensor(checkpoint_key))
else:
# Constructor-derived persistent buffers have no checkpoint
# entry; preserve them as canonical cache tensors.
write_tensor(_cache_key_for_runtime_name(runtime_name, source_prefix), target)
for module_name, module in quantized_modules.items():
scale = module.weight_scale.detach()
logical_widths = getattr(module, "logical_widths", None)
if scale.numel() == 1 and logical_widths is not None and len(logical_widths) > 1:
# Serialized Fp8LinearMethod represents a fused per-tensor
# weight with one scale slot per logical shard. Online FP8
# produces one scalar for the fused tensor, so replicate that
# scalar to the offline runtime shape without changing its
# value.
scale = scale.reshape(1).expand(len(logical_widths)).contiguous()
write_tensor(
_cache_key_for_runtime_name(f"{module_name}.weight_scale", source_prefix),
scale,
)
index = {
"metadata": {"total_size": total_size},
"weight_map": dict(sorted(weight_map.items())),
}
(component_root / "model.safetensors.index.json").write_text(
json.dumps(index, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
manifest = {
"schema_version": CACHE_SCHEMA_VERSION,
"cache_kind": CACHE_KIND,
"component": "transformer",
"component_subfolder": CACHE_COMPONENT_SUBFOLDER,
"source_prefix": source_prefix,
"quantization": {
"method": "fp8",
"serialized": True,
"activation_scheme": "dynamic",
},
"tensor_count": len(weight_map),
"source_fingerprint": _source_fingerprint(source_files),
"source_files": sorted(source_files),
}
(tmp_root / CACHE_MANIFEST_NAME).write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
(tmp_root / CACHE_COMPLETE_NAME).write_text("phase1\n", encoding="utf-8")
for handle in file_cache.values():
close = getattr(handle, "__exit__", None)
if callable(close):
close(None, None, None)
try:
os.replace(tmp_root, cache_root)
except FileExistsError:
# Multiple DLO workers may build the same cache concurrently. If
# another worker published a complete artifact first, treat this
# publisher as a successful cache race rather than a build error.
published = load_fp8_cache_manifest(cache_root)
if published is None:
raise
shutil.rmtree(tmp_root, ignore_errors=True)
logger.info("Normalized FP8 cache was published concurrently at %s", cache_root)
return published
logger.info("Published normalized FP8 cache at %s (%d tensors)", cache_root, len(weight_map))
return manifest
except BaseException:
for handle in file_cache.values():
close = getattr(handle, "__exit__", None)
if callable(close):
close(None, None, None)
shutil.rmtree(tmp_root, ignore_errors=True)
raise
__all__ = [
"CACHE_SCHEMA_VERSION",
"CACHE_KIND",
"CACHE_COMPONENT_SUBFOLDER",
"build_normalized_fp8_cache",
"is_online_fp8_config",
"load_fp8_cache_manifest",
"_transformer_quant_config",
]
@@ -1,574 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Build node-local host weight cache plans for diffusion DLO.
``build_host_weight_cache_plan`` is the module's single entry point. It
validates final ordinary-loader DiT tensors, joins or atomically publishes an
immutable content-addressed entry, validates the mmap result, and returns a
fail-closed host-weight plan. Reusable identity, hashing, locking, and file
primitives live in :mod:`host_weight_cache_utils`.
"""
from __future__ import annotations
import contextlib
import hashlib
import json
import math
import os
import shutil
import time
import uuid
from collections.abc import Mapping, Sequence
from itertools import chain
from pathlib import Path
from typing import Any
import torch
from safetensors import safe_open
from safetensors.torch import save_file
from torch import nn
from vllm.logger import init_logger
from vllm_omni.diffusion.model_loader.host_weight_cache_utils import (
HostWeightCacheError,
IdentityNormalizationError,
RuntimeTensor,
canonical_json,
canonicalize_existing_local_path,
exclusive_lock,
file_digest,
fsync_directory,
fsync_file,
implementation_fingerprint,
normalize_identity,
remove_stale_temps,
runtime_content_digest,
split_shards,
tensor_metadata,
)
from vllm_omni.diffusion.model_loader.host_weight_plan import (
HostWeightPlan,
HostWeightPlanResult,
TensorBinding,
)
logger = init_logger(__name__)
# Internal cache-format and I/O defaults, not environment variables. Public
# configuration may override the root and lock timeout; chunk sizes stay private.
# Bump the schema to isolate incompatible manifest layouts.
HOST_WEIGHT_CACHE_SCHEMA_VERSION = 1
# Keep the default shared across local stage replicas.
DEFAULT_HOST_WEIGHT_CACHE_ROOT = "~/.cache/vllm-omni/dlo-host-weights"
# Bound waits for another publisher instead of hanging initialization forever.
DEFAULT_LOCK_TIMEOUT_SECONDS = 600.0
# Avoid one model-sized output file and bound hashing/read working sets.
DEFAULT_SHARD_SIZE_BYTES = 5 * 1024**3
_SUPPORTED_DTYPES = {
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.bfloat16,
torch.float32,
torch.float64,
}
for _dtype_name in ("float8_e4m3fn", "float8_e5m2"):
if (_dtype := getattr(torch, _dtype_name, None)) is not None:
_SUPPORTED_DTYPES.add(_dtype)
def default_host_weight_cache_root() -> str:
"""Return the default root, intentionally independent of VLLM_CACHE_ROOT."""
return os.path.expanduser(DEFAULT_HOST_WEIGHT_CACHE_ROOT)
def _persistent_buffer(module: nn.Module, local_name: str) -> bool:
parent_path, _, leaf_name = local_name.rpartition(".")
owner = module.get_submodule(parent_path)
return leaf_name not in owner._non_persistent_buffers_set
def _named_parameters_with_duplicates(module: nn.Module):
try:
return module.named_parameters(remove_duplicate=False)
except TypeError: # pragma: no cover - compatibility with old torch
return module.named_parameters()
def _named_buffers_with_duplicates(module: nn.Module):
try:
return module.named_buffers(remove_duplicate=False)
except TypeError: # pragma: no cover - compatibility with old torch
return module.named_buffers()
def _resolve_pipeline_tensor(pipeline: nn.Module, runtime_name: str) -> torch.Tensor | None:
parent_path, _, leaf_name = runtime_name.rpartition(".")
parent = pipeline.get_submodule(parent_path)
tensor = parent._parameters.get(leaf_name)
if tensor is None:
tensor = parent._buffers.get(leaf_name)
return tensor
def _collect_runtime_tensors(
pipeline: nn.Module,
dit_modules: Sequence[tuple[str, nn.Module]],
) -> list[RuntimeTensor]:
"""Collect a complete, safely remappable view of final CPU DiT tensors.
Parameters and persistent buffers must resolve to the same objects through
the owning pipeline, occupy complete contiguous CPU storages, use supported
dtypes, and have no aliases inside or outside the discovered DiT modules.
The stable name ordering is reused by content hashing and publication.
"""
records: dict[str, RuntimeTensor] = {}
for dit_name, dit_module in dit_modules:
for local_name, tensor in _named_parameters_with_duplicates(dit_module):
runtime_name = f"{dit_name}.{local_name}"
candidate = RuntimeTensor(runtime_name, tensor, "parameter")
existing = records.get(runtime_name)
if existing is not None and existing.tensor is not tensor:
raise HostWeightCacheError(
"ambiguous_ownership",
f"multiple DiT tensors resolve to {runtime_name!r}",
)
records[runtime_name] = candidate
for local_name, tensor in _named_buffers_with_duplicates(dit_module):
if not _persistent_buffer(dit_module, local_name):
continue
runtime_name = f"{dit_name}.{local_name}"
candidate = RuntimeTensor(runtime_name, tensor, "buffer")
existing = records.get(runtime_name)
if existing is not None and existing.tensor is not tensor:
raise HostWeightCacheError(
"ambiguous_ownership",
f"multiple DiT tensors resolve to {runtime_name!r}",
)
records[runtime_name] = candidate
if not records:
raise HostWeightCacheError(
"ambiguous_ownership",
"no DiT parameters or persistent buffers were discovered",
)
storage_owners: dict[tuple[int, int], str] = {}
for record in records.values():
tensor = record.tensor
try:
pipeline_tensor = _resolve_pipeline_tensor(pipeline, record.name)
except AttributeError as exc:
raise HostWeightCacheError(
"ambiguous_ownership",
f"DiT tensor {record.name!r} is not owned by the pipeline",
) from exc
if pipeline_tensor is not tensor:
raise HostWeightCacheError(
"ambiguous_ownership",
f"DiT tensor {record.name!r} does not resolve to the discovered object",
)
if tensor.device.type != "cpu" or tensor.is_meta:
raise HostWeightCacheError(
"unsupported_tensor",
f"{record.name!r} must be a materialized CPU tensor, got {tensor.device}",
)
if tensor.layout != torch.strided:
raise HostWeightCacheError(
"unsupported_tensor",
f"{record.name!r} uses unsupported layout {tensor.layout}",
)
if not tensor.is_contiguous():
raise HostWeightCacheError(
"unsupported_tensor",
f"{record.name!r} is non-contiguous with stride {tensor.stride()}",
)
if tensor.dtype not in _SUPPORTED_DTYPES:
raise HostWeightCacheError(
"unsupported_tensor",
f"{record.name!r} uses unsupported dtype {tensor.dtype}",
)
if hasattr(tensor, "to_local"):
raise HostWeightCacheError(
"unsupported_tensor",
f"{record.name!r} is a distributed tensor",
)
tensor_nbytes = tensor.numel() * tensor.element_size()
storage = tensor.untyped_storage()
if tensor.storage_offset() != 0 or storage.nbytes() != tensor_nbytes:
raise HostWeightCacheError(
"unsupported_tensor",
f"{record.name!r} is a view into a larger storage",
)
if tensor_nbytes:
storage_id = (storage.data_ptr(), storage.nbytes())
owner = storage_owners.setdefault(storage_id, record.name)
if owner != record.name:
raise HostWeightCacheError(
"unsupported_alias",
f"{record.name!r} shares storage with {owner!r}",
)
# The ownership boundary is the complete pipeline, not only the discovered
# DiT traversal. Reject a DiT storage that is also registered by an encoder,
# VAE, resident component, or non-persistent buffer.
for pipeline_name, tensor in chain(
_named_parameters_with_duplicates(pipeline),
_named_buffers_with_duplicates(pipeline),
):
if tensor.device.type != "cpu" or tensor.is_meta or tensor.numel() == 0:
continue
storage = tensor.untyped_storage()
owner = storage_owners.get((storage.data_ptr(), storage.nbytes()))
if owner is not None and owner != pipeline_name:
raise HostWeightCacheError(
"unsupported_alias",
f"cached tensor {owner!r} shares storage with pipeline tensor {pipeline_name!r}",
)
return [records[name] for name in sorted(records)]
def _publish_entry(
*,
cache_root: Path,
entry_dir: Path,
cache_key: str,
layout_identity: dict[str, Any],
content_digest: str,
records: Sequence[RuntimeTensor],
max_shard_bytes: int,
) -> None:
total_nbytes = sum(record.tensor.numel() * record.tensor.element_size() for record in records)
free_bytes = shutil.disk_usage(cache_root).free
required_bytes = total_nbytes + max(64 * 1024**2, total_nbytes // 100)
if free_bytes < required_bytes:
raise HostWeightCacheError(
"insufficient_disk",
f"host weight cache needs {required_bytes} bytes but only {free_bytes} are free under {cache_root}",
)
tmp_root = cache_root / ".tmp"
tmp_root.mkdir(parents=True, exist_ok=True)
temp_dir = tmp_root / f"{cache_key}.{os.getpid()}.{uuid.uuid4().hex}"
temp_dir.mkdir()
try:
shards = split_shards(records, max_shard_bytes)
tensor_manifest: dict[str, dict[str, Any]] = {}
file_manifest: dict[str, dict[str, Any]] = {}
shard_count = len(shards)
for index, shard in enumerate(shards, start=1):
filename = f"model-{index:05d}-of-{shard_count:05d}.safetensors"
path = temp_dir / filename
save_file(
{record.name: record.tensor.detach() for record in shard},
str(path),
metadata={"format": "pt", "dlo_host_weight_cache_schema": str(HOST_WEIGHT_CACHE_SCHEMA_VERSION)},
)
fsync_file(path)
file_manifest[filename] = {
"size": path.stat().st_size,
"sha256": file_digest(path),
}
for record in shard:
tensor_manifest[record.name] = {
**tensor_metadata(record),
"file": filename,
"storage_key": record.name,
}
manifest = {
"schema_version": HOST_WEIGHT_CACHE_SCHEMA_VERSION,
"cache_key": cache_key,
"layout_identity": layout_identity,
"runtime_content_sha256": content_digest,
"total_tensor_bytes": total_nbytes,
"created_at_unix_seconds": time.time(),
"writer_pid": os.getpid(),
"files": file_manifest,
"tensors": tensor_manifest,
}
manifest_path = temp_dir / "manifest.json"
with manifest_path.open("w", encoding="utf-8") as handle:
json.dump(manifest, handle, sort_keys=True, separators=(",", ":"))
handle.flush()
os.fsync(handle.fileno())
fsync_directory(temp_dir)
invalid_dir: Path | None = None
if entry_dir.exists():
invalid_dir = tmp_root / f"{cache_key}.invalid.{os.getpid()}.{uuid.uuid4().hex}"
os.replace(entry_dir, invalid_dir)
os.replace(temp_dir, entry_dir)
fsync_directory(entry_dir.parent)
if invalid_dir is not None:
shutil.rmtree(invalid_dir, ignore_errors=True)
except Exception:
shutil.rmtree(temp_dir, ignore_errors=True)
raise
def _validate_entry(
*,
entry_dir: Path,
cache_key: str,
layout_identity: dict[str, Any],
content_digest: str,
expected_records: Sequence[RuntimeTensor],
) -> HostWeightPlan:
manifest_path = entry_dir / "manifest.json"
try:
with manifest_path.open(encoding="utf-8") as handle:
manifest = json.load(handle)
except (OSError, ValueError) as exc:
raise HostWeightCacheError("entry_rejected", f"cannot read {manifest_path}: {exc}") from exc
if manifest.get("schema_version") != HOST_WEIGHT_CACHE_SCHEMA_VERSION:
raise HostWeightCacheError("entry_rejected", "host weight cache schema mismatch")
if manifest.get("cache_key") != cache_key:
raise HostWeightCacheError("entry_rejected", "host weight cache key mismatch")
if manifest.get("layout_identity") != layout_identity:
raise HostWeightCacheError("entry_rejected", "host weight cache layout identity mismatch")
if manifest.get("runtime_content_sha256") != content_digest:
raise HostWeightCacheError("entry_rejected", "host weight cache content identity mismatch")
expected_metadata = {record.name: tensor_metadata(record) for record in expected_records}
tensor_manifest = manifest.get("tensors")
file_manifest = manifest.get("files")
if not isinstance(tensor_manifest, dict) or not isinstance(file_manifest, dict):
raise HostWeightCacheError("entry_rejected", "host weight cache manifest has invalid sections")
if set(tensor_manifest) != set(expected_metadata):
raise HostWeightCacheError("entry_rejected", "host weight cache tensor names differ from the loaded DiT")
for filename, metadata in file_manifest.items():
if Path(filename).name != filename or not isinstance(metadata, dict):
raise HostWeightCacheError("entry_rejected", f"invalid cache filename {filename!r}")
path = entry_dir / filename
try:
stat = path.stat()
except OSError as exc:
raise HostWeightCacheError("entry_rejected", f"missing cache shard {path}: {exc}") from exc
if stat.st_size != metadata.get("size") or file_digest(path) != metadata.get("sha256"):
raise HostWeightCacheError("entry_rejected", f"content digest mismatch for cache shard {path}")
bindings: dict[str, TensorBinding] = {}
mapped_records: list[RuntimeTensor] = []
with contextlib.ExitStack() as stack:
handles = {
filename: stack.enter_context(safe_open(entry_dir / filename, framework="pt", device="cpu"))
for filename in file_manifest
}
expected_keys_by_file: dict[str, set[str]] = {filename: set() for filename in file_manifest}
for record in expected_records:
stored = tensor_manifest.get(record.name)
if not isinstance(stored, dict):
raise HostWeightCacheError("entry_rejected", f"invalid tensor metadata for {record.name!r}")
filename = stored.get("file")
storage_key = stored.get("storage_key")
if filename not in handles or not isinstance(storage_key, str):
raise HostWeightCacheError("entry_rejected", f"invalid storage binding for {record.name!r}")
actual_metadata = {key: stored.get(key) for key in expected_metadata[record.name]}
if actual_metadata != expected_metadata[record.name]:
raise HostWeightCacheError("entry_rejected", f"metadata mismatch for {record.name!r}")
expected_keys_by_file[filename].add(storage_key)
try:
tensor = handles[filename].get_tensor(storage_key)
except Exception as exc:
raise HostWeightCacheError(
"entry_rejected",
f"cannot map {record.name!r} from {filename}: {exc}",
) from exc
if tuple(tensor.shape) != tuple(record.tensor.shape) or tensor.dtype != record.tensor.dtype:
raise HostWeightCacheError("entry_rejected", f"mapped metadata mismatch for {record.name!r}")
mapped_records.append(RuntimeTensor(record.name, tensor, record.kind))
bindings[record.name] = TensorBinding(storage_key=storage_key, file_path=str(entry_dir / filename))
for filename, expected_keys in expected_keys_by_file.items():
if set(handles[filename].keys()) != expected_keys:
raise HostWeightCacheError("entry_rejected", f"unexpected tensor keys in cache shard {filename}")
if runtime_content_digest(mapped_records) != content_digest:
raise HostWeightCacheError("entry_rejected", "mapped runtime tensor content does not match the publisher")
return HostWeightPlan(
backing_kind="host_weight_cache",
bindings=bindings,
runtime_layout_key=cache_key,
expected_file_digests={
str(entry_dir / filename): metadata["sha256"] for filename, metadata in file_manifest.items()
},
)
def build_host_weight_cache_plan(
pipeline: nn.Module,
*,
dit_modules: Sequence[tuple[str, nn.Module]],
loader_type: type,
cache_root: str | os.PathLike[str] | None,
lock_timeout_seconds: float,
max_shard_bytes: int,
model_identity: str | None,
revision: str | None,
runtime_dtype: Any,
load_format: str,
loader_inputs: Mapping[str, Any],
tensor_parallel_size: int,
tensor_parallel_rank: int,
sequence_parallel_guard: Mapping[str, Any],
use_hsdp: bool,
enable_expert_parallel: bool,
quantization_config: Any,
cfg_parallel_size: int,
pipeline_parallel_size: int,
) -> HostWeightPlanResult:
"""Build a host-weight plan from the host weight cache after ordinary loading.
This is the module's main entry point. It first rejects unsupported runtime
layouts, then discovers and validates the final CPU DiT tensors and derives
a content-addressed identity from their metadata, bytes, loader inputs, and
parallelism guards. A valid existing entry is joined immediately; otherwise
one process publishes the entry under a per-identity file lock and peers
validate it after the lock is released.
Success returns bindings to immutable safetensors mmap files. Any expected
incompatibility, lock, publication, or validation failure returns a
fail-closed result with no plan and a stable fallback code, leaving the
already-loaded pipeline tensors intact.
"""
if load_format == "dummy":
return HostWeightPlanResult(
None,
"dummy/random weights do not have a reusable runtime identity",
"unsupported_load_format",
)
if use_hsdp:
return HostWeightPlanResult(
None, "HSDP/DTensor layouts are not host weight cache compatible", "unsupported_hsdp"
)
if enable_expert_parallel:
return HostWeightPlanResult(
None,
"expert-parallel weight ownership is outside host weight cache v1",
"unsupported_expert_parallel",
)
if quantization_config is not None:
return HostWeightPlanResult(
None,
"quantized runtime layouts are not supported by the first host weight cache version",
"unsupported_quantization",
)
if cfg_parallel_size != 1:
return HostWeightPlanResult(None, "CFG-parallel component ownership is not proven", "unsupported_cfg_parallel")
if pipeline_parallel_size != 1:
return HostWeightPlanResult(
None, "pipeline-parallel ownership is outside host weight cache v1", "unsupported_pp"
)
if not math.isfinite(lock_timeout_seconds) or lock_timeout_seconds <= 0:
return HostWeightPlanResult(None, "host weight cache lock timeout must be positive", "invalid_configuration")
if max_shard_bytes <= 0:
return HostWeightPlanResult(None, "host weight cache shard size must be positive", "invalid_configuration")
try:
records = _collect_runtime_tensors(pipeline, dit_modules)
content_digest = runtime_content_digest(records)
# The cache stores loader-produced ownership: TP coordinates differ,
# while equivalent DP replicas and SP ranks may share one entry. The
# conservative SP guard prevents reuse across unproven implementations.
layout_identity = {
"schema_version": HOST_WEIGHT_CACHE_SCHEMA_VERSION,
"model": (canonicalize_existing_local_path(model_identity) if model_identity is not None else None),
"revision": revision,
"runtime_dtype": str(runtime_dtype),
"load_format": load_format,
"loader_inputs": normalize_identity(loader_inputs),
"loader_implementation_sha256": implementation_fingerprint(loader_type, pipeline, dit_modules),
"components": [name for name, _ in dit_modules],
"tensor_parallel_size": tensor_parallel_size,
"tensor_parallel_rank": tensor_parallel_rank,
"sequence_parallel_guard": normalize_identity(sequence_parallel_guard),
"runtime_content_sha256": content_digest,
}
cache_key = hashlib.sha256(canonical_json(layout_identity)).hexdigest()
root = Path(cache_root or default_host_weight_cache_root()).expanduser().resolve()
entries_root = root / f"v{HOST_WEIGHT_CACHE_SCHEMA_VERSION}"
entry_dir = entries_root / cache_key
entries_root.mkdir(parents=True, exist_ok=True)
if entry_dir.is_dir():
try:
plan = _validate_entry(
entry_dir=entry_dir,
cache_key=cache_key,
layout_identity=layout_identity,
content_digest=content_digest,
expected_records=records,
)
logger.info("Reusing DLO host weight cache entry %s from %s", cache_key[:12], entry_dir)
return HostWeightPlanResult(plan)
except HostWeightCacheError as exc:
logger.warning("DLO host weight cache entry %s will be rebuilt: %s", cache_key[:12], exc)
lock_path = root / ".locks" / f"{cache_key}.lock"
with exclusive_lock(lock_path, lock_timeout_seconds):
remove_stale_temps(root, cache_key)
if entry_dir.is_dir():
try:
plan = _validate_entry(
entry_dir=entry_dir,
cache_key=cache_key,
layout_identity=layout_identity,
content_digest=content_digest,
expected_records=records,
)
logger.info("Joined DLO host weight cache entry %s from %s", cache_key[:12], entry_dir)
return HostWeightPlanResult(plan)
except HostWeightCacheError:
pass
_publish_entry(
cache_root=root,
entry_dir=entry_dir,
cache_key=cache_key,
layout_identity=layout_identity,
content_digest=content_digest,
records=records,
max_shard_bytes=max_shard_bytes,
)
plan = _validate_entry(
entry_dir=entry_dir,
cache_key=cache_key,
layout_identity=layout_identity,
content_digest=content_digest,
expected_records=records,
)
logger.info(
"Published DLO host weight cache entry %s with %d tensors at %s",
cache_key[:12],
len(records),
entry_dir,
)
return HostWeightPlanResult(plan)
except HostWeightCacheError as exc:
return HostWeightPlanResult(None, str(exc), exc.code)
except IdentityNormalizationError as exc:
return HostWeightPlanResult(None, str(exc), "unstable_identity")
except Exception as exc:
return HostWeightPlanResult(None, f"host weight cache operation failed: {exc}", "cache_operation_failed")
__all__ = [
"DEFAULT_LOCK_TIMEOUT_SECONDS",
"DEFAULT_HOST_WEIGHT_CACHE_ROOT",
"DEFAULT_SHARD_SIZE_BYTES",
"HOST_WEIGHT_CACHE_SCHEMA_VERSION",
"build_host_weight_cache_plan",
"default_host_weight_cache_root",
]
@@ -1,287 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Reusable identity, hashing, and file helpers for the DLO host weight cache."""
from __future__ import annotations
import contextlib
import dataclasses
import errno
import fcntl
import hashlib
import inspect
import json
import os
import shutil
import time
from collections.abc import Iterator, Mapping, Sequence
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Protocol, TypeAlias, TypeVar
import torch
from torch import nn
JsonScalar: TypeAlias = None | bool | int | float | str
JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
PathIdentity: TypeAlias = str | os.PathLike[str] | os.PathLike[bytes]
_HASH_CHUNK_BYTES = 64 * 1024**2
_FILE_HASH_CHUNK_BYTES = 8 * 1024**2
class IdentityNormalizationError(ValueError):
"""Raised when a cache identity contains a process-unstable value."""
@dataclass(frozen=True)
class RuntimeTensor:
"""One final runtime tensor and its manifest identity."""
name: str
tensor: torch.Tensor
kind: str
class HostWeightCacheError(RuntimeError):
"""Expected cache failure that should fall back with a stable code."""
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
class _HashUpdater(Protocol):
def update(self, data: bytes | bytearray | memoryview) -> object: ...
_RuntimeTensorT = TypeVar("_RuntimeTensorT", bound=RuntimeTensor)
def _type_identity(value: object) -> str:
value_type = value if isinstance(value, type) else type(value)
return f"{value_type.__module__}.{value_type.__qualname__}"
def canonicalize_existing_local_path(value: PathIdentity) -> str:
"""Resolve equivalent local paths while leaving model repository IDs alone."""
original = os.fsdecode(os.fspath(value))
candidate = Path(original).expanduser()
try:
if candidate.exists():
return str(candidate.resolve())
except OSError:
pass
return original
def normalize_identity(value: object) -> JsonValue:
"""Convert supported loader inputs into deterministic JSON-compatible data."""
if value is None or isinstance(value, (bool, int, float, str)):
return value
if isinstance(value, Enum):
return {"enum": _type_identity(value), "name": value.name}
if isinstance(value, torch.dtype):
return str(value)
if isinstance(value, os.PathLike):
return canonicalize_existing_local_path(value)
if isinstance(value, type):
return _type_identity(value)
if dataclasses.is_dataclass(value):
return normalize_identity(dataclasses.asdict(value))
model_dump = getattr(value, "model_dump", None)
if callable(model_dump):
try:
dumped = model_dump(mode="json")
except (TypeError, ValueError):
dumped = model_dump()
return normalize_identity(dumped)
if isinstance(value, Mapping):
normalized: dict[str, JsonValue] = {}
for key, item in value.items():
if not isinstance(key, str):
raise IdentityNormalizationError("host weight cache identity mappings require string keys")
normalized[key] = normalize_identity(item)
return {key: normalized[key] for key in sorted(normalized)}
if isinstance(value, (set, frozenset)):
return sorted((normalize_identity(item) for item in value), key=canonical_json)
if isinstance(value, (list, tuple)):
return [normalize_identity(item) for item in value]
raise IdentityNormalizationError(
f"host weight cache identity does not support values of type {_type_identity(value)}"
)
def canonical_json(value: object) -> bytes:
"""Serialize a supported identity with stable ordering and separators."""
return json.dumps(
normalize_identity(value),
sort_keys=True,
separators=(",", ":"),
ensure_ascii=True,
).encode()
def implementation_fingerprint(
loader_type: type,
pipeline: nn.Module,
dit_modules: Sequence[tuple[str, nn.Module]],
) -> str:
"""Hash relevant loader and module implementations for cache identity."""
objects: list[object] = [loader_type, type(pipeline)]
load_weights = getattr(type(pipeline), "load_weights", None)
if load_weights is not None:
objects.append(load_weights)
for _, dit_module in dit_modules:
objects.append(type(dit_module))
post_load = getattr(type(dit_module), "post_load_weights", None)
if post_load is not None:
objects.append(post_load)
digest = hashlib.sha256()
identities: set[str] = set()
for obj in objects:
identity = f"{getattr(obj, '__module__', '')}.{getattr(obj, '__qualname__', type(obj).__qualname__)}"
if identity in identities:
continue
identities.add(identity)
digest.update(identity.encode())
try:
digest.update(inspect.getsource(obj).encode())
except (OSError, TypeError):
pass
return digest.hexdigest()
def tensor_metadata(record: RuntimeTensor) -> dict[str, JsonValue]:
"""Return stable manifest metadata for one runtime tensor."""
tensor = record.tensor
return {
"kind": record.kind,
"shape": list(tensor.shape),
"dtype": str(tensor.dtype),
"nbytes": tensor.numel() * tensor.element_size(),
"layout": "contiguous",
}
def _update_hash_with_tensor(digest: _HashUpdater, tensor: torch.Tensor) -> None:
byte_view = tensor.detach().reshape(-1).view(torch.uint8)
for offset in range(0, byte_view.numel(), _HASH_CHUNK_BYTES):
chunk = byte_view[offset : offset + _HASH_CHUNK_BYTES]
digest.update(memoryview(chunk.numpy()))
def runtime_content_digest(records: Sequence[RuntimeTensor]) -> str:
"""Hash ordered runtime tensor metadata and contents."""
digest = hashlib.sha256()
for record in records:
digest.update(canonical_json({"name": record.name, **tensor_metadata(record)}))
_update_hash_with_tensor(digest, record.tensor)
return digest.hexdigest()
def file_digest(path: Path) -> str:
"""Hash one cache file with a bounded read working set."""
digest = hashlib.sha256()
with path.open("rb") as handle:
while chunk := handle.read(_FILE_HASH_CHUNK_BYTES):
digest.update(chunk)
return digest.hexdigest()
@contextlib.contextmanager
def exclusive_lock(path: Path, timeout_seconds: float) -> Iterator[None]:
"""Acquire one non-blocking file lock within a bounded wait."""
path.parent.mkdir(parents=True, exist_ok=True)
deadline = time.monotonic() + timeout_seconds
with path.open("a+b") as handle:
while True:
try:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except OSError as exc:
if exc.errno not in (errno.EACCES, errno.EAGAIN):
raise HostWeightCacheError("lock_failed", f"failed to lock {path}: {exc}") from exc
remaining = deadline - time.monotonic()
if remaining <= 0:
raise HostWeightCacheError(
"lock_timeout",
f"timed out after {timeout_seconds:g}s waiting for host weight cache writer {path.name}",
) from exc
time.sleep(min(0.2, remaining))
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
def fsync_file(path: Path) -> None:
"""Persist one cache file before publication."""
with path.open("rb") as handle:
os.fsync(handle.fileno())
def fsync_directory(path: Path) -> None:
"""Persist directory entries needed for atomic publication."""
fd = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(fd)
finally:
os.close(fd)
def split_shards(
records: Sequence[_RuntimeTensorT],
max_shard_bytes: int,
) -> list[list[_RuntimeTensorT]]:
"""Split ordered tensors without exceeding the target when possible."""
shards: list[list[_RuntimeTensorT]] = []
current: list[_RuntimeTensorT] = []
current_bytes = 0
for record in records:
nbytes = record.tensor.numel() * record.tensor.element_size()
if current and current_bytes + nbytes > max_shard_bytes:
shards.append(current)
current = []
current_bytes = 0
current.append(record)
current_bytes += nbytes
if current:
shards.append(current)
return shards
def remove_stale_temps(cache_root: Path, cache_key: str) -> None:
"""Remove abandoned temporary entries for one cache identity."""
temp_root = cache_root / ".tmp"
if not temp_root.is_dir():
return
for path in temp_root.glob(f"{cache_key}.*"):
if path.is_dir():
shutil.rmtree(path, ignore_errors=True)
else:
path.unlink(missing_ok=True)
__all__ = [
"IdentityNormalizationError",
"JsonValue",
"HostWeightCacheError",
"RuntimeTensor",
"canonical_json",
"canonicalize_existing_local_path",
"exclusive_lock",
"file_digest",
"fsync_directory",
"fsync_file",
"implementation_fingerprint",
"normalize_identity",
"remove_stale_temps",
"runtime_content_digest",
"split_shards",
"tensor_metadata",
]
@@ -9,7 +9,6 @@ import json
import os import os
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal
import torch import torch
from safetensors import safe_open from safetensors import safe_open
@@ -29,7 +28,7 @@ TensorTransform = Callable[[torch.Tensor], torch.Tensor]
class TensorBinding: class TensorBinding:
"""One runtime tensor backed by one safetensors entry.""" """One runtime tensor backed by one safetensors entry."""
storage_key: str checkpoint_key: str
file_path: str file_path: str
transform: TensorTransform | None = None transform: TensorTransform | None = None
@@ -38,16 +37,9 @@ class TensorBinding:
class HostWeightPlan: class HostWeightPlan:
"""Complete, prevalidated host backing consumed by an offload backend.""" """Complete, prevalidated host backing consumed by an offload backend."""
backing_kind: Literal["checkpoint_mmap", "host_weight_cache"] backing_kind: str
bindings: dict[str, TensorBinding] bindings: dict[str, TensorBinding]
planned_source_prefixes: frozenset[str] = frozenset() planned_source_prefixes: frozenset[str] = frozenset()
runtime_layout_key: str | None = None
expected_file_digests: dict[str, str] | None = None
@property
def requires_backend(self) -> bool:
"""Whether ordinary loading was skipped and this plan must be consumed."""
return self.backing_kind == "checkpoint_mmap"
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -56,7 +48,6 @@ class HostWeightPlanResult:
plan: HostWeightPlan | None plan: HostWeightPlan | None
fallback_reason: str | None = None fallback_reason: str | None = None
fallback_code: str | None = None
class _PlanIncompatibleError(RuntimeError): class _PlanIncompatibleError(RuntimeError):
@@ -267,11 +258,11 @@ def _validate_source_metadata(
with safe_open(file_path, framework="pt", device="cpu") as handle: with safe_open(file_path, framework="pt", device="cpu") as handle:
available = set(handle.keys()) available = set(handle.keys())
for runtime_name, target, binding in entries: for runtime_name, target, binding in entries:
if binding.storage_key not in available: if binding.checkpoint_key not in available:
raise _PlanIncompatibleError( raise _PlanIncompatibleError(
f"checkpoint key {binding.storage_key!r} for {runtime_name!r} is missing from {file_path}" f"checkpoint key {binding.checkpoint_key!r} for {runtime_name!r} is missing from {file_path}"
) )
source = handle.get_slice(binding.storage_key) source = handle.get_slice(binding.checkpoint_key)
source_shape = tuple(source.get_shape()) source_shape = tuple(source.get_shape())
source_dtype = _SAFETENSORS_DTYPES.get(source.get_dtype()) source_dtype = _SAFETENSORS_DTYPES.get(source.get_dtype())
if source_shape != tuple(target.shape): if source_shape != tuple(target.shape):
@@ -296,19 +287,11 @@ def build_checkpoint_mmap_plan(
) -> HostWeightPlanResult: ) -> HostWeightPlanResult:
"""Build a complete direct-checkpoint plan or return a fallback reason.""" """Build a complete direct-checkpoint plan or return a fallback reason."""
if tensor_parallel_size != 1: if tensor_parallel_size != 1:
return HostWeightPlanResult( return HostWeightPlanResult(None, f"TP={tensor_parallel_size} requires the ordinary loader")
None,
f"TP={tensor_parallel_size} requires the ordinary loader",
"unsupported_tp",
)
if use_hsdp: if use_hsdp:
return HostWeightPlanResult(None, "HSDP requires the ordinary loader", "unsupported_hsdp") return HostWeightPlanResult(None, "HSDP requires the ordinary loader")
if online_quantization: if online_quantization:
return HostWeightPlanResult( return HostWeightPlanResult(None, "online quantization requires the ordinary loader")
None,
"online quantization requires the ordinary loader",
"unsupported_online_quantization",
)
remap_fn = getattr(type(pipeline), "_remap_ckpt_key", None) remap_fn = getattr(type(pipeline), "_remap_ckpt_key", None)
if not callable(remap_fn): if not callable(remap_fn):
@@ -345,14 +328,14 @@ def build_checkpoint_mmap_plan(
) )
checkpoint_key, file_path = model_to_ckpt[runtime_name] checkpoint_key, file_path = model_to_ckpt[runtime_name]
bindings[runtime_name] = TensorBinding( bindings[runtime_name] = TensorBinding(
storage_key=checkpoint_key, checkpoint_key=checkpoint_key,
file_path=file_path, file_path=file_path,
transform=policy.transform if policy is not None else None, transform=policy.transform if policy is not None else None,
) )
_validate_source_metadata(required, bindings) _validate_source_metadata(required, bindings)
except (OSError, ValueError, _PlanIncompatibleError) as exc: except (OSError, ValueError, _PlanIncompatibleError) as exc:
return HostWeightPlanResult(None, str(exc), "checkpoint_plan_incompatible") return HostWeightPlanResult(None, str(exc))
return HostWeightPlanResult( return HostWeightPlanResult(
HostWeightPlan( HostWeightPlan(
@@ -29,7 +29,7 @@ from vllm_omni.diffusion.cache.cachedit import (
) )
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.parallel_state import ( from vllm_omni.diffusion.distributed.parallel_state import (
get_dit_group, get_world_group,
init_world_group, init_world_group,
) )
from vllm_omni.diffusion.distributed.utils import get_local_device from vllm_omni.diffusion.distributed.utils import get_local_device
@@ -423,7 +423,7 @@ def _validate_reference_image(image: Image.Image) -> None:
def _dit_rank_world() -> tuple[Any, int, int]: def _dit_rank_world() -> tuple[Any, int, int]:
if not dist.is_initialized(): if not dist.is_initialized():
return None, 0, 1 return None, 0, 1
group = get_dit_group() group = get_world_group().device_group
return group, dist.get_rank(group), dist.get_world_size(group) return group, dist.get_rank(group), dist.get_world_size(group)
@@ -1715,7 +1715,7 @@ class MiniMaxH3Pipeline(
dist.broadcast( dist.broadcast(
has_audio_tensor, has_audio_tensor,
src=0, src=0,
group=get_dit_group(), group=get_world_group().device_group,
) )
has_audio = [bool(value) for value in has_audio_tensor.tolist()] has_audio = [bool(value) for value in has_audio_tensor.tolist()]
+2 -2
View File
@@ -18,7 +18,7 @@ from transformers.dynamic_module_utils import get_class_from_dynamic_module
from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor import ( from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor import (
DistributedVaeMixin, DistributedVaeMixin,
) )
from vllm_omni.diffusion.distributed.parallel_state import get_dit_group from vllm_omni.diffusion.distributed.parallel_state import get_world_group
from vllm_omni.diffusion.offloader.module_residency import PinnedModuleStager from vllm_omni.diffusion.offloader.module_residency import PinnedModuleStager
from .packed_tokens import minimax_h3_patchify_video_latent from .packed_tokens import minimax_h3_patchify_video_latent
@@ -157,7 +157,7 @@ class MiniMaxH3VideoVAE(nn.Module, DistributedVaeMixin):
) -> None: ) -> None:
if mode != "tile": if mode != "tile":
raise ValueError(f"MiniMax H3 VAE supports its native tile parallel mode only, got {mode!r}") raise ValueError(f"MiniMax H3 VAE supports its native tile parallel mode only, got {mode!r}")
group = get_dit_group() group = get_world_group().device_group
world_size = dist.get_world_size(group) world_size = dist.get_world_size(group)
rank = dist.get_rank(group) rank = dist.get_rank(group)
parallel_size = int(parallel_size) parallel_size = int(parallel_size)
+7 -12
View File
@@ -71,9 +71,8 @@ def get_offload_backend(
Args: Args:
od_config: OmniDiffusionConfig with offload settings od_config: OmniDiffusionConfig with offload settings
device: Target device (auto-detected if None) device: Target device (auto-detected if None)
host_weight_plan: Exact loader-produced backing plan. Checkpoint plans host_weight_plan: Exact loader-produced backing plan, if ordinary
may have skipped ordinary materialization; host weight cache plans weight materialization was skipped.
describe already-valid ordinary tensors that can be remapped.
Returns: Returns:
OffloadBackend instance or None if offloading disabled OffloadBackend instance or None if offloading disabled
@@ -86,14 +85,10 @@ def get_offload_backend(
# Extract and validate configuration # Extract and validate configuration
config = OffloadConfig.from_od_config(od_config) config = OffloadConfig.from_od_config(od_config)
plan_requires_backend = host_weight_plan is not None and host_weight_plan.requires_backend
if host_weight_plan is not None and config.strategy != OffloadStrategy.DISTRIBUTED_LAYER_WISE: if host_weight_plan is not None and config.strategy != OffloadStrategy.DISTRIBUTED_LAYER_WISE:
if plan_requires_backend: raise RuntimeError(
raise RuntimeError( "A loader-owned DLO host-weight plan was produced, but distributed layerwise offload is not selected"
"A loader-owned DLO host-weight plan was produced, but distributed layerwise offload is not selected" )
)
logger.warning("Ignoring optional host weight cache plan because distributed layerwise offload is not selected")
host_weight_plan = None
# Return None if no offloading requested # Return None if no offloading requested
if config.strategy == OffloadStrategy.NONE: if config.strategy == OffloadStrategy.NONE:
@@ -101,7 +96,7 @@ def get_offload_backend(
# Validate platform (CUDA required for now) # Validate platform (CUDA required for now)
if not current_omni_platform.supports_cpu_offload() or current_omni_platform.get_device_count() < 1: if not current_omni_platform.supports_cpu_offload() or current_omni_platform.get_device_count() < 1:
if plan_requires_backend: if host_weight_plan is not None:
raise RuntimeError( raise RuntimeError(
"The loader skipped ordinary weight materialization for DLO, " "The loader skipped ordinary weight materialization for DLO, "
"but this platform cannot create the required offload backend" "but this platform cannot create the required offload backend"
@@ -117,7 +112,7 @@ def get_offload_backend(
try: try:
device = current_omni_platform.get_torch_device() device = current_omni_platform.get_torch_device()
except (NotImplementedError, AttributeError) as exc: except (NotImplementedError, AttributeError) as exc:
if plan_requires_backend: if host_weight_plan is not None:
raise RuntimeError( raise RuntimeError(
"The loader skipped ordinary weight materialization for DLO, " "The loader skipped ordinary weight materialization for DLO, "
"but the target device could not be resolved" "but the target device could not be resolved"
+1 -6
View File
@@ -31,9 +31,6 @@ class OffloadConfig:
# blocks from the loader-selected host backing with H2D only. # blocks from the loader-selected host backing with H2D only.
dlo_use_allgather: bool = True dlo_use_allgather: bool = True
dlo_resident_layers: int = 0 # leading DiT layers kept on device dlo_resident_layers: int = 0 # leading DiT layers kept on device
# Optional per-worker ceiling for registering shared host weight cache mappings.
# Zero applies no additional ceiling; pin_cpu_memory controls registration.
dlo_host_weight_cache_pin_limit_gib: float = 0.0
@classmethod @classmethod
def from_od_config(cls, od_config: OmniDiffusionConfig) -> "OffloadConfig": def from_od_config(cls, od_config: OmniDiffusionConfig) -> "OffloadConfig":
@@ -65,7 +62,7 @@ class OffloadConfig:
dp_size = 1 dp_size = 1
if parallel_config is not None: if parallel_config is not None:
dp_size = getattr(parallel_config, "data_parallel_size", 1) dp_size = getattr(parallel_config, "data_parallel_size", 1)
# HSDP's fully_shard_degree also contributes to effective DP # HSDP shard and replica sizes determine the effective group size.
hsdp_shard_size = getattr(parallel_config, "hsdp_shard_size", -1) if use_hsdp else -1 hsdp_shard_size = getattr(parallel_config, "hsdp_shard_size", -1) if use_hsdp else -1
hsdp_replicate_size = getattr(parallel_config, "hsdp_replicate_size", 1) if use_hsdp else 1 hsdp_replicate_size = getattr(parallel_config, "hsdp_replicate_size", 1) if use_hsdp else 1
if use_hsdp and hsdp_shard_size > 0: if use_hsdp and hsdp_shard_size > 0:
@@ -104,7 +101,6 @@ class OffloadConfig:
# requirements (concurrent requests, dummy run skip). # requirements (concurrent requests, dummy run skip).
dlo_use_allgather = getattr(od_config, "dlo_use_allgather", True) dlo_use_allgather = getattr(od_config, "dlo_use_allgather", True)
dlo_resident_layers = int(getattr(od_config, "dlo_resident_layers", 0)) dlo_resident_layers = int(getattr(od_config, "dlo_resident_layers", 0))
dlo_host_weight_cache_pin_limit_gib = float(getattr(od_config, "dlo_host_weight_cache_pin_limit_gib", 0.0))
if dlo_resident_layers < 0: if dlo_resident_layers < 0:
raise ValueError(f"dlo_resident_layers must be >= 0, got {dlo_resident_layers}") raise ValueError(f"dlo_resident_layers must be >= 0, got {dlo_resident_layers}")
if dlo_resident_layers and dlo_use_allgather: if dlo_resident_layers and dlo_use_allgather:
@@ -140,7 +136,6 @@ class OffloadConfig:
dp_size=dp_size, dp_size=dp_size,
dlo_use_allgather=dlo_use_allgather, dlo_use_allgather=dlo_use_allgather,
dlo_resident_layers=dlo_resident_layers, dlo_resident_layers=dlo_resident_layers,
dlo_host_weight_cache_pin_limit_gib=dlo_host_weight_cache_pin_limit_gib,
) )
@@ -1,280 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CUDA implementation of read-only host-mapping registration."""
from __future__ import annotations
import ctypes
import mmap
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Protocol
import torch
from .host_registration import (
HostRegistrationBudgetError,
HostRegistrationCleanupError,
HostRegistrationError,
)
_CUDA_HOST_REGISTER_READ_ONLY = 0x08
_CUDA_DEVICE_ATTRIBUTE_HOST_REGISTER_READ_ONLY_SUPPORTED = 113
CudaHostRegistrationError = HostRegistrationError
CudaHostRegistrationBudgetError = HostRegistrationBudgetError
CudaHostRegistrationCleanupError = HostRegistrationCleanupError
class _CudaRuntime(Protocol):
def cudaHostRegister(self, address: int, size: int, flags: int) -> int: ...
def cudaHostUnregister(self, address: int) -> int: ...
def cudaGetErrorString(self, error: int) -> str | bytes: ...
@dataclass(frozen=True)
class _AddressRange:
start: int
end: int
@property
def size(self) -> int:
return self.end - self.start
def _coalesce_ranges(
ranges: Sequence[tuple[int, int]],
page_size: int = mmap.PAGESIZE,
) -> tuple[_AddressRange, ...]:
"""Page-align and merge overlapping ranges from one backing mapping."""
if page_size <= 0:
raise ValueError(f"page_size must be positive, got {page_size}")
aligned: list[_AddressRange] = []
for start, size in ranges:
if start <= 0 or size < 0:
raise ValueError(f"invalid host range start={start}, size={size}")
if size == 0:
continue
aligned_start = start - start % page_size
end = start + size
aligned_end = ((end + page_size - 1) // page_size) * page_size
aligned.append(_AddressRange(aligned_start, aligned_end))
merged: list[_AddressRange] = []
for region in sorted(aligned, key=lambda item: (item.start, item.end)):
if merged and region.start <= merged[-1].end:
previous = merged[-1]
merged[-1] = _AddressRange(previous.start, max(previous.end, region.end))
else:
merged.append(region)
return tuple(merged)
def _tensor_regions(
sources_by_mapping: Mapping[str, Sequence[torch.Tensor]],
) -> tuple[_AddressRange, ...]:
"""Resolve storage spans without merging unrelated file mappings."""
regions: list[_AddressRange] = []
for mapping_name, tensors in sources_by_mapping.items():
ranges: list[tuple[int, int]] = []
for tensor in tensors:
if tensor.device.type != "cpu":
raise HostRegistrationError(
f"CUDA host registration requires CPU storage, but {mapping_name!r} contains {tensor.device}"
)
storage = tensor.untyped_storage()
ranges.append((storage.data_ptr(), storage.nbytes()))
regions.extend(_coalesce_ranges(ranges))
return tuple(regions)
def _error_message(runtime: _CudaRuntime, error: int) -> str:
try:
message = runtime.cudaGetErrorString(error)
except Exception:
return str(error)
if isinstance(message, bytes):
return message.decode(errors="replace")
return str(message)
def _consume_last_cuda_error(expected_error: int) -> None:
"""Clear a handled CUDA Runtime error before returning to PyTorch.
PyTorch's ``torch.cuda.cudart()`` binding exposes host registration but not
``cudaGetLastError``. Resolve the already-loaded process symbol so this call
clears the same CUDA Runtime instance that produced the registration error.
A different pending error is not safe to hide behind the staging fallback.
"""
try:
get_last_error = ctypes.CDLL(None).cudaGetLastError
get_last_error.argtypes = []
get_last_error.restype = ctypes.c_int
pending_error = int(get_last_error())
except (AttributeError, OSError) as exc:
raise HostRegistrationCleanupError("cannot clear CUDA's pending error after host-registration failure") from exc
if pending_error not in (0, expected_error):
raise HostRegistrationCleanupError(
f"host registration returned CUDA error {expected_error}, but cudaGetLastError reported {pending_error}"
)
def _handled_error_message(runtime: _CudaRuntime, error: int) -> str:
"""Format and consume one nonzero CUDA Runtime return code."""
error_code = int(error)
message = _error_message(runtime, error)
try:
_consume_last_cuda_error(error_code)
except HostRegistrationCleanupError as exc:
raise HostRegistrationCleanupError(f"{message}; {exc}") from exc
return message
def _supports_read_only_host_registration(runtime: _CudaRuntime) -> bool:
"""Query support required by immutable file-backed cache mappings."""
try:
get_attribute = getattr(runtime, "cudaDeviceGetAttribute", None)
if get_attribute is None:
get_attribute = ctypes.CDLL(None).cudaDeviceGetAttribute
get_attribute.argtypes = [
ctypes.POINTER(ctypes.c_int),
ctypes.c_int,
ctypes.c_int,
]
get_attribute.restype = ctypes.c_int
except (AttributeError, OSError) as exc:
raise HostRegistrationError("cannot query CUDA read-only host-registration support") from exc
supported = ctypes.c_int()
try:
error = get_attribute(
ctypes.byref(supported),
_CUDA_DEVICE_ATTRIBUTE_HOST_REGISTER_READ_ONLY_SUPPORTED,
torch.accelerator.current_device_index(),
)
except Exception as exc:
raise HostRegistrationError(f"cannot query CUDA read-only host-registration support: {exc}") from exc
if int(error) != 0:
raise HostRegistrationError(
f"cudaDeviceGetAttribute(read-only host registration) failed: {_handled_error_message(runtime, error)}"
)
return bool(supported.value)
class CudaHostRegistration:
"""Own CUDA registrations for already-existing file-backed CPU tensors."""
def __init__(
self,
runtime: _CudaRuntime,
regions: tuple[_AddressRange, ...],
) -> None:
self._runtime = runtime
self._regions = regions
self._closed = False
@classmethod
def create(
cls,
sources_by_mapping: Mapping[str, Sequence[torch.Tensor]],
*,
max_bytes: int | None,
) -> CudaHostRegistration:
regions = _tensor_regions(sources_by_mapping)
total_bytes = sum(region.size for region in regions)
if max_bytes is not None and total_bytes > max_bytes:
raise HostRegistrationBudgetError(
f"mapped host ranges need {total_bytes} bytes, exceeding the {max_bytes}-byte registration budget"
)
if not regions:
raise HostRegistrationError("no non-empty host ranges were available for registration")
if not torch.cuda.is_available():
raise HostRegistrationError("CUDA is not available")
try:
runtime = torch.cuda.cudart()
except Exception as exc:
raise HostRegistrationError(f"cannot access the CUDA runtime: {exc}") from exc
if not _supports_read_only_host_registration(runtime):
raise HostRegistrationError(
"CUDA device does not support read-only host registration required by immutable "
"host weight cache mappings"
)
registered: list[_AddressRange] = []
try:
for region in regions:
error = runtime.cudaHostRegister(
region.start,
region.size,
_CUDA_HOST_REGISTER_READ_ONLY,
)
if int(error) != 0:
raise HostRegistrationError(
"cudaHostRegister(read-only) failed for "
f"[{region.start:#x}, {region.end:#x}): {_handled_error_message(runtime, error)}"
)
registered.append(region)
unpinned = [
mapping_name
for mapping_name, tensors in sources_by_mapping.items()
if any(tensor.numel() and not tensor.is_pinned() for tensor in tensors)
]
if unpinned:
raise HostRegistrationError(
f"CUDA registration succeeded but PyTorch did not recognize pinned storage for {unpinned[:3]}"
)
except Exception as exc:
rollback_errors: list[str] = []
for region in reversed(registered):
try:
error = runtime.cudaHostUnregister(region.start)
if int(error) != 0:
rollback_errors.append(
f"cudaHostUnregister({region.start:#x}) failed: {_handled_error_message(runtime, error)}"
)
except Exception as rollback_exc:
rollback_errors.append(f"cudaHostUnregister({region.start:#x}) raised: {rollback_exc}")
if rollback_errors:
raise HostRegistrationCleanupError(
f"CUDA host registration failed ({exc}); rollback errors: {rollback_errors[:3]}"
) from exc
if isinstance(exc, HostRegistrationError):
raise
raise HostRegistrationError(f"CUDA host registration raised: {exc}") from exc
return cls(runtime, regions)
@property
def total_bytes(self) -> int:
return sum(region.size for region in self._regions)
@property
def region_count(self) -> int:
return len(self._regions)
def close(self) -> list[str]:
"""Unregister every range, returning errors after best-effort cleanup."""
if self._closed:
return []
errors: list[str] = []
failed: list[_AddressRange] = []
for region in reversed(self._regions):
try:
error = self._runtime.cudaHostUnregister(region.start)
if int(error) != 0:
errors.append(
f"cudaHostUnregister({region.start:#x}) failed: {_handled_error_message(self._runtime, error)}"
)
failed.append(region)
except Exception as exc:
errors.append(f"cudaHostUnregister({region.start:#x}) raised: {exc}")
failed.append(region)
self._regions = tuple(reversed(failed))
self._closed = not self._regions
return errors
@@ -18,9 +18,7 @@ This module implements the RFC-1 "Distributed Layerwise Offload" mechanism that:
from __future__ import annotations from __future__ import annotations
import time
from itertools import chain from itertools import chain
from pathlib import Path
from typing import Any from typing import Any
import torch import torch
@@ -29,7 +27,6 @@ from torch import nn
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm_omni.diffusion.hooks import HookRegistry, ModelHook from vllm_omni.diffusion.hooks import HookRegistry, ModelHook
from vllm_omni.diffusion.model_loader.host_weight_cache_utils import file_digest
from vllm_omni.diffusion.model_loader.host_weight_plan import ( from vllm_omni.diffusion.model_loader.host_weight_plan import (
HostWeightPlan, HostWeightPlan,
) )
@@ -37,12 +34,6 @@ from vllm_omni.platforms import current_omni_platform
from .base import OffloadBackend, OffloadConfig from .base import OffloadBackend, OffloadConfig
from .block_discovery import get_blocks_from_dit from .block_discovery import get_blocks_from_dit
from .host_registration import (
HostRegistration,
HostRegistrationCleanupError,
HostRegistrationError,
register_host_mappings,
)
from .module_collector import ModuleDiscovery from .module_collector import ModuleDiscovery
from .offload_plan import ( from .offload_plan import (
OffloadPlan, OffloadPlan,
@@ -102,7 +93,6 @@ class DistributedLayerwiseOffloadHook(ModelHook):
self.rank = rank self.rank = rank
self.pin_memory = pin_memory self.pin_memory = pin_memory
self.rank_local_mmap = rank_local_mmap self.rank_local_mmap = rank_local_mmap
self.registered_mmap = False
self.tensor_transforms = tensor_transforms or {} self.tensor_transforms = tensor_transforms or {}
self.copy_stream = copy_stream or current_omni_platform.Stream() self.copy_stream = copy_stream or current_omni_platform.Stream()
@@ -454,26 +444,6 @@ class DistributedLayerwiseOffloadHook(ModelHook):
# Prefetch: H2D + AllGather (overlapped on dedicated streams) # # Prefetch: H2D + AllGather (overlapped on dedicated streams) #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@staticmethod
def _resolve_mmap_source(
source_info: dict[str, Any],
meta: dict[str, Any],
dtype: torch.dtype,
) -> torch.Tensor:
source = source_info["tensor"]
transform = source_info["transform"]
if callable(transform):
source = transform(source)
if source.dtype != dtype or source.shape != meta["shape"] or source.stride() != meta["stride"]:
raise ValueError(
"mmap weight transform changed tensor layout for "
f"{source_info['name']!r}: expected dtype={dtype}, "
f"shape={tuple(meta['shape'])}, stride={meta['stride']}; "
f"got dtype={source.dtype}, shape={tuple(source.shape)}, "
f"stride={source.stride()}"
)
return source
@staticmethod @staticmethod
def _pack_mmap_sources( def _pack_mmap_sources(
cpu_sources: dict[torch.dtype, list[dict[str, Any]]], cpu_sources: dict[torch.dtype, list[dict[str, Any]]],
@@ -486,7 +456,18 @@ class DistributedLayerwiseOffloadHook(ModelHook):
destination = slot_buffers[dtype][:total_numel] destination = slot_buffers[dtype][:total_numel]
sources = cpu_sources[dtype] sources = cpu_sources[dtype]
for source_info, meta in zip(sources, metas, strict=True): for source_info, meta in zip(sources, metas, strict=True):
source = DistributedLayerwiseOffloadHook._resolve_mmap_source(source_info, meta, dtype) source = source_info["tensor"]
transform = source_info["transform"]
if callable(transform):
source = transform(source)
if source.dtype != dtype or source.shape != meta["shape"] or source.stride() != meta["stride"]:
raise ValueError(
"mmap weight transform changed tensor layout for "
f"{source_info['name']!r}: expected dtype={dtype}, "
f"shape={tuple(meta['shape'])}, stride={meta['stride']}; "
f"got dtype={source.dtype}, shape={tuple(source.shape)}, "
f"stride={source.stride()}"
)
start = meta["offset"] start = meta["offset"]
physical_storage = destination[start : start + meta["numel"]] physical_storage = destination[start : start + meta["numel"]]
if source.is_contiguous(): if source.is_contiguous():
@@ -500,32 +481,6 @@ class DistributedLayerwiseOffloadHook(ModelHook):
staged[dtype] = destination staged[dtype] = destination
return staged return staged
@staticmethod
def _copy_mmap_sources_to_device(
cpu_sources: dict[torch.dtype, list[dict[str, Any]]],
metadata: dict[torch.dtype, list[dict[str, Any]]],
gpu_buffers: dict[torch.dtype, torch.Tensor],
*,
non_blocking: bool,
) -> None:
"""Copy registered source views directly into flattened device buffers."""
for dtype, metas in metadata.items():
destination = gpu_buffers[dtype]
sources = cpu_sources[dtype]
for source_info, meta in zip(sources, metas, strict=True):
source = DistributedLayerwiseOffloadHook._resolve_mmap_source(source_info, meta, dtype)
start = meta["offset"]
physical_storage = destination[start : start + meta["numel"]]
async_copy = non_blocking and source.is_pinned()
if source.is_contiguous():
physical_storage.copy_(source.flatten(), non_blocking=async_copy)
else:
torch.as_strided(
physical_storage,
size=source.shape,
stride=source.stride(),
).copy_(source, non_blocking=async_copy)
def _stage_mmap_sources(self, slot: int) -> dict[torch.dtype, torch.Tensor]: def _stage_mmap_sources(self, slot: int) -> dict[torch.dtype, torch.Tensor]:
"""Pack this block's mmap views into one bounded host staging slot.""" """Pack this block's mmap views into one bounded host staging slot."""
previous_copy = self.cpu_staging_events[slot] previous_copy = self.cpu_staging_events[slot]
@@ -559,27 +514,17 @@ class DistributedLayerwiseOffloadHook(ModelHook):
assert gpu_weights is not None, f"gpu_buffers[{slot}] not allocated" assert gpu_weights is not None, f"gpu_buffers[{slot}] not allocated"
if self.dp_size <= 1 or self.dp_group is None: if self.dp_size <= 1 or self.dp_group is None:
if self.rank_local_mmap and self.registered_mmap: cpu_weights = self._stage_mmap_sources(slot) if self.rank_local_mmap else self.cpu_shards
with current_omni_platform.stream(self.copy_stream): with current_omni_platform.stream(self.copy_stream):
self._copy_mmap_sources_to_device( for dtype, cpu_shard in cpu_weights.items():
self.cpu_sources, gw = gpu_weights[dtype]
self.metadata, async_copy = non_blocking and cpu_shard.is_pinned()
gpu_weights, gw[: cpu_shard.numel()].copy_(cpu_shard, non_blocking=async_copy)
non_blocking=non_blocking, evt.record(self.copy_stream)
) if self.rank_local_mmap:
evt.record(self.copy_stream) # The CPU slot may be overwritten only after this H2D copy has
else: # finished. The shared event protects reuse by another hook.
cpu_weights = self._stage_mmap_sources(slot) if self.rank_local_mmap else self.cpu_shards self.cpu_staging_events[slot] = evt
with current_omni_platform.stream(self.copy_stream):
for dtype, cpu_shard in cpu_weights.items():
gw = gpu_weights[dtype]
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: else:
gpu_shards: dict[torch.dtype, torch.Tensor] = {} gpu_shards: dict[torch.dtype, torch.Tensor] = {}
shard_bufs = self.gpu_shard_buffers[slot] shard_bufs = self.gpu_shard_buffers[slot]
@@ -789,7 +734,6 @@ class PinnedResidentLayerGroup:
self.copy_stream = copy_stream self.copy_stream = copy_stream
self.loaded = False self.loaded = False
self.rank_local_mmap = rank_local_mmap self.rank_local_mmap = rank_local_mmap
self.registered_mmap = False
self.pin_memory = pin_memory self.pin_memory = pin_memory
self._states: list[dict[str, Any]] = [] self._states: list[dict[str, Any]] = []
self._gpu_buffers: list[dict[torch.dtype, torch.Tensor]] = [] self._gpu_buffers: list[dict[torch.dtype, torch.Tensor]] = []
@@ -860,14 +804,6 @@ class PinnedResidentLayerGroup:
ready = current_omni_platform.Event() ready = current_omni_platform.Event()
with current_omni_platform.stream(self.copy_stream): with current_omni_platform.stream(self.copy_stream):
for index, (state, block_buffers) in enumerate(zip(self._states, gpu_buffers)): for index, (state, block_buffers) in enumerate(zip(self._states, gpu_buffers)):
if self.rank_local_mmap and self.registered_mmap:
DistributedLayerwiseOffloadHook._copy_mmap_sources_to_device(
state["cpu_sources"],
state["metadata"],
block_buffers,
non_blocking=True,
)
continue
if self.rank_local_mmap: if self.rank_local_mmap:
slot = index % 2 slot = index % 2
previous_copy = self._cpu_staging_events[slot] previous_copy = self._cpu_staging_events[slot]
@@ -965,11 +901,8 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self._resident_layer_group: PinnedResidentLayerGroup | None = None self._resident_layer_group: PinnedResidentLayerGroup | None = None
self._using_mmap = False self._using_mmap = False
self._using_rank_local_mmap = False self._using_rank_local_mmap = False
self._using_registered_mmap = False
self.host_weight_plan = host_weight_plan self.host_weight_plan = host_weight_plan
self._mmap_transforms_by_tensor_id: dict[int, Any] = {} self._mmap_transforms_by_tensor_id: dict[int, Any] = {}
self._host_weight_cache_mapped_sources: dict[str, list[torch.Tensor]] = {}
self._host_registration: HostRegistration | None = None
def load_resident_layers(self) -> None: def load_resident_layers(self) -> None:
"""Load the model-declared leading blocks for the denoise stage.""" """Load the model-declared leading blocks for the denoise stage."""
@@ -1090,7 +1023,7 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
framework="pt", framework="pt",
device="cpu", device="cpu",
) )
tensor = file_cache[binding.file_path].get_tensor(binding.storage_key) tensor = file_cache[binding.file_path].get_tensor(binding.checkpoint_key)
if is_parameter: if is_parameter:
replacement = torch.nn.Parameter(tensor, requires_grad=target.requires_grad) replacement = torch.nn.Parameter(tensor, requires_grad=target.requires_grad)
parent._parameters[leaf_name] = replacement parent._parameters[leaf_name] = replacement
@@ -1101,6 +1034,9 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
logger.info("Realized %d loader-planned tensors as mmap views", len(loaded_names)) logger.info("Realized %d loader-planned tensors as mmap views", len(loaded_names))
if getattr(pipeline, "_dlo_fp8_cache_normalized", False):
self._process_normalized_fp8_cache(pipeline)
# Keep file handles open — _shard_and_pin will read from the mmap views. # Keep file handles open — _shard_and_pin will read from the mmap views.
# They will be released after _shard_and_pin completes (when params are # They will be released after _shard_and_pin completes (when params are
# replaced with offload placeholders). # replaced with offload placeholders).
@@ -1170,178 +1106,51 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
f"(first 5: {remaining_meta[:5]})." f"(first 5: {remaining_meta[:5]})."
) )
def _load_weights_from_host_weight_cache( @staticmethod
self, def _process_normalized_fp8_cache(pipeline: nn.Module) -> None:
pipeline: nn.Module, """Finalize serialized FP8 parameters after mmap replacement.
modules,
plan: HostWeightPlan,
) -> None:
"""Replace final ordinary-loader tensors with validated cache views."""
from safetensors import safe_open
if any(binding.transform is not None for binding in plan.bindings.values()): Phase-I cache files use the serialized FP8 checkpoint layout: the
raise RuntimeError("Host weight cache bindings cannot carry deferred checkpoint transforms") weight is stored before the quantization method's final transpose, and
``weight_scale`` is stored as an explicit cache tensor. The ordinary
loader normally runs this method before DLO owns the plan; mmap loading
must run the same finalizer after replacing meta parameters.
"""
from vllm.model_executor.layers.quantization.fp8 import Fp8LinearMethod
from vllm.model_executor.utils import replace_parameter
cache_files = {binding.file_path for binding in plan.bindings.values()} for module in pipeline.modules():
if plan.expected_file_digests is None or set(plan.expected_file_digests) != cache_files: quant_method = getattr(module, "quant_method", None)
raise RuntimeError("Host weight cache plan does not cover every shard digest") if not isinstance(quant_method, Fp8LinearMethod):
for file_path in sorted(cache_files): continue
try: if getattr(module, "_already_called_process_weights_after_loading", False):
actual_digest = file_digest(Path(file_path)) continue
except OSError as exc:
raise RuntimeError(f"Cannot revalidate host weight cache shard {file_path}: {exc}") from exc
if actual_digest != plan.expected_file_digests[file_path]:
raise RuntimeError(f"Host weight cache shard changed after loader validation: {file_path}")
required_names: set[str] = set() # ``Fp8LinearMethod.process_weights_after_loading`` also handles
for dit_name, dit_module in zip(modules.dit_names, modules.dits): # non-serialized checkpoints. Its common path calls
required_names.update(f"{dit_name}.{name}" for name, _ in dit_module.named_parameters()) # ``process_fp8_weight_tensor_strategy`` to requantize fused
for name, _ in dit_module.named_buffers(): # tensors, which is a CUDA-only operation. A normalized cache
parent_path, _, leaf_name = name.rpartition(".") # already contains the serialized FP8 values and the generated
owner = dit_module.get_submodule(parent_path) # scales, so only perform the layout transition and the kernel's
if leaf_name not in owner._non_persistent_buffers_set: # own preprocessing here. This keeps the warm path CPU/mmap
required_names.add(f"{dit_name}.{name}") # compatible and preserves the cache bytes exactly.
if required_names != set(plan.bindings): if quant_method.block_quant:
missing = sorted(required_names - set(plan.bindings)) raise ValueError("Phase-I normalized FP8 cache does not support block-quantized weights")
unexpected = sorted(set(plan.bindings) - required_names) replace_parameter(module, "weight", module.weight.t().data)
raise RuntimeError( # The online quantizer records one scalar. A serialized FP8
"Host weight cache plan coverage changed after loader validation: " # checkpoint may expose one repeated slot per logical shard in a
f"missing={missing[:5]}, unexpected={unexpected[:5]}" # fused module, but those slots must agree before they can be
) # collapsed to the canonical per-tensor ``[1, 1]`` form.
weight_scale = module.weight_scale.reshape(-1)
file_cache: dict[str, Any] = {} if weight_scale.numel() > 1 and not torch.all(weight_scale == weight_scale[0]):
replacements: list[tuple[torch.Tensor, torch.Tensor]] = [] raise ValueError("Phase-I normalized FP8 cache contains non-uniform fused weight scales")
mapped_sources: dict[str, list[torch.Tensor]] = {} weight_scale = weight_scale[:1].reshape(1, 1)
try: replace_parameter(module, "weight_scale", weight_scale.data)
# Validate and map every tensor before mutating the pipeline. A module.input_scale = None
# missing/corrupt shard therefore leaves the ordinary tensors intact. if quant_method.use_marlin and hasattr(quant_method.fp8_linear, "marlin_input_dtype"):
for runtime_name, binding in plan.bindings.items(): quant_method.fp8_linear.marlin_input_dtype = quant_method.marlin_input_dtype
parent_path, _, leaf_name = runtime_name.rpartition(".") quant_method.fp8_linear.process_weights_after_loading(module)
try: module._already_called_process_weights_after_loading = True
parent = pipeline.get_submodule(parent_path)
except AttributeError as exc:
raise RuntimeError(f"Host weight cache target module {parent_path!r} no longer exists") from exc
target = parent._parameters.get(leaf_name)
if target is None:
target = parent._buffers.get(leaf_name)
if target is None:
raise RuntimeError(f"Host weight cache target tensor {runtime_name!r} no longer exists")
if target.device.type != "cpu" or target.is_meta:
raise RuntimeError(
f"Host weight cache target {runtime_name!r} is not an ordinary CPU tensor: {target.device}"
)
if binding.file_path not in file_cache:
file_cache[binding.file_path] = safe_open(
binding.file_path,
framework="pt",
device="cpu",
)
tensor = file_cache[binding.file_path].get_tensor(binding.storage_key)
if tensor.shape != target.shape or tensor.dtype != target.dtype:
raise RuntimeError(
f"Host weight cache metadata changed for {runtime_name!r}: "
f"cache={tuple(tensor.shape)}/{tensor.dtype}, "
f"runtime={tuple(target.shape)}/{target.dtype}"
)
replacements.append((target, tensor))
mapped_sources.setdefault(binding.file_path, []).append(tensor)
except Exception:
file_cache.clear()
raise
# Preserve Parameter objects and their loader/runtime attributes; only
# their storage changes. This also keeps any references held by model
# code valid while dropping the private allocator-backed storage.
for target, tensor in replacements:
set_tensor_storage(target, tensor)
self._mmap_transforms_by_tensor_id.clear()
self._mmap_file_cache = file_cache
self._host_weight_cache_mapped_sources = mapped_sources
logger.info(
"Remapped %d final DiT tensors from host weight cache %s",
len(replacements),
plan.runtime_layout_key[:12] if plan.runtime_layout_key else "unknown",
)
def _try_register_host_weight_cache_mmap(self) -> bool:
"""Register complete cache mappings under the pinned-memory policy."""
pin_limit_gib = self.config.dlo_host_weight_cache_pin_limit_gib
if not self.config.pin_cpu_memory:
logger.info("DLO host weight cache registration disabled by pin_cpu_memory=False; using host staging")
return False
if not self._host_weight_cache_mapped_sources:
logger.warning("DLO host weight cache registration found no mapped sources; using bounded host staging")
return False
max_bytes = int(pin_limit_gib * 1024**3) if pin_limit_gib > 0 else None
started = time.perf_counter()
try:
registration = register_host_mappings(
self._host_weight_cache_mapped_sources,
device=self.device,
max_bytes=max_bytes,
)
except HostRegistrationCleanupError:
# Falling back could unmap a range the platform still owns. Abort
# the worker so process/context teardown provides final cleanup.
logger.exception("DLO host weight cache registration rollback failed")
raise
except HostRegistrationError as exc:
logger.warning(
"DLO host weight cache direct H2D unavailable (%s); using bounded host staging",
exc,
)
return False
self._host_registration = registration
logger.info(
"Registered %.2f GiB of shared host weight cache mappings in %d range(s) for direct H2D in %.3f s",
registration.total_bytes / 1024**3,
registration.region_count,
time.perf_counter() - started,
)
return True
def _configure_host_weight_cache_transfer(
self,
hooks: list[DistributedLayerwiseOffloadHook],
) -> None:
"""Select registered mmap or staging once for every cache consumer."""
plan = self.host_weight_plan
if (
(not hooks and self._resident_layer_group is None)
or not self._using_rank_local_mmap
or plan is None
or plan.backing_kind != "host_weight_cache"
):
return
self._using_registered_mmap = self._try_register_host_weight_cache_mmap()
self._host_weight_cache_mapped_sources.clear()
for hook in hooks:
hook.registered_mmap = self._using_registered_mmap
if self._resident_layer_group is not None:
self._resident_layer_group.registered_mmap = self._using_registered_mmap
if self._using_registered_mmap:
self._resident_layer_group._cpu_staging_buffers.clear()
def _release_registered_mmap(self) -> bool:
registration = self._host_registration
if registration is None:
return True
errors = registration.close()
if errors:
logger.error(
"DLO host weight cache unregistration failed; retaining mmap handles for retry/process exit: %s",
errors[:3],
)
return False
self._host_registration = None
logger.info("Unregistered DLO host weight cache mappings")
return True
def _init_dp_group(self) -> None: def _init_dp_group(self) -> None:
"""Reuse the process group initialized by parallel_state. """Reuse the process group initialized by parallel_state.
@@ -1683,41 +1492,17 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self._using_mmap = self.host_weight_plan is not None self._using_mmap = self.host_weight_plan is not None
self._using_rank_local_mmap = self._using_mmap and self.dp_size <= 1 self._using_rank_local_mmap = self._using_mmap and self.dp_size <= 1
if self._using_mmap: if self._using_mmap:
if self.host_weight_plan.backing_kind == "checkpoint_mmap": if self.host_weight_plan.backing_kind != "checkpoint_mmap":
self._load_weights_via_mmap(
pipeline,
modules,
self.host_weight_plan,
)
elif self.host_weight_plan.backing_kind == "host_weight_cache":
if self.config.dlo_use_allgather:
raise ValueError("Host weight cache backing is no-AllGather only in the first implementation")
self._using_rank_local_mmap = True
try:
self._load_weights_from_host_weight_cache(
pipeline,
modules,
self.host_weight_plan,
)
except Exception as exc:
logger.warning(
"DLO host weight cache remapping failed; retaining ordinary host tensors: %s",
exc,
)
self.host_weight_plan = None
self._using_mmap = False
self._using_rank_local_mmap = False
else:
# Assignments above dropped the model's private tensor
# references. Reclaim those allocations before hook setup
# so steady-state PSS reflects the shared mmap backing.
self._cleanup_after_loading()
else:
raise ValueError(f"Unsupported DLO host-weight backing: {self.host_weight_plan.backing_kind}") raise ValueError(f"Unsupported DLO host-weight backing: {self.host_weight_plan.backing_kind}")
self._load_weights_via_mmap(
pipeline,
modules,
self.host_weight_plan,
)
if self._using_rank_local_mmap: if self._using_rank_local_mmap:
logger.info( logger.info(
"DLO rank-local mmap storage enabled: file-backed pages are node-shared; " "DLO rank-local mmap storage enabled: checkpoint pages are "
"transfer setup will select registered direct H2D or bounded host staging" "node-shared; each worker owns only two bounded host staging slots"
) )
else: else:
remaining_meta = [ remaining_meta = [
@@ -1887,9 +1672,6 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
) )
pipeline._dlo_residency_controller = self pipeline._dlo_residency_controller = self
all_hooks = [hook for group in self._all_hook_groups for hook in group]
self._configure_host_weight_cache_transfer(all_hooks)
if not self._all_hook_groups: if not self._all_hook_groups:
self.enabled = bool(self._resident_blocks) self.enabled = bool(self._resident_blocks)
if self._using_mmap and not self.enabled: if self._using_mmap and not self.enabled:
@@ -1899,14 +1681,17 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
# Unified allocation: 2 shared output buffers + 2 shared shard buffers # Unified allocation: 2 shared output buffers + 2 shared shard buffers
# sized to the max block across ALL module groups (gen_layers + # sized to the max block across ALL module groups (gen_layers +
# language_model). Groups execute sequentially, so 2 buffers suffice. # language_model). Groups execute sequentially, so 2 buffers suffice.
all_hooks: list[DistributedLayerwiseOffloadHook] = []
for group in self._all_hook_groups:
all_hooks.extend(group)
unified_buffers = self._allocate_shared_buffers(all_hooks) unified_buffers = self._allocate_shared_buffers(all_hooks)
unified_shard_buffers = None unified_shard_buffers = None
if self.dp_size > 1: if self.dp_size > 1:
unified_shard_buffers = self._allocate_shared_shard_buffers(all_hooks) unified_shard_buffers = self._allocate_shared_shard_buffers(all_hooks)
unified_cpu_staging = None unified_cpu_staging = None
cpu_staging_events = None cpu_staging_events = None
if self._using_rank_local_mmap and not self._using_registered_mmap: if self._using_rank_local_mmap:
unified_cpu_staging = self._allocate_shared_cpu_staging_buffers( unified_cpu_staging = self._allocate_shared_cpu_staging_buffers(
all_hooks, all_hooks,
self._resident_layer_group, self._resident_layer_group,
@@ -1988,7 +1773,6 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
def _release_mmap_handles(self) -> None: def _release_mmap_handles(self) -> None:
"""Release safetensors mmap file handles.""" """Release safetensors mmap file handles."""
self._mmap_transforms_by_tensor_id.clear() self._mmap_transforms_by_tensor_id.clear()
self._host_weight_cache_mapped_sources.clear()
if hasattr(self, "_mmap_file_cache"): if hasattr(self, "_mmap_file_cache"):
self._mmap_file_cache.clear() self._mmap_file_cache.clear()
del self._mmap_file_cache del self._mmap_file_cache
@@ -2011,11 +1795,8 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
if not self.enabled and not hasattr(self, "_mmap_file_cache"): if not self.enabled and not hasattr(self, "_mmap_file_cache"):
return return
# Copy and communication streams are asynchronous in both AllGather if self._using_rank_local_mmap:
# and no-AllGather modes. Drain all device work before hooks, process current_omni_platform.synchronize()
# groups, registrations, or mapped storage can be torn down.
current_omni_platform.synchronize()
registration_released = self._release_registered_mmap()
for blocks in self._blocks: for blocks in self._blocks:
for block in blocks: for block in blocks:
@@ -2031,11 +1812,9 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
self._all_hook_groups.clear() self._all_hook_groups.clear()
self._resident_blocks.clear() self._resident_blocks.clear()
self._resident_layer_group = None self._resident_layer_group = None
if registration_released: self._release_mmap_handles()
self._release_mmap_handles()
self._using_mmap = False self._using_mmap = False
self._using_rank_local_mmap = False self._using_rank_local_mmap = False
self._using_registered_mmap = False
self.enabled = False self.enabled = False
logger.info("Distributed layer-wise offloading disabled") logger.info("Distributed layer-wise offloading disabled")
@@ -1,62 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Platform-neutral contract for registering existing host mappings."""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Protocol
import torch
class HostRegistrationError(RuntimeError):
"""Raised when existing host ranges cannot be registered safely."""
class HostRegistrationBudgetError(HostRegistrationError):
"""Raised before mutation when the complete mapping exceeds its budget."""
class HostRegistrationCleanupError(HostRegistrationError):
"""Raised when partial registration cannot be rolled back safely."""
class HostRegistration(Protocol):
"""Lifecycle owned by DLO for one platform's registered host ranges."""
@property
def total_bytes(self) -> int: ...
@property
def region_count(self) -> int: ...
def close(self) -> list[str]: ...
def register_host_mappings(
sources_by_mapping: Mapping[str, Sequence[torch.Tensor]],
*,
device: torch.device,
max_bytes: int | None,
) -> HostRegistration:
"""Register mappings with the active platform or report unsupported use.
CUDA is the first implementation. Other platforms retain DLO's bounded
host-staging path until they provide an equivalent registration backend.
``max_bytes=None`` applies no additional ceiling to the complete mapping.
"""
if device.type == "cuda":
from .cuda_host_registration import CudaHostRegistration
return CudaHostRegistration.create(sources_by_mapping, max_bytes=max_bytes)
raise HostRegistrationError(f"host-mapping registration is not supported on {device.type}")
__all__ = [
"HostRegistration",
"HostRegistrationBudgetError",
"HostRegistrationCleanupError",
"HostRegistrationError",
"register_host_mappings",
]
+6 -14
View File
@@ -279,9 +279,8 @@ class DiffusionWorker:
allgather_degree=parallel_config.allgather_degree, allgather_degree=parallel_config.allgather_degree,
tensor_parallel_size=parallel_config.tensor_parallel_size, tensor_parallel_size=parallel_config.tensor_parallel_size,
pipeline_parallel_size=parallel_config.pipeline_parallel_size, pipeline_parallel_size=parallel_config.pipeline_parallel_size,
fully_shard_degree=parallel_config.hsdp_shard_size if parallel_config.use_hsdp else 1,
hsdp_replicate_size=parallel_config.hsdp_replicate_size if parallel_config.use_hsdp else 1,
enable_expert_parallel=parallel_config.enable_expert_parallel, enable_expert_parallel=parallel_config.enable_expert_parallel,
use_hsdp=parallel_config.use_hsdp,
) )
if ( if (
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY) getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
@@ -859,18 +858,11 @@ class DiffusionWorker:
def shutdown(self) -> None: def shutdown(self) -> None:
"""Shutdown the worker and cleanup distributed environment.""" """Shutdown the worker and cleanup distributed environment."""
try: if self.model_runner is not None:
if self.model_runner is not None: mgr = getattr(self.model_runner, "kv_transfer_manager", None)
mgr = getattr(self.model_runner, "kv_transfer_manager", None) if mgr is not None:
try: mgr.shutdown_prefetch()
offload_backend = getattr(self.model_runner, "offload_backend", None) destroy_distributed_env()
if offload_backend is not None:
offload_backend.disable()
finally:
if mgr is not None:
mgr.shutdown_prefetch()
finally:
destroy_distributed_env()
class CustomPipelineWorkerExtension: class CustomPipelineWorkerExtension:
+1 -4
View File
@@ -545,11 +545,8 @@ class OrchestratorArgs:
enable_layerwise_offload: bool = False enable_layerwise_offload: bool = False
enable_distributed_layerwise_offload: bool = False enable_distributed_layerwise_offload: bool = False
dlo_use_allgather: bool = True dlo_use_allgather: bool = True
dlo_use_host_weight_cache: bool = False
dlo_host_weight_cache_dir: str | None = None
dlo_host_weight_cache_lock_timeout: float = 600.0
dlo_host_weight_cache_pin_limit_gib: float = 0.0
dlo_resident_layers: int = 0 dlo_resident_layers: int = 0
dlo_fp8_cache_dir: str | None = None
boundary_ratio: float | None = None boundary_ratio: float | None = None
flow_shift: float | None = None flow_shift: float | None = None
diffusion_kv_cache_dtype: str | None = None diffusion_kv_cache_dtype: str | None = None
+9 -5
View File
@@ -971,7 +971,7 @@ class AsyncOmniEngine:
ulysses_mode = normalized_kwargs.get("ulysses_mode") or "strict" ulysses_mode = normalized_kwargs.get("ulysses_mode") or "strict"
sequence_parallel_size = normalized_kwargs.get("sequence_parallel_size") sequence_parallel_size = normalized_kwargs.get("sequence_parallel_size")
pipeline_parallel_size = normalized_kwargs.get("pipeline_parallel_size") or 1 pipeline_parallel_size = normalized_kwargs.get("pipeline_parallel_size") or 1
data_parallel_size = normalized_kwargs.get("data_parallel_size") or 1 data_parallel_size = normalized_kwargs.get("data_parallel_size")
tensor_parallel_size = normalized_kwargs.get("tensor_parallel_size") or 1 tensor_parallel_size = normalized_kwargs.get("tensor_parallel_size") or 1
cfg_parallel_size = normalized_kwargs.get("cfg_parallel_size") or 1 cfg_parallel_size = normalized_kwargs.get("cfg_parallel_size") or 1
pipeline_parallel_size = normalized_kwargs.get("pipeline_parallel_size") or 1 pipeline_parallel_size = normalized_kwargs.get("pipeline_parallel_size") or 1
@@ -1004,6 +1004,11 @@ class AsyncOmniEngine:
hsdp_replicate_size=hsdp_replicate_size, hsdp_replicate_size=hsdp_replicate_size,
) )
num_gpus = normalized_kwargs.get("num_gpus")
if num_gpus is not None:
num_gpus = int(num_gpus)
parallel_config.resolve_data_parallel_size(num_gpus)
num_devices = max(1, int(parallel_config.world_size)) num_devices = max(1, int(parallel_config.world_size))
devices = ",".join(str(i) for i in range(num_devices)) devices = ",".join(str(i) for i in range(num_devices))
model_class_name = kwargs.get("model_class_name", None) model_class_name = kwargs.get("model_class_name", None)
@@ -1040,11 +1045,8 @@ class AsyncOmniEngine:
"enable_layerwise_offload": kwargs.get("enable_layerwise_offload", False), "enable_layerwise_offload": kwargs.get("enable_layerwise_offload", False),
"enable_distributed_layerwise_offload": kwargs.get("enable_distributed_layerwise_offload", False), "enable_distributed_layerwise_offload": kwargs.get("enable_distributed_layerwise_offload", False),
"dlo_use_allgather": kwargs.get("dlo_use_allgather", True), "dlo_use_allgather": kwargs.get("dlo_use_allgather", True),
"dlo_use_host_weight_cache": kwargs.get("dlo_use_host_weight_cache", False),
"dlo_host_weight_cache_dir": kwargs.get("dlo_host_weight_cache_dir", None),
"dlo_host_weight_cache_lock_timeout": kwargs.get("dlo_host_weight_cache_lock_timeout", 600.0),
"dlo_host_weight_cache_pin_limit_gib": kwargs.get("dlo_host_weight_cache_pin_limit_gib", 0.0),
"dlo_resident_layers": kwargs.get("dlo_resident_layers", 0), "dlo_resident_layers": kwargs.get("dlo_resident_layers", 0),
"dlo_fp8_cache_dir": kwargs.get("dlo_fp8_cache_dir", None),
"enforce_eager": False if kwargs.get("enforce_eager") is None else kwargs.get("enforce_eager"), "enforce_eager": False if kwargs.get("enforce_eager") is None else kwargs.get("enforce_eager"),
"diffusion_compile_granularity": ( "diffusion_compile_granularity": (
"regional" "regional"
@@ -1096,6 +1098,8 @@ class AsyncOmniEngine:
else {} else {}
), ),
} }
if num_gpus is not None:
stage_engine_args["num_gpus"] = num_gpus
# Only set dtype if it was already explicitly passed and normalized # Only set dtype if it was already explicitly passed and normalized
if "dtype" in normalized_kwargs: if "dtype" in normalized_kwargs:
stage_engine_args["dtype"] = normalized_kwargs["dtype"] stage_engine_args["dtype"] = normalized_kwargs["dtype"]
+8 -19
View File
@@ -722,25 +722,6 @@ class OmniServeCommand(CLISubcommand):
"independently (including existing TP shards)." "independently (including existing TP shards)."
), ),
) )
omni_config_group.add_argument(
"--dlo-use-host-weight-cache",
action="store_true",
help=(
"Use the node-local final-layout host weight cache for no-AllGather DLO. "
"With the default pinned-memory policy, supported platforms register the immutable mapping "
"for direct H2D; otherwise DLO uses bounded host staging."
),
)
omni_config_group.add_argument(
"--dlo-host-weight-cache-pin-limit-gib",
type=float,
default=0.0,
help=(
"Optional maximum GiB of read-only host weight cache mappings each worker may "
"register for direct H2D. Zero applies no additional registration ceiling; "
"this option requires --dlo-use-host-weight-cache."
),
)
omni_config_group.add_argument( omni_config_group.add_argument(
"--dlo-resident-layers", "--dlo-resident-layers",
type=int, type=int,
@@ -748,6 +729,14 @@ class OmniServeCommand(CLISubcommand):
help="Keep this many leading main-DiT blocks resident on the device " help="Keep this many leading main-DiT blocks resident on the device "
"while distributed layerwise offload streams the remaining blocks.", "while distributed layerwise offload streams the remaining blocks.",
) )
omni_config_group.add_argument(
"--dlo-fp8-cache-dir",
type=str,
default=None,
help="Optional Phase-I normalized online-FP8 cache directory. "
"A cache miss builds the cache after the ordinary loader; a valid "
"cache hit uses serialized FP8 checkpoint mmap with DLO.",
)
# Video model parameters (e.g., Wan2.2) - engine-level # Video model parameters (e.g., Wan2.2) - engine-level
omni_config_group.add_argument( omni_config_group.add_argument(
"--boundary-ratio", "--boundary-ratio",