Files
vllm-omni/vllm_omni/diffusion/model_loader/fp8_cache.py
T
2026-08-19 11:55:29 +08:00

350 lines
14 KiB
Python

# 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",
]