Signed-off-by: lishunyang12 <lishunyang12@users.noreply.github.com> Signed-off-by: Armaan Amatya <armaanamatya2014@gmail.com> Co-authored-by: lishunyang12 <lishunyang12@users.noreply.github.com> Co-authored-by: Armaan Amatya <38102001+armaanamatya@users.noreply.github.com>
181 lines
7.9 KiB
Python
181 lines
7.9 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
"""Model-independent exact caching of small conditioning projections."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import weakref
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
import torch
|
|
from torch import nn
|
|
from torch.nn.modules import module as module_hooks
|
|
|
|
|
|
class ExactProjectionCache:
|
|
"""Bounded, exact memoization for deterministic conditioning projections.
|
|
|
|
Models call :meth:`prepare` once for an immutable conditioning tensor, then
|
|
:meth:`project` for each leaf projection using a stable, model-defined name.
|
|
The supplied computation must depend only on that tensor, the projection's
|
|
parameters/buffers and fixed preprocessing. Include all request conditioning
|
|
in the tensor; timestep alone is insufficient for prompt/guidance-dependent
|
|
projections. Outputs returned on cache hits must be treated as read-only.
|
|
|
|
The original projection computes misses, preserving its quantization, LoRA
|
|
and TP behavior. Parameter/buffer versions guard reuse, and TP ranks vote
|
|
before skipping collectives. Call :meth:`clear` for adapter/configuration
|
|
changes or model moves. Gradient-enabled and compiled execution bypass reuse.
|
|
|
|
Each cache belongs to one model instance executing forwards serially. It
|
|
retains a bounded subset across requests, so it is independent of the
|
|
request-scoped approximate cache backends. It does not offload weights or
|
|
suppress block prefetch. Models may supply validated precomputed results by
|
|
overriding :meth:`_lookup_precomputed`; artifact loading stays model-owned.
|
|
"""
|
|
|
|
def __init__(self, *, max_bytes: int = 256 * 1024**2) -> None:
|
|
if type(max_bytes) is not int or max_bytes < 0:
|
|
raise ValueError("Projection cache budget must be a nonnegative byte count")
|
|
self.max_bytes = max_bytes
|
|
self._entries: dict[tuple[str, str, tuple[Any, ...]], tuple[Any, torch.Tensor, int]] = {}
|
|
self._bytes = 0
|
|
self._input: weakref.ReferenceType[torch.Tensor] | None = None
|
|
self._key: str | None = None
|
|
self.hits = self.misses = 0
|
|
|
|
def clear(self) -> None:
|
|
self._entries.clear()
|
|
self._bytes = 0
|
|
self._input = None
|
|
self._key = None
|
|
|
|
def prepare(self, embedding: torch.Tensor) -> None:
|
|
self._input = None
|
|
self._key = None
|
|
if not self.max_bytes or torch.is_grad_enabled() or torch.compiler.is_compiling():
|
|
return
|
|
self._key = tensor_digest(embedding)
|
|
self._input = weakref.ref(embedding)
|
|
|
|
@staticmethod
|
|
def _numerical_settings(device_type: str) -> tuple[Any, ...]:
|
|
# A projection may enter its own autocast context after prepare().
|
|
return (
|
|
torch.get_float32_matmul_precision(),
|
|
torch.is_autocast_enabled(device_type),
|
|
torch.get_autocast_dtype(device_type),
|
|
torch.backends.cuda.matmul.allow_tf32,
|
|
torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction,
|
|
torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction,
|
|
)
|
|
|
|
@staticmethod
|
|
def _has_forward_hooks(module: nn.Module) -> bool:
|
|
# Match nn.Module's global hooks as well as hooks on wrapped linears.
|
|
return bool(
|
|
module_hooks._global_forward_pre_hooks
|
|
or module_hooks._global_forward_hooks
|
|
or any(child._forward_pre_hooks or child._forward_hooks for child in module.modules())
|
|
)
|
|
|
|
@staticmethod
|
|
def _signature(module: nn.Module) -> tuple[Any, ...]:
|
|
# A replaced or restored storage conservatively causes a miss. The
|
|
# containing block's offload hooks still run before this method.
|
|
tensors = [*module.named_parameters(), *module.named_buffers()]
|
|
# vLLM LoRA stores these tensors outside registered buffers. Its
|
|
# suspend/resume mask can change without changing the base weights.
|
|
for field in ("lora_a_stacked", "lora_b_stacked"):
|
|
tensors.extend((f"{field}.{i}", value) for i, value in enumerate(getattr(module, field, ())))
|
|
return (
|
|
id(module),
|
|
id(getattr(module, "quant_method", None)),
|
|
getattr(module, "_diffusion_lora_active_slices", None),
|
|
*(
|
|
(name, id(value), value.data_ptr(), value._version, tuple(value.shape), value.dtype, value.device)
|
|
for name, value in tensors
|
|
),
|
|
)
|
|
|
|
def project(
|
|
self,
|
|
name: str,
|
|
module: nn.Module,
|
|
embedding: torch.Tensor,
|
|
compute: Callable[[], torch.Tensor],
|
|
) -> torch.Tensor:
|
|
if (
|
|
torch.is_grad_enabled()
|
|
or torch.compiler.is_compiling()
|
|
or self._key is None
|
|
or self._input is None
|
|
or self._input() is not embedding
|
|
):
|
|
return compute()
|
|
key = self._key, name, self._numerical_settings(embedding.device.type)
|
|
signature: tuple[Any, ...] | None
|
|
try:
|
|
signature = self._signature(module)
|
|
if self._has_forward_hooks(module):
|
|
# A linear hook may mutate weights or transform the output;
|
|
# executing it only on misses would change its semantics.
|
|
signature = None
|
|
except RuntimeError:
|
|
# Inference tensors without version counters cannot establish that
|
|
# weights stayed unchanged. Still participate in the TP vote.
|
|
signature = None
|
|
cached = self._entries.get(key)
|
|
hit = signature is not None and cached is not None and cached[0] == signature
|
|
from vllm.distributed import get_tensor_model_parallel_world_size, get_tp_group
|
|
|
|
world_size = get_tensor_model_parallel_world_size()
|
|
if world_size > 1:
|
|
vote = torch.tensor([int(hit)], device=embedding.device, dtype=torch.int32)
|
|
hit = int(get_tp_group().all_reduce(vote).item()) == world_size
|
|
if hit:
|
|
assert cached is not None
|
|
self.hits += 1
|
|
return cached[1]
|
|
self.misses += 1
|
|
value = (
|
|
self._lookup_precomputed(name, embedding, signature) if world_size == 1 and signature is not None else None
|
|
)
|
|
if value is None:
|
|
value = compute()
|
|
if cached is not None:
|
|
self._bytes -= self._entries.pop(key)[2]
|
|
size = value.numel() * value.element_size()
|
|
if signature is not None and self._bytes + size <= self.max_bytes:
|
|
# Retain a reusable subset when a long denoising schedule exceeds
|
|
# the budget. LRU would evict every entry before the next request
|
|
# reaches it, producing zero hits for a repeated cyclic schedule.
|
|
# Own the cached storage even if a provider returns workspace views.
|
|
self._entries[key] = (signature, value.detach().clone(), size)
|
|
self._bytes += size
|
|
return value
|
|
|
|
def _lookup_precomputed(self, name: str, embedding: torch.Tensor, signature: Any) -> torch.Tensor | None:
|
|
"""Return a validated TP1 projection, or None to execute the original.
|
|
|
|
Called only on runtime-cache misses with a usable weight signature.
|
|
Implementations must also validate inputs, numerical settings, artifact
|
|
compatibility and any state not represented in the module signature.
|
|
"""
|
|
return None
|
|
|
|
|
|
def canonical_json(value: Any) -> str:
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
|
|
|
|
def tensor_digest(tensor: torch.Tensor) -> str:
|
|
"""Hash dtype, shape and actual bytes, including BF16, without widening."""
|
|
value = tensor.detach().cpu().contiguous()
|
|
digest = hashlib.sha256(canonical_json([str(value.dtype), list(value.shape)]).encode())
|
|
digest.update(memoryview(value.reshape(-1).view(torch.uint8).numpy()))
|
|
return digest.hexdigest()
|