Files
vllm-project--vllm/vllm/v1/worker/gpu/buffer_utils.py
T
wehub-resource-sync 7ce4c8e27e
pre-commit / pre-run-check (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:55:37 +08:00

317 lines
10 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections.abc import Iterable, Sequence
from functools import partial
import numpy as np
import torch
from vllm.triton_utils import tl, triton
from vllm.utils.platform_utils import is_uva_available
from vllm.utils.torch_utils import (
async_tensor_h2d,
get_accelerator_view_from_cpu_tensor,
)
# Default round-robin depth for the UVA buffer pools. Must be >= the number of
# concurrent in-flight steps (engine batch_queue_size).
_DEFAULT_MAX_CONCURRENCY = 2
def set_default_max_concurrency(n: int) -> None:
global _DEFAULT_MAX_CONCURRENCY
_DEFAULT_MAX_CONCURRENCY = max(2, n)
def async_copy_to_gpu(
x: torch.Tensor | np.ndarray,
out: torch.Tensor | None = None,
device: torch.device | None = None,
) -> torch.Tensor:
if isinstance(x, np.ndarray):
x = torch.from_numpy(x)
assert x.is_cpu
if out is None:
assert device is not None
out = torch.empty_like(x, device=device)
# pin_memory() is no-op if the memory is already pinned.
pinned = x.pin_memory()
return out.copy_(pinned, non_blocking=True)
class UvaBuffer:
def __init__(self, size: int | Sequence[int], dtype: torch.dtype):
if not is_uva_available():
raise RuntimeError("UVA is not available")
self.cpu = torch.zeros(size, dtype=dtype, device="cpu", pin_memory=True)
self.np = self.cpu.numpy()
self.uva = get_accelerator_view_from_cpu_tensor(self.cpu)
class UvaBufferPool:
def __init__(
self,
size: int | Sequence[int],
dtype: torch.dtype,
max_concurrency: int | None = None,
):
if max_concurrency is None:
max_concurrency = _DEFAULT_MAX_CONCURRENCY
self.size = size
self.dtype = dtype
self.max_concurrency = max_concurrency
# UVA buffers for concurrency
self._uva_bufs = [UvaBuffer(size, dtype) for _ in range(max_concurrency)]
# Current buffer index
self._curr = 0
def copy_to_uva(self, x: torch.Tensor | np.ndarray | list) -> torch.Tensor:
# Round robin to the next buffer.
self._curr = (self._curr + 1) % self.max_concurrency
buf = self._uva_bufs[self._curr]
# CPU-to-CPU copy
dst = buf.cpu if isinstance(x, torch.Tensor) else buf.np
n = len(x)
dst[:n] = x
return buf.uva[:n]
def copy_to_gpu(
self,
x: torch.Tensor | np.ndarray,
out: torch.Tensor | None = None,
) -> torch.Tensor:
uva = self.copy_to_uva(x)
# CPU-to-GPU copy
return uva.clone() if out is None else out.copy_(uva, non_blocking=True)
class UvaBackedTensor:
def __init__(
self,
size: int | Sequence[int],
dtype: torch.dtype,
max_concurrency: int | None = None,
):
self.dtype = dtype
# Source of truth
self.cpu = torch.zeros(size, dtype=dtype, device="cpu", pin_memory=False)
self.np = self.cpu.numpy()
# Buffers for concurrency
self.pool = UvaBufferPool(size, dtype, max_concurrency)
self.gpu = self.pool.copy_to_uva(self.np)
def copy_to_uva(self, n: int | None = None) -> torch.Tensor:
# CPU-to-CPU copy
self.gpu = self.pool.copy_to_uva(self.np[:n] if n is not None else self.np)
return self.gpu
class StagedWriteTensor:
def __init__(
self,
size: int | Sequence[int],
dtype: torch.dtype,
device: torch.device,
max_concurrency: int | None = None,
uva_instead_of_gpu: bool = False,
):
if max_concurrency is None:
max_concurrency = _DEFAULT_MAX_CONCURRENCY
supported_dtypes = [torch.int32, torch.int64, torch.float32]
if dtype not in supported_dtypes:
raise ValueError(
f"Unsupported dtype {dtype}: should be one of {supported_dtypes}"
)
self.num_rows = size if isinstance(size, int) else size[0]
self.dtype = dtype
self.device = device
self.max_concurrency = max_concurrency
if not uva_instead_of_gpu:
# Create a GPU tensor (default)
self.gpu = torch.zeros(size, dtype=dtype, device=device)
else:
# For a large but not-frequently-accessed tensor, we can use UVA instead of
# GPU to save GPU memory
self._uva_buf = UvaBuffer(size, dtype)
self.gpu = self._uva_buf.uva
self._staged_write_indices: list[int] = []
self._staged_write_starts: list[int] = []
self._staged_write_contents: list[int | float] = []
self._staged_write_cu_lens: list[int] = []
new_buffer = partial(UvaBufferPool, max_concurrency=max_concurrency)
self.write_indices = new_buffer(self.num_rows, dtype=torch.int32)
self.write_starts = new_buffer(self.num_rows, dtype=torch.int32)
self.write_cu_lens = new_buffer(self.num_rows, dtype=torch.int32)
def stage_write(
self, index: int, start: int, x: Iterable[int] | Iterable[float]
) -> None:
assert index >= 0
assert start >= 0
if not x:
return
self._staged_write_indices.append(index)
self._staged_write_starts.append(start)
self._staged_write_contents.extend(x)
self._staged_write_cu_lens.append(len(self._staged_write_contents))
def stage_write_elem(self, index: int, x: int) -> None:
assert index >= 0
self._staged_write_indices.append(index)
self._staged_write_starts.append(0)
self._staged_write_contents.append(x)
self._staged_write_cu_lens.append(len(self._staged_write_contents))
def apply_write(self) -> None:
n = len(self._staged_write_indices)
if n == 0:
return
indices_uva = self.write_indices.copy_to_uva(self._staged_write_indices)
starts_uva = self.write_starts.copy_to_uva(self._staged_write_starts)
cu_lens_uva = self.write_cu_lens.copy_to_uva(self._staged_write_cu_lens)
# Special handling for write_contents
write_contents = async_tensor_h2d(
self._staged_write_contents, device=self.device, dtype=self.dtype
)
# Write diffs to the GPU buffer
_apply_write_kernel[(n,)](
self.gpu,
self.gpu.stride(0),
indices_uva,
starts_uva,
write_contents,
cu_lens_uva,
None,
BLOCK_SIZE=1024,
MULTI_GROUP=False,
)
# Clear the staged writes
self.clear_staged_writes()
def clear_staged_writes(self) -> None:
self._staged_write_indices.clear()
self._staged_write_starts.clear()
self._staged_write_contents.clear()
self._staged_write_cu_lens.clear()
class FusedStagedWriter:
"""Applies the staged writes of several `StagedWriteTensor`s at once."""
def __init__(
self, device: torch.device, max_writes: int, max_concurrency: int | None = None
):
new_pool = partial(
UvaBufferPool, dtype=torch.int32, max_concurrency=max_concurrency
)
self.group_ids = new_pool(max_writes)
self.indices = new_pool(max_writes)
self.starts = new_pool(max_writes)
self.cu_lens = new_pool(max_writes)
self.device = device
def apply(
self,
tensors: Sequence[StagedWriteTensor],
output_ptrs: torch.Tensor,
output_strides: torch.Tensor,
) -> None:
"""Apply and clear the staged writes of `tensors` with one kernel."""
group_ids: list[int] = []
indices: list[int] = []
starts: list[int] = []
contents: list[int | float] = []
cu_lens: list[int] = []
for group_id, t in enumerate(tensors):
n = len(t._staged_write_indices)
if n == 0:
continue
group_ids.extend([group_id] * n)
indices.extend(t._staged_write_indices)
starts.extend(t._staged_write_starts)
content_base = len(contents)
contents.extend(t._staged_write_contents)
cu_lens.extend(content_base + cu_len for cu_len in t._staged_write_cu_lens)
if not group_ids:
return
group_ids_uva = self.group_ids.copy_to_uva(group_ids)
indices_uva = self.indices.copy_to_uva(indices)
starts_uva = self.starts.copy_to_uva(starts)
cu_lens_uva = self.cu_lens.copy_to_uva(cu_lens)
contents_gpu = async_tensor_h2d(contents, device=self.device, dtype=torch.int32)
_apply_write_kernel[(len(group_ids),)](
output_ptrs,
output_strides,
indices_uva,
starts_uva,
contents_gpu,
cu_lens_uva,
group_ids_uva,
BLOCK_SIZE=1024,
MULTI_GROUP=True,
)
for t in tensors:
t.clear_staged_writes()
@triton.jit
def _apply_write_kernel(
output_ptr, # MULTI_GROUP: ptr-to-ptrs [num_groups]; else: data ptr
output_stride, # MULTI_GROUP: ptr-to-strides [num_groups]; else: row stride
write_indices_ptr,
write_starts_ptr,
write_contents_ptr,
write_cu_lens_ptr,
write_group_ids_ptr, # [num_writes], used only when MULTI_GROUP
BLOCK_SIZE: tl.constexpr,
MULTI_GROUP: tl.constexpr,
):
pid = tl.program_id(0)
row_idx = tl.load(write_indices_ptr + pid)
start_idx = tl.load(write_starts_ptr + pid)
cu_start = tl.load(write_cu_lens_ptr + pid - 1) if pid > 0 else 0
cu_end = tl.load(write_cu_lens_ptr + pid)
content_len = cu_end - cu_start
if MULTI_GROUP:
# Each write targets a different output tensor (KV cache group);
# resolve its base pointer and row stride per write.
group_id = tl.load(write_group_ids_ptr + pid)
row_ptr = _load_ptr(output_ptr + group_id, tl.int32)
row_stride = tl.load(output_stride + group_id)
else:
row_ptr = output_ptr
row_stride = output_stride
row_ptr += row_idx * row_stride + start_idx
for i in range(0, content_len, BLOCK_SIZE):
block = i + tl.arange(0, BLOCK_SIZE)
mask = block < content_len
content = tl.load(write_contents_ptr + cu_start + block, mask=mask)
tl.store(row_ptr + block, content, mask=mask)
@triton.jit
def _load_ptr(ptr_to_ptr, elem_dtype):
ptr = tl.load(ptr_to_ptr)
ptr = tl.cast(ptr, tl.pointer_type(elem_dtype))
return tl.multiple_of(ptr, 16)