2026-07-30 21:35:38 +08:00
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
|
"""Shared tensor utilities for distributed layerwise offload.
|
|
|
|
|
|
|
|
|
|
These helpers are used by both DistributedLayerwiseOffloadHook and
|
|
|
|
|
DistributedLayerwiseOffloadBackend, and can be reused by other
|
|
|
|
|
offload backends.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-09-06 05:40:09 +08:00
|
|
|
from collections.abc import Callable, Iterable, Mapping
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from itertools import chain
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
2026-07-30 21:35:38 +08:00
|
|
|
import torch
|
2026-09-06 05:40:09 +08:00
|
|
|
from torch import nn
|
2026-07-30 21:35:38 +08:00
|
|
|
from torch.distributed.tensor import DTensor
|
|
|
|
|
|
|
|
|
|
|
2026-09-06 05:40:09 +08:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class TensorStorageSpec:
|
|
|
|
|
"""One tensor's target and physical-storage layout."""
|
|
|
|
|
|
|
|
|
|
name: str
|
|
|
|
|
target: torch.Tensor
|
|
|
|
|
value: torch.Tensor
|
|
|
|
|
storage_numel: int
|
|
|
|
|
stride: tuple[int, ...]
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 21:35:38 +08:00
|
|
|
def dtype_size(dtype: torch.dtype) -> int:
|
|
|
|
|
"""Return element size in bytes for a torch.dtype."""
|
|
|
|
|
return torch.empty(1, dtype=dtype).element_size()
|
|
|
|
|
|
|
|
|
|
|
2026-09-06 05:40:09 +08:00
|
|
|
def group_named_tensors_by_dtype(
|
|
|
|
|
params: Mapping[str, torch.Tensor],
|
|
|
|
|
buffers: Mapping[str, torch.Tensor],
|
|
|
|
|
) -> dict[torch.dtype, list[tuple[str, torch.Tensor]]]:
|
|
|
|
|
"""Group parameters and buffers by dtype while preserving their order."""
|
|
|
|
|
grouped: dict[torch.dtype, list[tuple[str, torch.Tensor]]] = {}
|
|
|
|
|
for name, tensor in chain(params.items(), buffers.items()):
|
|
|
|
|
grouped.setdefault(tensor.dtype, []).append((name, tensor))
|
|
|
|
|
return grouped
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def physical_storage_numel(tensor: torch.Tensor) -> int:
|
|
|
|
|
"""Return the storage span needed to preserve ``tensor``'s stride."""
|
|
|
|
|
if tensor.numel() == 0:
|
|
|
|
|
return 0
|
|
|
|
|
return 1 + sum((size - 1) * stride for size, stride in zip(tensor.shape, tensor.stride()))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def flatten_physical_storage(
|
|
|
|
|
tensor: torch.Tensor,
|
|
|
|
|
storage_numel: int | None = None,
|
|
|
|
|
) -> torch.Tensor:
|
|
|
|
|
"""Pack a possibly strided tensor in physical storage order."""
|
|
|
|
|
if tensor.is_contiguous():
|
|
|
|
|
return tensor.flatten()
|
|
|
|
|
storage_numel = physical_storage_numel(tensor) if storage_numel is None else storage_numel
|
|
|
|
|
storage = torch.zeros(storage_numel, dtype=tensor.dtype, device=tensor.device)
|
|
|
|
|
torch.as_strided(storage, size=tensor.shape, stride=tensor.stride()).copy_(tensor)
|
|
|
|
|
return storage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def describe_tensor_storage(
|
|
|
|
|
named_tensors: Iterable[tuple[str, torch.Tensor]],
|
|
|
|
|
transforms: Mapping[int, Callable[[torch.Tensor], torch.Tensor]] | None = None,
|
|
|
|
|
) -> list[TensorStorageSpec]:
|
|
|
|
|
"""Resolve local tensors and their transport-preserving layouts."""
|
|
|
|
|
specs: list[TensorStorageSpec] = []
|
|
|
|
|
for name, target in named_tensors:
|
|
|
|
|
value = target.to_local() if hasattr(target, "to_local") else target
|
|
|
|
|
transform = (transforms or {}).get(id(target))
|
|
|
|
|
if callable(transform):
|
|
|
|
|
value = transform(value)
|
|
|
|
|
specs.append(
|
|
|
|
|
TensorStorageSpec(
|
|
|
|
|
name=name,
|
|
|
|
|
target=target,
|
|
|
|
|
value=value,
|
|
|
|
|
storage_numel=physical_storage_numel(value),
|
|
|
|
|
stride=value.stride(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return specs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tensor_storage_metadata(spec: TensorStorageSpec, offset: int, *, include_device: bool = False) -> dict[str, Any]:
|
|
|
|
|
"""Build reconstruction metadata shared by offload transports."""
|
|
|
|
|
metadata: dict[str, Any] = {
|
|
|
|
|
"name": spec.name,
|
|
|
|
|
"offset": offset,
|
|
|
|
|
"numel": spec.storage_numel,
|
|
|
|
|
"shape": spec.value.shape,
|
|
|
|
|
"stride": spec.stride,
|
|
|
|
|
}
|
|
|
|
|
if include_device:
|
|
|
|
|
metadata["device"] = spec.value.device
|
|
|
|
|
return metadata
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 21:35:38 +08:00
|
|
|
def is_dtensor(t: torch.Tensor) -> bool:
|
|
|
|
|
"""Check if tensor is a DTensor."""
|
|
|
|
|
return isinstance(t, DTensor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_tensor_storage(target: torch.Tensor, value: torch.Tensor) -> None:
|
|
|
|
|
"""Replace target's underlying storage with value (zero-copy)."""
|
|
|
|
|
if is_dtensor(target):
|
|
|
|
|
target._local_tensor = value
|
|
|
|
|
else:
|
|
|
|
|
target.data = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_offload_placeholder(tensor: torch.Tensor) -> torch.Tensor:
|
|
|
|
|
"""Create a zero-element placeholder to free GPU memory."""
|
|
|
|
|
if is_dtensor(tensor):
|
|
|
|
|
local_shape = tuple(tensor.to_local().shape)
|
|
|
|
|
return torch.empty(local_shape, device="meta", dtype=tensor.dtype)
|
|
|
|
|
return torch.empty((0,), device=tensor.device, dtype=tensor.dtype)
|
|
|
|
|
|
|
|
|
|
|
2026-09-06 05:40:09 +08:00
|
|
|
def clear_tensor_storage(tensors: Iterable[torch.Tensor]) -> None:
|
|
|
|
|
"""Atomically replace tensor storage with offload placeholders."""
|
|
|
|
|
targets = list(tensors)
|
|
|
|
|
originals = [tensor.to_local() if is_dtensor(tensor) else tensor.data for tensor in targets]
|
|
|
|
|
placeholders = [make_offload_placeholder(tensor) for tensor in targets]
|
|
|
|
|
committed = 0
|
|
|
|
|
try:
|
|
|
|
|
for target, placeholder in zip(targets, placeholders, strict=True):
|
|
|
|
|
set_tensor_storage(target, placeholder)
|
|
|
|
|
committed += 1
|
|
|
|
|
except BaseException:
|
|
|
|
|
rollback_error: BaseException | None = None
|
|
|
|
|
for target, original in zip(targets[:committed], originals[:committed], strict=True):
|
|
|
|
|
try:
|
|
|
|
|
set_tensor_storage(target, original)
|
|
|
|
|
except BaseException as exc:
|
|
|
|
|
rollback_error = rollback_error or exc
|
|
|
|
|
if rollback_error is not None:
|
|
|
|
|
raise RuntimeError("Tensor storage clear failed and rollback was incomplete") from rollback_error
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_block_storage(
|
|
|
|
|
params: Mapping[str, torch.Tensor],
|
|
|
|
|
buffers: Mapping[str, torch.Tensor],
|
|
|
|
|
ready_event: Any | None,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Wait for an outstanding transfer, then release one block's residency."""
|
|
|
|
|
if ready_event is not None:
|
|
|
|
|
from vllm_omni.platforms import current_omni_platform
|
|
|
|
|
|
|
|
|
|
current_omni_platform.current_stream().wait_event(ready_event)
|
|
|
|
|
clear_tensor_storage(chain(params.values(), buffers.values()))
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 21:35:38 +08:00
|
|
|
def is_materialized_tensor(t: torch.Tensor) -> bool:
|
|
|
|
|
"""Check if tensor holds real data (not meta or empty placeholder)."""
|
|
|
|
|
if is_dtensor(t):
|
|
|
|
|
local_t = t.to_local()
|
|
|
|
|
return not local_t.is_meta
|
|
|
|
|
return not t.is_meta and t.data.numel() > 0
|
2026-09-06 05:40:09 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def materialization_probe(
|
|
|
|
|
params: Mapping[str, torch.Tensor],
|
|
|
|
|
buffers: Mapping[str, torch.Tensor],
|
|
|
|
|
) -> torch.Tensor | None:
|
|
|
|
|
"""Pick one non-empty tensor that tracks a block's atomic storage state."""
|
|
|
|
|
return next((tensor for tensor in chain(params.values(), buffers.values()) if tensor.numel()), None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def module_materialization_probe(module: nn.Module) -> torch.Tensor | None:
|
|
|
|
|
"""Capture a probe before another hook releases the module's storage."""
|
|
|
|
|
return materialization_probe(dict(module.named_parameters()), dict(module.named_buffers()))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def restore_tensor_storage(
|
|
|
|
|
target: torch.Tensor,
|
|
|
|
|
value: torch.Tensor,
|
|
|
|
|
*,
|
|
|
|
|
device: torch.device | str,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Detach ``target`` from an offloader-owned backing with a real copy."""
|
|
|
|
|
restored = torch.empty_strided(
|
|
|
|
|
value.shape,
|
|
|
|
|
value.stride(),
|
|
|
|
|
dtype=value.dtype,
|
|
|
|
|
device=device,
|
|
|
|
|
)
|
|
|
|
|
restored.copy_(value)
|
|
|
|
|
set_tensor_storage(target, restored)
|