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
29 changed files with 1029 additions and 398 deletions

No files matched your search

@@ -105,6 +105,31 @@ flags such as `_supports_mmap_loading` or parameter attributes for mmap-only
transforms. Model-specific direct-layout knowledge, when required, lives in a
checkpoint adapter beside the ordinary loader.
### Phase-I normalized online-FP8 cache
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:
```text
cache miss: BF16 source -> ordinary online FP8 loader -> atomic FP8 cache
cache hit: FP8 cache -> direct mmap plan -> DLO host shard -> AllGather
```
The cache stores the serialized runtime representation, including finalized FP8
weights, generated scales, and persistent transformer buffers. Its manifest
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.
Phase I keeps the cache binding deliberately simple: one source per transformer
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).
### AllGather path
With the default `dlo_use_allgather=True`, each rank stores approximately
@@ -211,10 +236,10 @@ 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.
A normalized runtime mmap cache, built through the ordinary loader, is the
proposed general mechanism for sharing transformed TP or quantized layouts.
That cache and its publication/lifecycle protocol are intentionally outside
this phase; see [RFC #6195](https://github.com/vllm-project/vllm-omni/issues/6195).
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).
## Validation coverage
@@ -42,6 +42,12 @@ vllm serve /path/to/model --omni \
vllm serve /path/to/model --omni \
--enable-distributed-layerwise-offload \
--usp 4
# Opt-in normalized online-FP8 cache (TP1 + DLO AllGather)
vllm serve /path/to/model --omni \
--quantization fp8 \
--enable-distributed-layerwise-offload \
--dlo-fp8-cache-dir /path/to/node-local/fp8-cache
```
```python
@@ -63,6 +69,7 @@ omni = Omni(
| `--dlo-use-allgather` | Shard host weights and reconstruct with AllGather | `true` |
| `--dlo-no-use-allgather` | Stream complete rank-local blocks without a DLO weight collective | `false` |
| `--dlo-resident-layers N` | Keep N leading main-DiT blocks on device; requires no-AllGather and model-declared resident paths | `0` |
| `--dlo-fp8-cache-dir PATH` | Opt into the Phase-I normalized online-FP8 cache for the transformer | unset |
## Host-weight loading
@@ -104,6 +111,24 @@ 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:
@@ -147,10 +172,9 @@ must enter each collective.
`OffloadPlan` that declares eligible `resident_dit_paths`.
- DP concurrency requires an explicit, identical inference-step count.
Sharing transformed TP or quantized runtime layouts through a normalized mmap
cache is a follow-up design in
[RFC #6195](https://github.com/vllm-project/vllm-omni/issues/6195), not part of
the direct-checkpoint path.
Sharing transformed TP or quantized runtime layouts beyond this TP1 transformer
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)
for an end-to-end example.
@@ -152,7 +152,8 @@ In `DiffusionParallelConfig`:
**Constraints:**
- `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 -4
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():
cfg = OmniStageDiffusionParallelConfig(
pipeline_parallel_size=2,
ulysses_degree=2,
ulysses_degree=4,
use_hsdp=True,
hsdp_shard_size=-1,
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():
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)
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)
@@ -74,14 +74,6 @@ def mock_dist(monkeypatch: pytest.MonkeyPatch):
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)
def mock_dist_vae_executor(monkeypatch: pytest.MonkeyPatch):
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, "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_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)
for name in ("_TP", "_PCP", "_DP", "_EP", "_PP"):
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, "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_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)
for name in ("_TP", "_PCP", "_DP", "_EP", "_PP"):
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.core_model
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_forward_context", lambda: fake_forward_context)
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)
for name in ("_TP", "_PCP", "_DP", "_EP", "_PP"):
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):
"""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(
tensor_parallel_size=2,
use_hsdp=True,
@@ -243,7 +243,7 @@ class TestDiffusionParallelConfigHSDP:
def test_hsdp_cannot_use_with_dp(self):
"""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(
data_parallel_size=2,
use_hsdp=True,
@@ -264,158 +264,6 @@ class TestDiffusionParallelConfigHSDP:
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:
"""Tests for _hsdp_shard_conditions matching logic."""
@@ -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
@@ -92,6 +92,30 @@ class TestParallelConfigPropagation:
od = _roundtrip_diffusion_config(model="x", parallel_config=pc)
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:
"""Verify engine_args structure from create_default_diffusion."""
@@ -58,6 +58,23 @@ def test_default_stage_devices_from_sequence_parallel():
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():
"""Ensure default diffusion parallel_config uses CLI/API parallel sizes."""
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
def test_default_stage_config_defaults_nullified_parallel_size_kwargs():
"""Ensure nullified diffusion parallel-size kwargs fall back to defaults."""
def test_default_stage_config_preserves_omitted_dp_for_runtime_inference():
"""Keep omitted DP unresolved until the runtime WORLD size is known."""
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(
{
"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"]
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.enable_expert_parallel is False
assert stage_cfg["engine_args"]["enforce_eager"] is False
@@ -330,6 +347,8 @@ def test_serve_cli_forwards_distributed_offload_residency():
"--dlo-no-use-allgather",
"--dlo-resident-layers",
"20",
"--dlo-fp8-cache-dir",
"/tmp/dlo-fp8-cache",
]
)
@@ -340,9 +359,11 @@ def test_serve_cli_forwards_distributed_offload_residency():
assert args.enable_distributed_layerwise_offload is True
assert args.dlo_use_allgather is False
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["dlo_use_allgather"] is False
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():
+12 -6
View File
@@ -600,12 +600,17 @@ class OmniStageDiffusionParallelConfig(OmniStageParallelConfig):
* self.cfg_parallel_size
)
if self.use_hsdp:
if self.tensor_parallel_size > 1 or self.data_parallel_size > 1:
raise ValueError(
"HSDP (use_hsdp=True) cannot be used with TP or DP "
f"(tensor_parallel_size={self.tensor_parallel_size}, "
f"data_parallel_size={self.data_parallel_size})"
)
incompatible = []
if self.tensor_parallel_size > 1:
incompatible.append("TP")
if self.data_parallel_size > 1:
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 other_parallel_world_size == 1:
raise ValueError("Cannot auto-calculate hsdp_shard_size when other parallelism is all 1")
@@ -688,6 +693,7 @@ class _DiffusionConfigProjection:
enable_distributed_layerwise_offload: bool = False
dlo_use_allgather: bool = True
dlo_resident_layers: int = Field(default=0, ge=0)
dlo_fp8_cache_dir: str | None = None
pin_cpu_memory: bool = True
diffusion_compile_granularity: Literal["regional", "full"] = "regional"
diffusion_compile_dynamic: bool = Field(default=True, strict=True)
+1
View File
@@ -383,6 +383,7 @@ class StageDeployConfig:
enable_distributed_layerwise_offload: bool | None = None
dlo_use_allgather: bool | None = None
dlo_resident_layers: int | None = None
dlo_fp8_cache_dir: str | None = None
# Diffusion-specific debug and observability knobs.
enable_diffusion_pipeline_profiler: bool | None = None
+59 -16
View File
@@ -154,8 +154,8 @@ class DiffusionParallelConfig:
pipeline_parallel_size: int = 1
"""Number of pipeline parallel stages."""
data_parallel_size: int = 1
"""Number of data parallel groups."""
data_parallel_size: int | None = None
"""Number of data parallel groups. Inferred from WORLD when omitted."""
tensor_parallel_size: int = 1
"""Number of tensor parallel groups."""
@@ -243,7 +243,7 @@ class DiffusionParallelConfig:
def _validate_parallel_config(self) -> Self:
"""Validates the config relationships among the parallel strategies."""
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.sequence_parallel_size > 0, "Sequence parallel size 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
)
# 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 = (
self.pipeline_parallel_size
* self.data_parallel_size
* data_parallel_size
* self.tensor_parallel_size
* self.sequence_parallel_size
* self.cfg_parallel_size
@@ -297,13 +299,17 @@ class DiffusionParallelConfig:
# 1. Standalone: when other parallelism is all 1, HSDP determines world_size
# 2. Combined: HSDP overlays on top of other parallelism
if self.use_hsdp:
if self.tensor_parallel_size > 1 or self.data_parallel_size > 1:
raise ValueError(
"HSDP (use_hsdp=True) cannot be used with TP or DP "
f"(tensor_parallel_size={self.tensor_parallel_size}, "
f"data_parallel_size={self.data_parallel_size}). "
"Set tensor_parallel_size=1 and data_parallel_size=1 when using HSDP."
)
incompatible = []
if self.tensor_parallel_size > 1:
incompatible.append("TP")
if data_parallel_size > 1:
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:
# Auto-calculate: use other_parallel_world_size as shard_size
if self.hsdp_replicate_size <= 0:
@@ -338,6 +344,42 @@ class DiffusionParallelConfig:
else:
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
def from_dict(cls, data: dict[str, Any]) -> "DiffusionParallelConfig":
"""
@@ -710,6 +752,10 @@ class OmniDiffusionConfig:
dlo_use_allgather: bool = True
# Leading main-DiT blocks kept resident by distributed layerwise offload.
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
@@ -999,10 +1045,7 @@ class OmniDiffusionConfig:
else:
self.num_gpus = 1
if self.num_gpus < self.parallel_config.world_size:
raise ValueError(
f"num_gpus ({self.num_gpus}) < parallel_config.world_size ({self.parallel_config.world_size})"
)
self.parallel_config.resolve_data_parallel_size(self.num_gpus)
if self.diffusion_compile_granularity == "full":
incompatible_features = []
@@ -6,11 +6,6 @@ from vllm_omni.diffusion.distributed.hsdp import (
HSDPInferenceConfig,
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 (
SequenceParallelConfig,
SequenceParallelInput,
@@ -46,8 +41,4 @@ __all__ = [
"get_sharding_validator",
"HSDPInferenceConfig",
"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
from vllm.logger import init_logger
from vllm_omni.diffusion.distributed.parallel_state import get_dit_group
logger = init_logger(__name__)
@@ -45,9 +43,10 @@ class DistributedVaeExecutor:
"""
def __init__(self):
self.group = get_dit_group()
self.world_size = dist.get_world_size(self.group)
self.rank = dist.get_rank(self.group)
# VAE tiles are scheduled across the complete worker WORLD.
self.group = None
self.world_size = dist.get_world_size()
self.rank = dist.get_rank()
self.parallel_size = 1
self.parallel_mode = "tile"
@@ -189,7 +188,7 @@ class DistributedVaeMixin:
if self.distributed_executor.parallel_size > pp_size:
logger.warning(
f"vae_patch_parallel_size={self.distributed_executor.parallel_size} "
f"is greater than dit_group={world_size};"
f" using dit_group size={world_size}"
f"is greater than WORLD={world_size};"
f" using WORLD size={world_size}"
)
return True
@@ -991,6 +991,18 @@ class PipelineGroupCoordinator(GroupCoordinator):
def _pipeline_isend_skip(self, tensor: torch.tensor):
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):
def __init__(
@@ -1031,3 +1043,15 @@ class SequenceParallelGroupCoordinator(GroupCoordinator):
self.ring_rank = torch.distributed.get_rank(self.ring_group)
self.allgather_world_size = torch.distributed.get_world_size(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_omni.diffusion.distributed.parallel_state import (
get_fs_group,
get_fully_shard_rank,
get_fully_shard_world_size,
get_world_group,
)
from vllm_omni.diffusion.distributed.parallel_state import get_world_group
from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
@@ -54,36 +49,36 @@ class HSDPInferenceConfig:
def _create_hsdp_mesh(
device_type: str,
replicate_size: int,
shard_pg: torch.distributed.ProcessGroup,
shard_size: int,
) -> 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:
device_type: The device type (e.g., "cuda", "npu")
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:
A 2D DeviceMesh with dimensions ("replicate", "shard")
"""
shard_size = torch.distributed.get_world_size(shard_pg)
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)
# Ranks are arranged so that each row is a shard group
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_type,
mesh_shape=(replicate_size, shard_size),
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(
"Created HSDP mesh: replicate_size=%d, shard_size=%d, mesh=%s",
replicate_size,
@@ -119,34 +114,24 @@ def apply_hsdp_to_model(
if not hsdp_config.enabled:
raise ValueError("HSDP is not enabled in config")
# Use GroupCoordinator for distributed info
world_group = get_world_group()
fs_group = get_fs_group()
world_size = world_group.world_size
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_shard_size = hsdp_config.hsdp_shard_size
# Validate that the FS group matches the HSDP shard size
if fs_world_size != hsdp_shard_size:
if hsdp_replicate_size * hsdp_shard_size != world_size:
raise ValueError(
f"FS group world_size ({fs_world_size}) does not match "
f"HSDP shard_size ({hsdp_shard_size}). "
"Ensure fully_shard_degree is set correctly in initialize_model_parallel."
f"HSDP mesh dimensions ({hsdp_replicate_size} x {hsdp_shard_size}) must equal WORLD size ({world_size})"
)
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_shard_size,
world_size,
rank,
fs_world_size,
fs_rank,
)
# 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
# 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:
# - 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)
device_mesh = _create_hsdp_mesh(
device_type=device_type,
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)
+108 -134
View File
@@ -29,11 +29,11 @@ If you only need to use the distributed environment without model parallelism,
"""
import inspect
from math import prod
import torch
import torch.distributed
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_omni.diffusion import envs
@@ -59,8 +59,6 @@ _SP: SequenceParallelGroupCoordinator | None = None
_PP: PipelineGroupCoordinator | None = None
_CFG: 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;
# it is reused by platform-specific runtimes that must build companion groups
@@ -184,7 +182,6 @@ class RankGenerator:
pp: int,
cfg: int,
dp: int,
fs: int = 1,
order: str = "tp-sp-pp-cfg-dp",
rank_offset: int = 0,
) -> None:
@@ -193,9 +190,7 @@ class RankGenerator:
self.pp = pp
self.cfg = cfg
self.dp = dp
self.fs = fs
self.rank_offset = rank_offset
self.world_size = tp * sp * pp * cfg * dp
self.name_to_size = {
"tp": self.tp,
@@ -203,15 +198,10 @@ class RankGenerator:
"pp": self.pp,
"cfg": self.cfg,
"dp": self.dp,
"fs": self.fs,
}
order = order.lower()
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:
raise RuntimeError(
f"The size of ({name}) is ({self.name_to_size[name]}), "
@@ -234,7 +224,7 @@ class RankGenerator:
mask[ordered_token.index(t)] = True
return mask
def get_ranks(self, token, independent_ranks: bool = False):
def get_ranks(self, token):
"""Get rank group by input token.
Arguments:
@@ -243,27 +233,11 @@ class RankGenerator:
to obtain multiple parallel types, we can use a hyphen
'-' to separate them. For example, if we want to obtain
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)
ranks = generate_masked_orthogonal_rank_groups(self.world_size, self.ordered_size, mask)
if self.rank_offset > 0:
for rank_group in ranks:
for i in range(len(rank_group)):
rank_group[i] += self.rank_offset
ranks = generate_masked_orthogonal_rank_groups(prod(self.ordered_size), self.ordered_size, mask)
if self.rank_offset:
return [[rank + self.rank_offset for rank in rank_group] for rank_group in ranks]
return ranks
@@ -376,22 +350,6 @@ def get_data_parallel_rank():
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():
"""Return True if in the last data parallel group, False otherwise."""
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
@@ -498,7 +445,6 @@ def init_model_parallel_group(
"expert",
"sequence",
"classifier_free_guidance",
"fully_shard",
], f"parallel_mode {parallel_mode} is not supported"
if parallel_mode == "pipeline":
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
def set_seq_parallel_pg(
sp_ulysses_degree: int,
@@ -737,8 +670,8 @@ def set_seq_parallel_pg(
return ulysses_pg, ring_pg, allgather_pg
def initialize_model_parallel(
data_parallel_size: int = 1,
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,
@@ -746,9 +679,8 @@ def initialize_model_parallel(
allgather_degree: int = 1,
tensor_parallel_size: int = 1,
pipeline_parallel_size: int = 1,
fully_shard_degree: int = 1,
hsdp_replicate_size: int = 1,
enable_expert_parallel: bool = False,
use_hsdp: bool = False,
backend: str | None = None,
) -> None:
if backend is None:
@@ -768,7 +700,6 @@ def initialize_model_parallel(
(causal=False only). Mutually exclusive with ulysses/ring in v1.
tensor_parallel_size: number of GPUs used for tensor 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.
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.
"""
# 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()
backend = backend or torch.distributed.get_backend(get_world_group().device_group)
if allgather_degree > 1:
assert ulysses_degree == 1 and ring_degree == 1, (
"AllGather-KV (allgather_degree>1) is mutually exclusive with Ulysses/Ring in v1. "
f"Got ulysses_degree={ulysses_degree}, ring_degree={ring_degree}, "
f"allgather_degree={allgather_degree}."
)
if ulysses_degree != 1 or ring_degree != 1:
raise ValueError(
"AllGather-KV (allgather_degree>1) is mutually exclusive with Ulysses/Ring in v1. "
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
if sequence_parallel_size is None:
@@ -820,42 +755,38 @@ def initialize_model_parallel(
f"but got {sequence_parallel_size}"
)
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) as dit_parallel_size
# This ensures orthogonal rank generation works correctly for all HSDP workers
if is_standalone_hsdp:
dit_parallel_size = fully_shard_degree * hsdp_replicate_size
if world_size < dit_parallel_size:
raise RuntimeError(
f"world_size ({world_size}) is less than "
f"tensor_parallel_size ({tensor_parallel_size}) x "
f"pipeline_parallel_size ({pipeline_parallel_size}) x"
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
non_dp_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:
if data_parallel_size not in (None, 1):
raise ValueError("HSDP (FSDP2) requires data_parallel_size to be 1")
if non_dp_size not in (1, world_size):
raise ValueError(f"HSDP non-DP parallel size must be 1 or WORLD size ({world_size}), but got {non_dp_size}")
data_parallel_size = 1
else:
inferred_data_parallel_size = world_size // non_dp_size
if data_parallel_size is not None and data_parallel_size != inferred_data_parallel_size:
raise ValueError(
f"data_parallel_size ({data_parallel_size}) does not match WORLD-derived value "
f"({inferred_data_parallel_size})"
)
data_parallel_size = inferred_data_parallel_size
rank_generator: RankGenerator = RankGenerator(
tensor_parallel_size,
sequence_parallel_size,
pipeline_parallel_size,
cfg_parallel_size,
effective_dp_size,
fs=fully_shard_degree,
data_parallel_size,
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
if enable_expert_parallel:
od_config = get_forward_context().omni_diffusion_config
@@ -863,11 +794,11 @@ def initialize_model_parallel(
if not use_moe_parallel_mapping:
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
assert _DP is None, "data parallel group is already initialized"
_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,
backend=backend,
parallel_mode="data",
@@ -877,7 +808,7 @@ def initialize_model_parallel(
global _CFG
assert _CFG is None, "classifier_free_guidance group is already initialized"
_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,
backend=backend,
parallel_mode="classifier_free_guidance",
@@ -885,7 +816,7 @@ def initialize_model_parallel(
global _PP
assert _PP is None, "pipeline model parallel group is already initialized"
_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,
backend=backend,
parallel_mode="pipeline",
@@ -899,7 +830,7 @@ def initialize_model_parallel(
sp_ring_degree=ring_degree,
sp_allgather_degree=allgather_degree,
rank=get_world_group().rank_in_group,
world_size=dit_parallel_size,
world_size=world_size,
sp_group_ranks=sp_group_ranks,
)
_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"
tp_group_ranks = rank_generator.get_ranks("tp")
tp_group_ranks = get_rank_groups("tp")
if use_moe_parallel_mapping:
vllm_parallel_state._TP = init_vllm_model_parallel_group(
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
# only when constructing the vLLM EP layout for expert-parallel paths.
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,
backend=backend,
group_name="dp",
)
global _FS, _EXPERT_PARALLEL_GROUP_RANKS
assert _FS is None, "fully shard group is already initialized"
_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")
global _EXPERT_PARALLEL_GROUP_RANKS
_EXPERT_PARALLEL_GROUP_RANKS = get_rank_groups("tp-sp-cfg-dp")
if use_moe_parallel_mapping:
vllm_parallel_state._EP = init_vllm_model_parallel_group(
group_ranks=_EXPERT_PARALLEL_GROUP_RANKS,
@@ -969,12 +892,63 @@ def initialize_model_parallel(
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():
"""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:
vllm_parallel_state._DP.destroy()
@@ -1005,14 +979,14 @@ def destroy_model_parallel():
vllm_parallel_state._EP = 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:
_PP.destroy()
_PP = None
if _FS:
_FS.destroy()
_FS = None
def destroy_distributed_environment():
global _WORLD
@@ -41,6 +41,8 @@ class _MiniMaxH3DirectMmapAdapter:
target: torch.Tensor,
) -> DirectMmapTensorPolicy:
del target
if getattr(self.pipeline, "_dlo_fp8_cache_normalized", False):
return DirectMmapTensorPolicy(allow_custom_loader=True)
transform = None
suffix = ".qkv_proj.weight"
if runtime_name.endswith(suffix):
@@ -8,7 +8,7 @@ import re
import time
from collections.abc import Generator, Iterable, Sequence
from pathlib import Path
from typing import cast
from typing import Any, cast
import torch
from torch import nn
@@ -34,6 +34,12 @@ from vllm_omni.diffusion.distributed.hsdp import HSDPInferenceConfig, apply_hsdp
from vllm_omni.diffusion.model_loader.checkpoint_adapters import (
get_checkpoint_adapter,
)
from vllm_omni.diffusion.model_loader.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 (
HostWeightPlan,
build_checkpoint_mmap_plan,
@@ -141,6 +147,10 @@ class DiffusersPipelineLoader:
self.quant_config = od_config.quantization_config
self.parallel_config = od_config.parallel_config
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:
"""Transfer the loader-produced plan to the offload backend."""
@@ -367,12 +377,61 @@ class DiffusersPipelineLoader:
)
def _get_weight_sources(self, model: nn.Module) -> tuple["ComponentSource", ...]:
return tuple(
sources = tuple(
cast(
Iterable[DiffusersPipelineLoader.ComponentSource],
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]:
"""Return parameter names that should be covered by strict load checks."""
@@ -403,6 +462,7 @@ class DiffusersPipelineLoader:
self.host_weight_plan = None
if load_format is None:
load_format = "default"
self._prepare_fp8_cache()
# CPU offload + quantization: for offline-quantized models (e.g., AutoRound MXFP8),
# weights are already quantized in the checkpoint — load directly on CPU.
# For online quantization, load on device so quantization can run on accelerator,
@@ -413,7 +473,9 @@ class DiffusersPipelineLoader:
is_offline = getattr(quant_cfg, "data_type", None) == "mx_fp" or getattr(
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
offload_after_quant = True
logger.info(
@@ -431,6 +493,11 @@ class DiffusersPipelineLoader:
)
else:
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)
_use_ag = getattr(self.od_config, "dlo_use_allgather", True)
@@ -520,6 +587,20 @@ class DiffusersPipelineLoader:
model.to("cpu")
logger.info("Quantization complete, offloaded model back to CPU")
if self._fp8_cache_requested and self._fp8_cache_dir is not None and not self._fp8_cache_hit:
try:
build_normalized_fp8_cache(
model,
sources=self._get_weight_sources(model),
cache_dir=self._fp8_cache_dir,
)
except Exception as exc:
logger.warning(
"DLO normalized FP8 cache build failed; retaining ordinary-loader path: %s",
exc,
exc_info=True,
)
self._apply_skip_softmax_calibration(model)
return model.eval()
@@ -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",
]
@@ -29,7 +29,7 @@ from vllm_omni.diffusion.cache.cachedit import (
)
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.parallel_state import (
get_dit_group,
get_world_group,
init_world_group,
)
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]:
if not dist.is_initialized():
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)
@@ -1715,7 +1715,7 @@ class MiniMaxH3Pipeline(
dist.broadcast(
has_audio_tensor,
src=0,
group=get_dit_group(),
group=get_world_group().device_group,
)
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 (
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 .packed_tokens import minimax_h3_patchify_video_latent
@@ -157,7 +157,7 @@ class MiniMaxH3VideoVAE(nn.Module, DistributedVaeMixin):
) -> None:
if mode != "tile":
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)
rank = dist.get_rank(group)
parallel_size = int(parallel_size)
+1 -1
View File
@@ -62,7 +62,7 @@ class OffloadConfig:
dp_size = 1
if parallel_config is not None:
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_replicate_size = getattr(parallel_config, "hsdp_replicate_size", 1) if use_hsdp else 1
if use_hsdp and hsdp_shard_size > 0:
@@ -1034,6 +1034,9 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
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.
# They will be released after _shard_and_pin completes (when params are
# replaced with offload placeholders).
@@ -1103,6 +1106,52 @@ class DistributedLayerwiseOffloadBackend(OffloadBackend):
f"(first 5: {remaining_meta[:5]})."
)
@staticmethod
def _process_normalized_fp8_cache(pipeline: nn.Module) -> None:
"""Finalize serialized FP8 parameters after mmap replacement.
Phase-I cache files use the serialized FP8 checkpoint layout: the
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
for module in pipeline.modules():
quant_method = getattr(module, "quant_method", None)
if not isinstance(quant_method, Fp8LinearMethod):
continue
if getattr(module, "_already_called_process_weights_after_loading", False):
continue
# ``Fp8LinearMethod.process_weights_after_loading`` also handles
# non-serialized checkpoints. Its common path calls
# ``process_fp8_weight_tensor_strategy`` to requantize fused
# tensors, which is a CUDA-only operation. A normalized cache
# already contains the serialized FP8 values and the generated
# scales, so only perform the layout transition and the kernel's
# own preprocessing here. This keeps the warm path CPU/mmap
# compatible and preserves the cache bytes exactly.
if quant_method.block_quant:
raise ValueError("Phase-I normalized FP8 cache does not support block-quantized weights")
replace_parameter(module, "weight", module.weight.t().data)
# The online quantizer records one scalar. A serialized FP8
# checkpoint may expose one repeated slot per logical shard in a
# 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)
if weight_scale.numel() > 1 and not torch.all(weight_scale == weight_scale[0]):
raise ValueError("Phase-I normalized FP8 cache contains non-uniform fused weight scales")
weight_scale = weight_scale[:1].reshape(1, 1)
replace_parameter(module, "weight_scale", weight_scale.data)
module.input_scale = None
if quant_method.use_marlin and hasattr(quant_method.fp8_linear, "marlin_input_dtype"):
quant_method.fp8_linear.marlin_input_dtype = quant_method.marlin_input_dtype
quant_method.fp8_linear.process_weights_after_loading(module)
module._already_called_process_weights_after_loading = True
def _init_dp_group(self) -> None:
"""Reuse the process group initialized by parallel_state.
@@ -279,9 +279,8 @@ class DiffusionWorker:
allgather_degree=parallel_config.allgather_degree,
tensor_parallel_size=parallel_config.tensor_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,
use_hsdp=parallel_config.use_hsdp,
)
if (
getattr(self.od_config, "diffusion_kv_mode", DiffusionKVCacheMode.DENSE_LEGACY)
+1
View File
@@ -546,6 +546,7 @@ class OrchestratorArgs:
enable_distributed_layerwise_offload: bool = False
dlo_use_allgather: bool = True
dlo_resident_layers: int = 0
dlo_fp8_cache_dir: str | None = None
boundary_ratio: float | None = None
flow_shift: float | None = None
diffusion_kv_cache_dtype: str | None = None
+9 -1
View File
@@ -971,7 +971,7 @@ class AsyncOmniEngine:
ulysses_mode = normalized_kwargs.get("ulysses_mode") or "strict"
sequence_parallel_size = normalized_kwargs.get("sequence_parallel_size")
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
cfg_parallel_size = normalized_kwargs.get("cfg_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,
)
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))
devices = ",".join(str(i) for i in range(num_devices))
model_class_name = kwargs.get("model_class_name", None)
@@ -1041,6 +1046,7 @@ class AsyncOmniEngine:
"enable_distributed_layerwise_offload": kwargs.get("enable_distributed_layerwise_offload", False),
"dlo_use_allgather": kwargs.get("dlo_use_allgather", True),
"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"),
"diffusion_compile_granularity": (
"regional"
@@ -1092,6 +1098,8 @@ class AsyncOmniEngine:
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
if "dtype" in normalized_kwargs:
stage_engine_args["dtype"] = normalized_kwargs["dtype"]
+8
View File
@@ -729,6 +729,14 @@ class OmniServeCommand(CLISubcommand):
help="Keep this many leading main-DiT blocks resident on the device "
"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
omni_config_group.add_argument(
"--boundary-ratio",