[Diffusion] Add node-local runtime weight cache for DLO
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
14f2c16fb2
commit
d30ccdc315
17 files changed
+1616
-68
No files matched your search
@@ -346,7 +346,7 @@ def test_dlo_plan_loads_component_sources_outside_planned_dit(monkeypatch):
|
||||
backing_kind="checkpoint_mmap",
|
||||
bindings={
|
||||
"transformer.weight": TensorBinding(
|
||||
checkpoint_key="weight",
|
||||
storage_key="weight",
|
||||
file_path="unused",
|
||||
)
|
||||
},
|
||||
@@ -405,6 +405,135 @@ def test_dlo_plan_fallback_runs_ordinary_loader(monkeypatch):
|
||||
assert loader.take_host_weight_plan() is None
|
||||
|
||||
|
||||
def test_dlo_runtime_cache_is_built_after_final_post_load_mutation(monkeypatch, tmp_path):
|
||||
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
|
||||
import vllm_omni.diffusion.model_loader.runtime_weight_cache as cache_mod
|
||||
|
||||
od_config = SimpleNamespace(
|
||||
dtype=torch.float32,
|
||||
parallel_config=SimpleNamespace(
|
||||
use_hsdp=False,
|
||||
tensor_parallel_size=1,
|
||||
sequence_parallel_size=1,
|
||||
data_parallel_size=2,
|
||||
cfg_parallel_size=1,
|
||||
pipeline_parallel_size=1,
|
||||
ulysses_degree=1,
|
||||
ring_degree=1,
|
||||
allgather_degree=1,
|
||||
ulysses_mode="strict",
|
||||
),
|
||||
quantization_config=None,
|
||||
enable_distributed_layerwise_offload=True,
|
||||
dlo_use_allgather=False,
|
||||
dlo_enable_runtime_cache=True,
|
||||
dlo_runtime_cache_dir=str(tmp_path),
|
||||
dlo_runtime_cache_lock_timeout=1.0,
|
||||
model="unused",
|
||||
revision=None,
|
||||
model_config={},
|
||||
)
|
||||
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
|
||||
model = nn.Module()
|
||||
model.transformer = nn.Linear(2, 2, bias=False)
|
||||
events: list[str] = []
|
||||
runtime_plan = HostWeightPlan(
|
||||
backing_kind="runtime_cache",
|
||||
bindings={},
|
||||
runtime_layout_key="layout-key",
|
||||
post_load_complete=True,
|
||||
)
|
||||
|
||||
def load_weights(_model):
|
||||
events.append("load")
|
||||
with torch.no_grad():
|
||||
model.transformer.weight.fill_(1)
|
||||
|
||||
def process_weights(_model, _device):
|
||||
events.append("process")
|
||||
with torch.no_grad():
|
||||
model.transformer.weight.add_(1)
|
||||
|
||||
def calibrate(_model):
|
||||
events.append("calibrate")
|
||||
with torch.no_grad():
|
||||
model.transformer.weight.add_(1)
|
||||
|
||||
original_eval = model.eval
|
||||
|
||||
def eval_model():
|
||||
events.append("eval")
|
||||
return original_eval()
|
||||
|
||||
def build_cache(_pipeline, **kwargs):
|
||||
events.append("cache")
|
||||
assert torch.equal(model.transformer.weight, torch.full((2, 2), 3.0))
|
||||
assert kwargs["tensor_parallel_rank"] == 0
|
||||
assert "data_parallel_size" not in kwargs["sequence_parallel_guard"]
|
||||
return HostWeightPlanResult(runtime_plan)
|
||||
|
||||
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
|
||||
loader.load_weights = load_weights # type: ignore[method-assign]
|
||||
loader._process_weights_after_loading = process_weights # type: ignore[method-assign]
|
||||
loader._apply_skip_softmax_calibration = calibrate # type: ignore[method-assign]
|
||||
model.eval = eval_model # type: ignore[method-assign]
|
||||
monkeypatch.setattr(
|
||||
loader_mod,
|
||||
"build_checkpoint_mmap_plan",
|
||||
lambda *_args, **_kwargs: HostWeightPlanResult(None, "requires normalization"),
|
||||
)
|
||||
monkeypatch.setattr(cache_mod, "build_runtime_weight_cache_plan", build_cache)
|
||||
|
||||
assert loader.load_model(load_device="cpu") is model
|
||||
assert events == ["load", "process", "calibrate", "eval", "cache"]
|
||||
assert loader.take_host_weight_plan() is runtime_plan
|
||||
|
||||
|
||||
def test_dlo_runtime_cache_failure_retains_ordinary_weights(monkeypatch, tmp_path):
|
||||
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
|
||||
import vllm_omni.diffusion.model_loader.runtime_weight_cache as cache_mod
|
||||
|
||||
od_config = SimpleNamespace(
|
||||
dtype=torch.float32,
|
||||
parallel_config=SimpleNamespace(
|
||||
use_hsdp=False,
|
||||
tensor_parallel_size=1,
|
||||
sequence_parallel_size=1,
|
||||
data_parallel_size=2,
|
||||
cfg_parallel_size=1,
|
||||
pipeline_parallel_size=1,
|
||||
),
|
||||
quantization_config=None,
|
||||
enable_distributed_layerwise_offload=True,
|
||||
dlo_use_allgather=False,
|
||||
dlo_enable_runtime_cache=True,
|
||||
dlo_runtime_cache_dir=str(tmp_path),
|
||||
model="unused",
|
||||
)
|
||||
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
|
||||
model = nn.Module()
|
||||
model.transformer = nn.Linear(2, 2, bias=False)
|
||||
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
|
||||
loader.load_weights = lambda _model: None # type: ignore[method-assign]
|
||||
loader._process_weights_after_loading = lambda *_args: None # type: ignore[method-assign]
|
||||
loader._apply_skip_softmax_calibration = lambda _model: None # type: ignore[method-assign]
|
||||
monkeypatch.setattr(
|
||||
loader_mod,
|
||||
"build_checkpoint_mmap_plan",
|
||||
lambda *_args, **_kwargs: HostWeightPlanResult(None, "requires normalization"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cache_mod,
|
||||
"build_runtime_weight_cache_plan",
|
||||
lambda *_args, **_kwargs: HostWeightPlanResult(None, "writer timed out", "lock_timeout"),
|
||||
)
|
||||
|
||||
original_weight = model.transformer.weight.detach().clone()
|
||||
assert loader.load_model(load_device="cpu") is model
|
||||
assert torch.equal(model.transformer.weight, original_weight)
|
||||
assert loader.take_host_weight_plan() is None
|
||||
|
||||
|
||||
def test_dlo_allgather_online_fp8_uses_ordinary_loader(monkeypatch):
|
||||
from vllm.model_executor.layers.quantization.online.fp8 import (
|
||||
Fp8PerTensorOnlineLinearMethod,
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import fcntl
|
||||
import multiprocessing as mp
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm_omni.diffusion.model_loader.runtime_weight_cache import (
|
||||
build_runtime_weight_cache_plan,
|
||||
default_runtime_cache_root,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.diffusion, pytest.mark.cpu, pytest.mark.core_model]
|
||||
|
||||
|
||||
class _LoaderMarker:
|
||||
pass
|
||||
|
||||
|
||||
class _Transformer(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.proj = nn.Linear(3, 2)
|
||||
self.register_buffer("persistent", torch.arange(2, dtype=torch.float32))
|
||||
self.register_buffer("temporary", torch.ones(1), persistent=False)
|
||||
with torch.no_grad():
|
||||
self.proj.weight.copy_(torch.arange(6, dtype=torch.float32).reshape(2, 3))
|
||||
self.proj.bias.copy_(torch.tensor([7.0, 8.0]))
|
||||
|
||||
|
||||
class _Pipeline(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.transformer = _Transformer()
|
||||
|
||||
|
||||
def test_default_root_is_not_split_by_replica_vllm_cache(monkeypatch):
|
||||
monkeypatch.setenv("VLLM_CACHE_ROOT", "/tmp/replica-specific-vllm-cache")
|
||||
|
||||
assert default_runtime_cache_root().endswith("/.cache/vllm-omni/dlo-runtime-weights")
|
||||
|
||||
|
||||
def _build(pipeline: nn.Module, cache_root: Path, **overrides):
|
||||
kwargs = {
|
||||
"dit_modules": (("transformer", pipeline.transformer),),
|
||||
"loader_type": _LoaderMarker,
|
||||
"cache_root": cache_root,
|
||||
"lock_timeout_seconds": 2.0,
|
||||
"max_shard_bytes": 32,
|
||||
"model_identity": "test/model",
|
||||
"revision": "revision",
|
||||
"runtime_dtype": torch.float32,
|
||||
"load_format": "default",
|
||||
"loader_inputs": {"model_config": {"hidden_size": 3}},
|
||||
"tensor_parallel_size": 1,
|
||||
"tensor_parallel_rank": 0,
|
||||
"sequence_parallel_guard": {"sequence_parallel_size": 1, "backend": "none"},
|
||||
"use_hsdp": False,
|
||||
"quantization_config": None,
|
||||
"cfg_parallel_size": 1,
|
||||
"pipeline_parallel_size": 1,
|
||||
}
|
||||
kwargs.update(overrides)
|
||||
return build_runtime_weight_cache_plan(pipeline, **kwargs)
|
||||
|
||||
|
||||
def _multiprocess_builder(cache_root: str, queue) -> None:
|
||||
result = _build(_Pipeline(), Path(cache_root))
|
||||
queue.put(
|
||||
(
|
||||
result.fallback_code,
|
||||
result.plan.runtime_layout_key if result.plan is not None else None,
|
||||
sorted(binding.file_path for binding in result.plan.bindings.values()) if result.plan is not None else [],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_equivalent_process_layouts_reuse_one_entry(tmp_path):
|
||||
first = _build(_Pipeline(), tmp_path)
|
||||
second = _build(_Pipeline(), tmp_path)
|
||||
|
||||
assert first.plan is not None
|
||||
assert second.plan is not None
|
||||
assert first.plan.runtime_layout_key == second.plan.runtime_layout_key
|
||||
assert first.plan.post_load_complete
|
||||
assert {binding.file_path for binding in first.plan.bindings.values()} == {
|
||||
binding.file_path for binding in second.plan.bindings.values()
|
||||
}
|
||||
assert set(first.plan.bindings) == {
|
||||
"transformer.persistent",
|
||||
"transformer.proj.bias",
|
||||
"transformer.proj.weight",
|
||||
}
|
||||
|
||||
|
||||
def test_tp_coordinate_and_sp_implementation_guard_split_entries(tmp_path):
|
||||
pipeline = _Pipeline()
|
||||
tp0 = _build(pipeline, tmp_path, tensor_parallel_size=2, tensor_parallel_rank=0)
|
||||
tp1 = _build(pipeline, tmp_path, tensor_parallel_size=2, tensor_parallel_rank=1)
|
||||
sp2 = _build(
|
||||
pipeline,
|
||||
tmp_path,
|
||||
sequence_parallel_guard={"sequence_parallel_size": 2, "backend": "ulysses"},
|
||||
)
|
||||
|
||||
assert tp0.plan is not None and tp1.plan is not None and sp2.plan is not None
|
||||
assert len({tp0.plan.runtime_layout_key, tp1.plan.runtime_layout_key, sp2.plan.runtime_layout_key}) == 3
|
||||
|
||||
|
||||
def test_final_runtime_content_is_authoritative(tmp_path):
|
||||
first_pipeline = _Pipeline()
|
||||
second_pipeline = _Pipeline()
|
||||
with torch.no_grad():
|
||||
second_pipeline.transformer.proj.weight[0, 0] += 1
|
||||
|
||||
first = _build(first_pipeline, tmp_path)
|
||||
second = _build(second_pipeline, tmp_path)
|
||||
|
||||
assert first.plan is not None and second.plan is not None
|
||||
assert first.plan.runtime_layout_key != second.plan.runtime_layout_key
|
||||
|
||||
|
||||
def test_corrupt_published_entry_is_rebuilt_under_the_key_lock(tmp_path):
|
||||
first = _build(_Pipeline(), tmp_path)
|
||||
assert first.plan is not None
|
||||
shard = Path(next(iter(first.plan.bindings.values())).file_path)
|
||||
old_inode = shard.stat().st_ino
|
||||
with shard.open("r+b") as handle:
|
||||
handle.seek(-1, 2)
|
||||
byte = handle.read(1)
|
||||
handle.seek(-1, 2)
|
||||
handle.write(bytes([byte[0] ^ 0xFF]))
|
||||
|
||||
second = _build(_Pipeline(), tmp_path)
|
||||
|
||||
assert second.plan is not None
|
||||
assert second.plan.runtime_layout_key == first.plan.runtime_layout_key
|
||||
assert Path(next(iter(second.plan.bindings.values())).file_path).stat().st_ino != old_inode
|
||||
|
||||
|
||||
def test_lock_timeout_keeps_the_ordinary_tensors(tmp_path):
|
||||
initial = _build(_Pipeline(), tmp_path)
|
||||
assert initial.plan is not None
|
||||
cache_key = initial.plan.runtime_layout_key
|
||||
entry_dir = Path(next(iter(initial.plan.bindings.values())).file_path).parent
|
||||
shutil.rmtree(entry_dir)
|
||||
lock_path = tmp_path / ".locks" / f"{cache_key}.lock"
|
||||
|
||||
with lock_path.open("a+b") as lock_handle:
|
||||
fcntl.flock(lock_handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
result = _build(_Pipeline(), tmp_path, lock_timeout_seconds=0.01)
|
||||
|
||||
assert result.plan is None
|
||||
assert result.fallback_code == "lock_timeout"
|
||||
|
||||
|
||||
def test_next_writer_removes_stale_temp_after_process_death(tmp_path):
|
||||
initial = _build(_Pipeline(), tmp_path)
|
||||
assert initial.plan is not None
|
||||
cache_key = initial.plan.runtime_layout_key
|
||||
entry_dir = Path(next(iter(initial.plan.bindings.values())).file_path).parent
|
||||
shutil.rmtree(entry_dir)
|
||||
stale_temp = tmp_path / ".tmp" / f"{cache_key}.stale-writer"
|
||||
stale_temp.mkdir(parents=True)
|
||||
(stale_temp / "partial").write_bytes(b"partial")
|
||||
|
||||
result = _build(_Pipeline(), tmp_path)
|
||||
|
||||
assert result.plan is not None
|
||||
assert not stale_temp.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", ["alias", "external_alias", "noncontiguous"])
|
||||
def test_unsupported_tensor_layouts_fail_closed(tmp_path, case):
|
||||
pipeline = _Pipeline()
|
||||
if case == "alias":
|
||||
pipeline.transformer.tied = pipeline.transformer.proj.weight
|
||||
expected_code = "unsupported_alias"
|
||||
elif case == "external_alias":
|
||||
pipeline.encoder = nn.Module()
|
||||
pipeline.encoder.shared = pipeline.transformer.proj.weight
|
||||
expected_code = "unsupported_alias"
|
||||
else:
|
||||
pipeline.transformer.proj.weight = nn.Parameter(torch.arange(6, dtype=torch.float32).reshape(3, 2).t())
|
||||
expected_code = "unsupported_tensor"
|
||||
|
||||
result = _build(pipeline, tmp_path)
|
||||
|
||||
assert result.plan is None
|
||||
assert result.fallback_code == expected_code
|
||||
assert list(tmp_path.rglob("*.safetensors")) == []
|
||||
|
||||
|
||||
def test_concurrent_processes_publish_one_immutable_entry(tmp_path):
|
||||
context = mp.get_context("spawn")
|
||||
queue = context.Queue()
|
||||
processes = [context.Process(target=_multiprocess_builder, args=(str(tmp_path), queue)) for _ in range(2)]
|
||||
for process in processes:
|
||||
process.start()
|
||||
results = [queue.get(timeout=30) for _ in processes]
|
||||
for process in processes:
|
||||
process.join(timeout=30)
|
||||
|
||||
assert all(process.exitcode == 0 for process in processes)
|
||||
assert all(code is None for code, _, _ in results)
|
||||
assert len({key for _, key, _ in results}) == 1
|
||||
assert len({tuple(paths) for _, _, paths in results}) == 1
|
||||
assert len(list((tmp_path / "v1").iterdir())) == 1
|
||||
Reference in new issue
Block a user