[VLM] perf: optimize CUDA IPC for multimodal transfer by caching IPC pool handles (#21418)

This commit is contained in:
saatwiknagpal
2026-03-30 00:20:38 +08:00
committed by GitHub
parent 5bb9ca0e63
commit d2440dcf58
3 changed files with 195 additions and 50 deletions
+1
View File
@@ -451,6 +451,7 @@ class Envs:
# VLM Item CUDA IPC Transport # VLM Item CUDA IPC Transport
SGLANG_USE_CUDA_IPC_TRANSPORT = EnvBool(False) SGLANG_USE_CUDA_IPC_TRANSPORT = EnvBool(False)
SGLANG_USE_IPC_POOL_HANDLE_CACHE = EnvBool(False)
SGLANG_MM_FEATURE_CACHE_MB = EnvInt(4 * 1024) SGLANG_MM_FEATURE_CACHE_MB = EnvInt(4 * 1024)
SGLANG_MM_ITEM_MEM_POOL_RECYCLE_INTERVAL_SEC = EnvFloat(0.05) SGLANG_MM_ITEM_MEM_POOL_RECYCLE_INTERVAL_SEC = EnvFloat(0.05)
@@ -40,6 +40,7 @@ _is_npu = is_npu()
_is_xpu = is_xpu() _is_xpu = is_xpu()
SGL_USE_CUDA_IPC = envs.SGLANG_USE_CUDA_IPC_TRANSPORT.get() SGL_USE_CUDA_IPC = envs.SGLANG_USE_CUDA_IPC_TRANSPORT.get()
_IPC_POOL_HANDLE_CACHE = envs.SGLANG_USE_IPC_POOL_HANDLE_CACHE.get()
@dataclasses.dataclass @dataclasses.dataclass
@@ -1150,7 +1151,7 @@ class BaseMultimodalProcessor(ABC):
# post-process # post-process
for item in all_collected_items: for item in all_collected_items:
if isinstance(item.feature, torch.Tensor) and item.feature.is_cuda: if isinstance(item.feature, torch.Tensor) and item.feature.is_cuda:
sync_flag, available_slice = ( sync_flag, available_slice, byte_offset = (
self.cudaipc_mmfeature_pool.return_a_slice_tensor_with_flag( self.cudaipc_mmfeature_pool.return_a_slice_tensor_with_flag(
item.feature item.feature
) )
@@ -1163,6 +1164,13 @@ class BaseMultimodalProcessor(ABC):
data=available_slice, data=available_slice,
info_data=item.feature, info_data=item.feature,
sync_buffer_meta=sync_flag, sync_buffer_meta=sync_flag,
pool_ipc_handle=(
self.cudaipc_mmfeature_pool._pool_ipc_handle
if _IPC_POOL_HANDLE_CACHE
else None
),
pool_byte_offset=byte_offset,
pool_device_index=self.cudaipc_mmfeature_pool._pool_device_index,
) )
elif not self.server_args.keep_mm_feature_on_device: elif not self.server_args.keep_mm_feature_on_device:
item.feature = item.feature.cpu() item.feature = item.feature.cpu()
@@ -1171,7 +1179,7 @@ class BaseMultimodalProcessor(ABC):
and item.precomputed_embeddings.is_cuda and item.precomputed_embeddings.is_cuda
): ):
sync_flag, available_slice = ( sync_flag, available_slice, byte_offset = (
self.cudaipc_mmfeature_pool.return_a_slice_tensor_with_flag( self.cudaipc_mmfeature_pool.return_a_slice_tensor_with_flag(
item.precomputed_embeddings item.precomputed_embeddings
) )
@@ -1185,6 +1193,13 @@ class BaseMultimodalProcessor(ABC):
data=available_slice, data=available_slice,
info_data=item.precomputed_embeddings, info_data=item.precomputed_embeddings,
sync_buffer_meta=sync_flag, sync_buffer_meta=sync_flag,
pool_ipc_handle=(
self.cudaipc_mmfeature_pool._pool_ipc_handle
if _IPC_POOL_HANDLE_CACHE
else None
),
pool_byte_offset=byte_offset,
pool_device_index=self.cudaipc_mmfeature_pool._pool_device_index,
) )
elif not self.server_args.keep_mm_feature_on_device: elif not self.server_args.keep_mm_feature_on_device:
item.precomputed_embeddings = item.precomputed_embeddings.cpu() item.precomputed_embeddings = item.precomputed_embeddings.cpu()
@@ -3,7 +3,7 @@ import logging
import threading import threading
import time import time
from multiprocessing import shared_memory from multiprocessing import shared_memory
from typing import Tuple from typing import Any, Tuple
import numpy as np import numpy as np
import torch import torch
@@ -22,6 +22,49 @@ MM_ITEM_MEMORY_POOL_RECYCLE_INTERVAL = (
SHM_LOCK_FILE = "/tmp/shm_wr_lock.lock" SHM_LOCK_FILE = "/tmp/shm_wr_lock.lock"
# Cache for pool-level IPC handles on the consumer side.
# Key: the pool CUDA IPC handle tuple. Value: opened UntypedStorage.
_pool_storage_cache: dict = {}
_pool_cache_lock = threading.Lock()
def _normalize_pool_cache_key(pool_handle, pool_device_index: int) -> tuple[Any, ...]:
normalized_handle = (
pool_handle if isinstance(pool_handle, tuple) else tuple(pool_handle)
)
return (pool_device_index, normalized_handle)
def _open_pooled_storage_uncached(pool_handle):
return torch.UntypedStorage._new_shared_cuda(*pool_handle)
def _pool_handle_cache_get_or_open(cache_key, pool_handle):
storage = _pool_storage_cache.get(cache_key)
if storage is None:
with _pool_cache_lock:
storage = _pool_storage_cache.get(cache_key)
if storage is None:
storage = _open_pooled_storage_uncached(pool_handle)
_pool_storage_cache[cache_key] = storage
return storage
def _pool_handle_cache_set(cache_key, storage):
with _pool_cache_lock:
_pool_storage_cache[cache_key] = storage
def _pool_handle_cache_invalidate(cache_key):
with _pool_cache_lock:
_pool_storage_cache.pop(cache_key, None)
def _pool_handle_cache_clear():
with _pool_cache_lock:
_pool_storage_cache.clear()
class ShmSyncBuffer: class ShmSyncBuffer:
def __init__(self, byte_size: int = 4): def __init__(self, byte_size: int = 4):
self.buffer = shared_memory.SharedMemory(create=True, size=byte_size) self.buffer = shared_memory.SharedMemory(create=True, size=byte_size)
@@ -80,6 +123,9 @@ class MmItemMemoryPool:
self.memory_pool = torch.empty( self.memory_pool = torch.empty(
memory_size, dtype=torch.int8, device="cuda" memory_size, dtype=torch.int8, device="cuda"
).contiguous() ).contiguous()
storage = self.memory_pool.untyped_storage()
self._pool_ipc_handle = storage._share_cuda_()
self._pool_device_index = self.memory_pool.device.index
self.sync_flag_list = [] self.sync_flag_list = []
@@ -181,8 +227,9 @@ class MmItemMemoryPool:
return ( return (
available_chunk.sync_flag.meta_data, available_chunk.sync_flag.meta_data,
self.memory_pool[available_chunk.start : available_chunk.end], self.memory_pool[available_chunk.start : available_chunk.end],
available_chunk.start,
) )
return None, None return None, None, None
def recycle_chunks(self): def recycle_chunks(self):
@@ -229,6 +276,9 @@ class CudaIpcTensorTransportProxy:
data: torch.Tensor, data: torch.Tensor,
info_data: torch.Tensor, info_data: torch.Tensor,
sync_buffer_meta, sync_buffer_meta,
pool_ipc_handle=None,
pool_byte_offset: int = 0,
pool_device_index: int = 0,
): ):
if (not isinstance(data, torch.Tensor)) or ( if (not isinstance(data, torch.Tensor)) or (
@@ -238,7 +288,24 @@ class CudaIpcTensorTransportProxy:
f"Input 'data' must be a torch.Tensor, but got {type(data)}" f"Input 'data' must be a torch.Tensor, but got {type(data)}"
) )
self.proxy_state = self.get_proxy_state(data, info_data) if pool_ipc_handle is not None:
self.proxy_state = {
"ipc_extra": {
"pool_handle": pool_ipc_handle,
"pool_byte_offset": pool_byte_offset,
"pool_device_index": pool_device_index,
"shape": data.shape,
"dtype": data.dtype,
"stride": data.stride(),
"storage_offset": 0,
"nbytes": data.numel() * data.element_size(),
"recons_shape": info_data.shape,
"recons_dtype": info_data.dtype,
},
"tensor_data": None,
}
else:
self.proxy_state = self.get_proxy_state(data, info_data)
self.reconstruct_tensor = None self.reconstruct_tensor = None
self.sync_data_meta = sync_buffer_meta self.sync_data_meta = sync_buffer_meta
self.sync_buffer = None self.sync_buffer = None
@@ -283,6 +350,62 @@ class CudaIpcTensorTransportProxy:
return state return state
def _reconstruct_from_ipc_extra(self, ipc_extra, *, use_cache: bool):
shape = ipc_extra["shape"]
dtype = ipc_extra["dtype"]
stride = ipc_extra["stride"]
target_device = torch.device(f"cuda:{ipc_extra['pool_device_index']}")
cache_key = _normalize_pool_cache_key(
ipc_extra["pool_handle"], ipc_extra["pool_device_index"]
)
with torch.cuda.device(target_device):
if use_cache:
storage = _pool_handle_cache_get_or_open(
cache_key, ipc_extra["pool_handle"]
)
storage_to_cache = None
else:
storage = _open_pooled_storage_uncached(ipc_extra["pool_handle"])
storage_to_cache = storage
slice_storage = storage[
ipc_extra["pool_byte_offset"] : ipc_extra["pool_byte_offset"]
+ ipc_extra["nbytes"]
]
slice_tensor = torch.empty(0, dtype=dtype, device=target_device).set_(
slice_storage,
storage_offset=ipc_extra["storage_offset"],
size=shape,
stride=stride,
)
return slice_tensor, target_device, cache_key, storage_to_cache
def _copy_slice_tensor_to_target(
self,
slice_tensor: torch.Tensor,
rebuild_device: torch.device,
recons_shape,
recons_dtype,
):
with torch.cuda.device(rebuild_device):
reconstructed_tensor = torch.empty(
recons_shape, dtype=recons_dtype, device=rebuild_device
).contiguous()
reconstructed_tensor.view(torch.int8).view(-1).copy_(slice_tensor)
open(SHM_LOCK_FILE, "a").close()
# write the shm_sync_buffer with a file lock
with open(SHM_LOCK_FILE, "w+") as f:
fcntl.flock(f, fcntl.LOCK_EX)
sync_flag = self.get_sync_flag
sync_flag += 1
fcntl.flock(f, fcntl.LOCK_UN)
self.close_shm()
return reconstructed_tensor
def reconstruct_on_target_device(self, rebuild_device_idx): def reconstruct_on_target_device(self, rebuild_device_idx):
rebuild_device = torch.device(f"cuda:{rebuild_device_idx}") rebuild_device = torch.device(f"cuda:{rebuild_device_idx}")
if ( if (
@@ -293,52 +416,58 @@ class CudaIpcTensorTransportProxy:
if self.proxy_state["ipc_extra"]: if self.proxy_state["ipc_extra"]:
ipc_extra = self.proxy_state["ipc_extra"] ipc_extra = self.proxy_state["ipc_extra"]
( recons_shape = ipc_extra["recons_shape"]
handle, recons_dtype = ipc_extra["recons_dtype"]
shape,
dtype, if "pool_handle" in ipc_extra:
stride, try:
source_device_index, (
s_offset, slice_tensor,
recons_shape, _target_device,
recons_dtype, cache_key,
) = ( storage_to_cache,
ipc_extra["handle"], ) = self._reconstruct_from_ipc_extra(ipc_extra, use_cache=True)
ipc_extra["shape"], except Exception as e:
ipc_extra["dtype"], cache_key = _normalize_pool_cache_key(
ipc_extra["stride"], ipc_extra["pool_handle"], ipc_extra["pool_device_index"]
ipc_extra["device_index"], )
ipc_extra["storage_offset"], logger.info(
ipc_extra["recons_shape"], "Failed to deserialize from cached pooled CUDA IPC handle (%s). "
ipc_extra["recons_dtype"], "Invalidating cache entry and retrying uncached.",
e,
)
_pool_handle_cache_invalidate(cache_key)
(
slice_tensor,
_target_device,
_cache_key,
storage_to_cache,
) = self._reconstruct_from_ipc_extra(ipc_extra, use_cache=False)
if storage_to_cache is not None:
_pool_handle_cache_set(cache_key, storage_to_cache)
else:
# Non-pooled path: open handle directly (original behavior)
try:
storage = torch.UntypedStorage._new_shared_cuda(
*ipc_extra["handle"]
)
target_device = torch.device(f"cuda:{ipc_extra['device_index']}")
with torch.cuda.device(target_device):
slice_tensor = torch.empty(
0, dtype=ipc_extra["dtype"], device=target_device
).set_(
storage,
storage_offset=ipc_extra["storage_offset"],
size=ipc_extra["shape"],
stride=ipc_extra["stride"],
)
except Exception as e:
logger.info("Failed to deserialize from CUDA IPC handle (%s).", e)
raise
reconstructed_tensor = self._copy_slice_tensor_to_target(
slice_tensor, rebuild_device, recons_shape, recons_dtype
) )
try:
target_device = torch.device(f"cuda:{source_device_index}")
with torch.cuda.device(target_device):
storage = torch.UntypedStorage._new_shared_cuda(*handle)
slice_tensor = torch.empty(
0, dtype=dtype, device=target_device
).set_(storage, storage_offset=s_offset, size=shape, stride=stride)
reconstructed_tensor = torch.empty(
recons_shape, dtype=recons_dtype, device=rebuild_device
).contiguous()
reconstructed_tensor.view(torch.int8).view(-1).copy_(slice_tensor)
open(SHM_LOCK_FILE, "a").close()
# write the shm_sync_buffer with a file lock
with open(SHM_LOCK_FILE, "w+") as f:
fcntl.flock(f, fcntl.LOCK_EX)
sync_flag = self.get_sync_flag
sync_flag += 1
fcntl.flock(f, fcntl.LOCK_UN)
self.close_shm()
except Exception as e:
logger.info(f"Error: Failed to deserialize from CUDA IPC handle ({e}).")
raise e
elif isinstance(self.proxy_state["tensor_data"], torch.Tensor): elif isinstance(self.proxy_state["tensor_data"], torch.Tensor):
reconstructed_tensor = self.proxy_state["tensor_data"].to( reconstructed_tensor = self.proxy_state["tensor_data"].to(
rebuild_device, non_blocking=True rebuild_device, non_blocking=True