Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ae56cfcfb | ||
|
|
14bd8f8772 |
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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,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)
|
||||
|
||||
@@ -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()]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user