Files
enginex-ascend-910-vllm/vllm_ascend/distributed/weight_transfer/packed_tensor.py
Sun Ruoxi 7f8a1b1f7a init v0.23.0
Signed-off-by: Sun Ruoxi <sunruoxi@4paradigm.com>
2026-08-27 15:11:51 +08:00

326 lines
13 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Packed tensor utilities for efficient weight transfer."""
import math
from collections.abc import Callable, Iterator
from typing import Any
import torch
from torch.multiprocessing.reductions import reduce_tensor
# Default values for packed tensor configuration.
# These are imported by HCCLWeightTransferUpdateInfo and trainer_send_weights.
DEFAULT_PACKED_BUFFER_SIZE_BYTES = 1024 * 1024 * 1024 # 1GB
DEFAULT_PACKED_NUM_BUFFERS = 2
def packed_broadcast_producer(
iterator: Iterator[tuple[str, torch.Tensor]],
group: Any,
src: int,
post_iter_func: Callable[[tuple[str, torch.Tensor]], torch.Tensor],
buffer_size_bytes: int = DEFAULT_PACKED_BUFFER_SIZE_BYTES,
num_buffers: int = DEFAULT_PACKED_NUM_BUFFERS,
) -> None:
"""Broadcast tensors in a packed manner from trainer to workers.
Args:
iterator: Iterator of model parameters. Returns a tuple of (name, tensor)
group: Process group (PyHcclCommunicator)
src: Source rank (0 in current implementation)
post_iter_func: Function to apply to each (name, tensor) pair before
packing, should return a tensor
buffer_size_bytes: Size in bytes for each packed tensor buffer.
Both producer and consumer must use the same value.
num_buffers: Number of buffers for double/triple buffering.
Both producer and consumer must use the same value.
"""
target_packed_tensor_size = buffer_size_bytes
streams = [torch.npu.Stream() for _ in range(num_buffers)]
buffer_idx = 0
packing_tensor_list: list[list[torch.Tensor]] = [[] for _ in range(num_buffers)]
packing_tensor_sizes: list[int] = [0 for _ in range(num_buffers)]
packed_tensors: list[torch.Tensor] = [torch.empty(0, dtype=torch.uint8, device="npu") for _ in range(num_buffers)]
done = False
while not done:
# Synchronize the current stream (waits for previous
# iteration's work on this buffer to finish)
streams[buffer_idx].synchronize()
# Start tasks for the new buffer in a new stream
with torch.npu.stream(streams[buffer_idx]):
# Initialize the packing tensor list and sizes
packing_tensor_list[buffer_idx] = []
packing_tensor_sizes[buffer_idx] = 0
# Pack the tensors
while True:
try:
item = next(iterator)
except StopIteration:
done = True
break
# Apply post processing and convert to linearized uint8 tensor
tensor = post_iter_func(item).contiguous().view(torch.uint8).view(-1)
packing_tensor_list[buffer_idx].append(tensor)
packing_tensor_sizes[buffer_idx] += tensor.numel()
if packing_tensor_sizes[buffer_idx] > target_packed_tensor_size:
break
if len(packing_tensor_list[buffer_idx]) > 0:
# Pack the tensors
packed_tensors[buffer_idx] = torch.cat(packing_tensor_list[buffer_idx], dim=0)
if len(packing_tensor_list[buffer_idx]) == 0:
# No more tensors — nothing left to broadcast
break
# torch.cat runs on the custom stream. Synchronize before
# broadcasting on the default stream so the packed data is ready.
streams[buffer_idx].synchronize()
group.broadcast(packed_tensors[buffer_idx], src=src)
# Move to the next buffer
buffer_idx = (buffer_idx + 1) % num_buffers
# Ensure the last broadcast on the default stream has completed
# before returning, so NPU tensor cleanup at exit doesn't hang.
torch.npu.current_stream().synchronize()
def packed_broadcast_consumer(
iterator: Iterator[tuple[str, tuple[list[int], torch.dtype]]],
group: Any,
src: int,
post_unpack_func: Callable[[list[tuple[str, torch.Tensor]]], None],
buffer_size_bytes: int = DEFAULT_PACKED_BUFFER_SIZE_BYTES,
num_buffers: int = DEFAULT_PACKED_NUM_BUFFERS,
) -> None:
"""Consume packed tensors and unpack them into a list of tensors.
Args:
iterator: Iterator of parameter metadata. Returns (name, (shape, dtype))
group: Process group (PyHcclCommunicator)
src: Source rank (0 in current implementation)
post_unpack_func: Function to apply to each list of (name, tensor) after
unpacking
buffer_size_bytes: Size in bytes for each packed tensor buffer.
Both producer and consumer must use the same value.
num_buffers: Number of buffers for double/triple buffering.
Both producer and consumer must use the same value.
"""
def unpack_tensor(
packed_tensor: torch.Tensor,
names: list[str],
shapes: list[list[int]],
dtypes: list[torch.dtype],
tensor_sizes: list[int],
) -> list[tuple[str, torch.Tensor]]:
"""Unpack a packed uint8 tensor into a list of typed tensors."""
unpacked_tensors = packed_tensor.split(tensor_sizes)
unpacked_list = [
(name, tensor.contiguous().view(dtype).view(*shape))
for name, shape, dtype, tensor in zip(names, shapes, dtypes, unpacked_tensors)
]
return unpacked_list
target_packed_tensor_size = buffer_size_bytes
streams = [torch.npu.Stream() for _ in range(num_buffers)]
default_stream = torch.npu.current_stream()
buffer_idx = 0
packing_tensor_meta_data: list[list[tuple[str, list[int], torch.dtype, int]]] = [[] for _ in range(num_buffers)]
packing_tensor_sizes: list[int] = [0 for _ in range(num_buffers)]
packed_tensors: list[torch.Tensor] = [torch.empty(0, dtype=torch.uint8, device="npu") for _ in range(num_buffers)]
done = False
while not done:
# Synchronize the current stream (waits for previous
# iteration's load_weights on this buffer to finish)
streams[buffer_idx].synchronize()
with torch.npu.stream(streams[buffer_idx]):
# Collect parameter metadata for this buffer
packing_tensor_meta_data[buffer_idx] = []
packing_tensor_sizes[buffer_idx] = 0
while True:
try:
name, (shape, dtype) = next(iterator)
except StopIteration:
done = True
break
tensor_size = math.prod(shape) * dtype.itemsize
packing_tensor_meta_data[buffer_idx].append((name, shape, dtype, tensor_size))
packing_tensor_sizes[buffer_idx] += tensor_size
if packing_tensor_sizes[buffer_idx] > target_packed_tensor_size:
break
if len(packing_tensor_meta_data[buffer_idx]) > 0:
packed_tensors[buffer_idx] = torch.empty(
packing_tensor_sizes[buffer_idx],
dtype=torch.uint8,
device="npu",
)
if len(packing_tensor_meta_data[buffer_idx]) == 0:
break
# Broadcast on the default stream.
group.broadcast(packed_tensors[buffer_idx], src=src)
# Synchronize the default stream so broadcast completes before
# load_weights (running on the custom stream) reads the data.
default_stream.synchronize()
# Unpack and load weights on the custom stream
with torch.npu.stream(streams[buffer_idx]):
names, shapes, dtypes, tensor_sizes = zip(*packing_tensor_meta_data[buffer_idx])
post_unpack_func(
unpack_tensor(
packed_tensors[buffer_idx],
list(names),
list(shapes),
list(dtypes),
list(tensor_sizes),
)
)
# Move to the next buffer
buffer_idx = (buffer_idx + 1) % num_buffers
# Wait for all in-flight load_weights (on custom streams) to finish.
# Otherwise NPU tensor cleanup at exit may hang.
for s in streams:
s.synchronize()
# ── NPU IPC packed transfer ────────────────────────────────────────────
def packed_npu_ipc_producer(
iterator: Iterator[tuple[str, torch.Tensor]],
npu_uuid: str,
post_iter_func: Callable[[tuple[str, torch.Tensor]], torch.Tensor],
buffer_size_bytes: int = DEFAULT_PACKED_BUFFER_SIZE_BYTES,
) -> Iterator[dict[str, Any]]:
"""Pack tensors into a reusable NPU IPC buffer and yield chunks.
Allocates a single NPU buffer of ``buffer_size_bytes`` and registers
it for IPC once via ``reduce_tensor``. Each chunk's packed data is
copied into this buffer before yielding, so only one IPC-shared
allocation exists for the lifetime of the transfer.
Args:
iterator: Iterator of (name, tensor) pairs.
npu_uuid: Physical NPU UUID string for this rank.
post_iter_func: Applied to each (name, tensor) before packing.
buffer_size_bytes: Exact capacity of the reusable IPC buffer.
"""
ipc_buffer = torch.empty(buffer_size_bytes, dtype=torch.uint8, device="npu")
# Store only the rebuild args (drop the func); the consumer rebuilds with
# the well-known ``rebuild_npu_tensor``, mirroring upstream's CUDA IPC engine.
_, ipc_args = reduce_tensor(ipc_buffer)
names: list[str] = []
shapes: list[list[int]] = []
dtypes: list[torch.dtype] = []
tensor_sizes: list[int] = []
total_bytes = 0
for name, orig_tensor in iterator:
flat = post_iter_func((name, orig_tensor)).contiguous().view(torch.uint8).view(-1)
if flat.numel() > buffer_size_bytes:
raise ValueError(
f"Tensor '{name}' has size {flat.numel()} bytes, "
f"which exceeds buffer_size_bytes={buffer_size_bytes}. "
f"Increase buffer_size_bytes to at least {flat.numel()}."
)
if total_bytes and total_bytes + flat.numel() > buffer_size_bytes:
torch.npu.current_stream().synchronize()
yield {
"names": names,
"shapes": shapes,
"dtype_names": [str(d).split(".")[-1] for d in dtypes],
"tensor_sizes": tensor_sizes,
"ipc_handle": {npu_uuid: ipc_args},
}
names, shapes, dtypes, tensor_sizes = [], [], [], []
total_bytes = 0
ipc_buffer[total_bytes : total_bytes + flat.numel()].copy_(flat)
names.append(name)
shapes.append(list(orig_tensor.shape))
dtypes.append(orig_tensor.dtype)
tensor_sizes.append(flat.numel())
total_bytes += flat.numel()
if total_bytes:
torch.npu.current_stream().synchronize()
yield {
"names": names,
"shapes": shapes,
"dtype_names": [str(d).split(".")[-1] for d in dtypes],
"tensor_sizes": tensor_sizes,
"ipc_handle": {npu_uuid: ipc_args},
}
def packed_npu_ipc_consumer(
ipc_handle: dict[str, tuple],
physical_npu_id: str,
names: list[str],
shapes: list[list[int]],
dtype_names: list[str],
tensor_sizes: list[int],
device_index: int,
) -> list[tuple[str, torch.Tensor]]:
"""Unpack a single packed IPC chunk into named tensors.
Reconstructs the packed buffer via the IPC handle, unpacks into
individual tensors, and clones each into independent storage before
returning. The clone is required because the producer reuses one
IPC buffer across chunks.
Args:
ipc_handle: Mapping of NPU UUID to a ``rebuild_npu_tensor`` args tuple
from ``reduce_tensor``.
physical_npu_id: Physical NPU UUID string for the current process.
names: Parameter names in the packed buffer.
shapes: Parameter shapes.
dtype_names: Parameter dtype name strings (e.g. "float16").
tensor_sizes: Size in bytes of each parameter in the packed buffer.
device_index: Local NPU device index.
"""
# Lazy import: ``rebuild_npu_tensor`` lives in ``torch_npu`` and must not be
# imported at module load time on non-NPU hosts.
from torch_npu.multiprocessing.reductions import rebuild_npu_tensor
if physical_npu_id not in ipc_handle:
raise ValueError(
f"IPC handle not found for NPU UUID {physical_npu_id}. Available UUIDs: {list(ipc_handle.keys())}"
)
args = ipc_handle[physical_npu_id]
list_args = list(args)
# Index 6 of the args from reduce_tensor is the device_index.
# Overwrite it with the receiver's device index.
list_args[6] = device_index
packed = rebuild_npu_tensor(*list_args)
content_size = sum(tensor_sizes)
packed = packed[:content_size]
dtypes = [getattr(torch, dn) for dn in dtype_names]
weights: list[tuple[str, torch.Tensor]] = []
offset = 0
for name, shape, dtype, size in zip(names, shapes, dtypes, tensor_sizes):
raw = packed[offset : offset + size]
tensor = raw.contiguous().view(dtype).view(*shape).clone()
weights.append((name, tensor))
offset += size
return weights