[Disagg] GPU staging buffer with dynamic ring allocator for heterogeneous TP KV transfer (#19890)
This commit is contained in:
@@ -148,6 +148,7 @@ class CommonKVManager(BaseKVManager):
|
||||
# These timeout requests should be aborted to release the tree cache.
|
||||
self.bootstrap_timeout = envs.SGLANG_DISAGGREGATION_BOOTSTRAP_TIMEOUT.get()
|
||||
elif self.disaggregation_mode == DisaggregationMode.DECODE:
|
||||
self.enable_staging: bool = False
|
||||
self.connection_pool: Dict[str, Dict[str, Union[str, int]]] = {}
|
||||
self.connection_lock = threading.Lock()
|
||||
self.required_prefill_response_num_table: Dict[int, int] = {}
|
||||
@@ -501,6 +502,7 @@ class CommonKVReceiver(BaseKVReceiver):
|
||||
self.bootstrap_addr = bootstrap_addr
|
||||
self.kv_mgr = mgr
|
||||
self.conclude_state: Optional[KVPoll] = None
|
||||
self.require_staging: bool = False
|
||||
self.kv_mgr.addr_to_rooms_tracker[self.bootstrap_addr].add(self.bootstrap_room)
|
||||
self.kv_mgr.update_status(self.bootstrap_room, KVPoll.Bootstrapping)
|
||||
|
||||
@@ -529,6 +531,12 @@ class CommonKVReceiver(BaseKVReceiver):
|
||||
self.required_prefill_response_num
|
||||
)
|
||||
|
||||
if self.kv_mgr.enable_staging:
|
||||
self.require_staging = (
|
||||
self.prefill_info.attn_tp_size != 0
|
||||
and self.prefill_info.attn_tp_size != self.kv_mgr.attn_tp_size
|
||||
)
|
||||
|
||||
self.prefill_dp_rank = prefill_dp_rank
|
||||
self._setup_bootstrap_infos()
|
||||
self.kv_mgr.update_status(self.bootstrap_room, KVPoll.WaitingForInput)
|
||||
|
||||
@@ -0,0 +1,768 @@
|
||||
"""
|
||||
GPU Staging Buffer for heterogeneous TP KV cache transfer.
|
||||
|
||||
When prefill attn_tp_size != decode attn_tp_size, the per-token RDMA approach
|
||||
generates O(tokens * layers) small RDMA requests. This module provides a staging
|
||||
buffer mechanism that gathers scattered head slices into contiguous GPU memory,
|
||||
enabling bulk RDMA transfers that reduce request count to O(layers) or O(1).
|
||||
|
||||
Usage:
|
||||
Activated by setting SGLANG_DISAGG_STAGING_BUFFER=1.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO(yangminl): remove torch fallback implementations once the Triton kernels
|
||||
# have been validated in production across all configurations.
|
||||
_USE_TRITON_STAGING = not bool(os.environ.get("SGLANG_STAGING_USE_TORCH", ""))
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fused_gather_to_staging_kernel(
|
||||
layer_ptrs,
|
||||
page_indices,
|
||||
staging,
|
||||
num_tokens,
|
||||
stride_pool_token,
|
||||
head_offset,
|
||||
per_layer_elems,
|
||||
ELEMS_PER_TOKEN: tl.constexpr,
|
||||
PAGE_SIZE: tl.constexpr,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
layer_id = tl.program_id(0)
|
||||
block_id = tl.program_id(1)
|
||||
|
||||
layer_ptr = tl.load(layer_ptrs + layer_id).to(staging.dtype)
|
||||
|
||||
offsets = block_id * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
|
||||
mask = offsets < per_layer_elems
|
||||
|
||||
t_idx = offsets // ELEMS_PER_TOKEN
|
||||
e_idx = offsets % ELEMS_PER_TOKEN
|
||||
|
||||
page_id = t_idx // PAGE_SIZE
|
||||
intra_page = t_idx % PAGE_SIZE
|
||||
page_val = tl.load(page_indices + page_id, mask=mask, other=0)
|
||||
pool_token = page_val * PAGE_SIZE + intra_page
|
||||
|
||||
src_offsets = (
|
||||
pool_token * stride_pool_token.to(tl.int64) + head_offset.to(tl.int64) + e_idx
|
||||
)
|
||||
vals = tl.load(layer_ptr + src_offsets, mask=mask)
|
||||
|
||||
dst_offsets = tl.program_id(0).to(tl.int64) * per_layer_elems.to(tl.int64) + offsets
|
||||
tl.store(staging + dst_offsets, vals, mask=mask)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fused_scatter_from_staging_kernel(
|
||||
layer_ptrs,
|
||||
page_indices,
|
||||
staging,
|
||||
writer_head_offsets,
|
||||
num_tokens,
|
||||
stride_pool_token,
|
||||
per_layer_elems,
|
||||
ELEMS_PER_TOKEN: tl.constexpr,
|
||||
PAGE_SIZE: tl.constexpr,
|
||||
NUM_LAYERS_X2: tl.constexpr,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
prog_id = tl.program_id(0)
|
||||
block_id = tl.program_id(1)
|
||||
|
||||
writer_id = prog_id // NUM_LAYERS_X2
|
||||
layer_kv_id = prog_id % NUM_LAYERS_X2
|
||||
|
||||
layer_ptr = tl.load(layer_ptrs + layer_kv_id).to(staging.dtype)
|
||||
head_offset = tl.load(writer_head_offsets + writer_id)
|
||||
|
||||
offsets = block_id * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
|
||||
mask = offsets < per_layer_elems
|
||||
|
||||
t_idx = offsets // ELEMS_PER_TOKEN
|
||||
e_idx = offsets % ELEMS_PER_TOKEN
|
||||
|
||||
page_id = t_idx // PAGE_SIZE
|
||||
intra_page = t_idx % PAGE_SIZE
|
||||
page_val = tl.load(page_indices + page_id, mask=mask, other=0)
|
||||
pool_token = page_val * PAGE_SIZE + intra_page
|
||||
|
||||
per_rank_elems = per_layer_elems.to(tl.int64) * NUM_LAYERS_X2
|
||||
src_offsets = (
|
||||
writer_id.to(tl.int64) * per_rank_elems
|
||||
+ layer_kv_id.to(tl.int64) * per_layer_elems.to(tl.int64)
|
||||
+ offsets
|
||||
)
|
||||
vals = tl.load(staging + src_offsets, mask=mask)
|
||||
|
||||
dst_offsets = (
|
||||
pool_token * stride_pool_token.to(tl.int64) + head_offset.to(tl.int64) + e_idx
|
||||
)
|
||||
tl.store(layer_ptr + dst_offsets, vals, mask=mask)
|
||||
|
||||
|
||||
class StagingBuffer:
|
||||
"""Pre-allocated GPU staging buffer for bulk KV transfer.
|
||||
|
||||
When a custom_mem_pool is provided (e.g., mooncake NVLink allocator),
|
||||
the buffer is allocated within that pool so it's compatible with
|
||||
NVLink/MNNVL transport (requires cuMemCreate-backed memory).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size_bytes: int,
|
||||
device: str,
|
||||
gpu_id: int,
|
||||
custom_mem_pool=None,
|
||||
):
|
||||
self.size_bytes = size_bytes
|
||||
self.device = device
|
||||
self.gpu_id = gpu_id
|
||||
|
||||
torch.cuda.set_device(gpu_id)
|
||||
if custom_mem_pool is not None:
|
||||
with torch.cuda.use_mem_pool(custom_mem_pool):
|
||||
self.buffer = torch.empty(size_bytes, dtype=torch.uint8, device=device)
|
||||
alloc_method = "custom_mem_pool (cuMemCreate)"
|
||||
else:
|
||||
self.buffer = torch.empty(size_bytes, dtype=torch.uint8, device=device)
|
||||
alloc_method = "cudaMalloc (NVLink incompatible!)"
|
||||
self.data_ptr = self.buffer.data_ptr()
|
||||
|
||||
logger.info(
|
||||
f"StagingBuffer allocated: {size_bytes / (1024*1024):.1f} MB "
|
||||
f"on {device}, method={alloc_method}, ptr=0x{self.data_ptr:x}"
|
||||
)
|
||||
|
||||
def get_ptr(self) -> int:
|
||||
return self.data_ptr
|
||||
|
||||
def get_size(self) -> int:
|
||||
return self.size_bytes
|
||||
|
||||
def fits(self, required_bytes: int) -> bool:
|
||||
return required_bytes <= self.size_bytes
|
||||
|
||||
|
||||
class StagingAllocator:
|
||||
"""Decode-side dynamic staging ring buffer allocator with overcommit.
|
||||
|
||||
One large pre-allocated GPU buffer used as a ring buffer. Each request
|
||||
gets a (alloc_id, offset, round) triple based on its actual byte
|
||||
requirement. Allocation (assign) is overcommit — it always succeeds
|
||||
as long as the request fits in the buffer. Overlap safety is enforced
|
||||
on the prefill side before RDMA, using a watermark that tracks the
|
||||
oldest un-freed allocation.
|
||||
|
||||
The watermark (round, tail_offset) is periodically sent to prefill.
|
||||
Prefill transfer workers wait before writing if their target region
|
||||
overlaps with not-yet-freed data from a previous round.
|
||||
"""
|
||||
|
||||
# Permanent alloc failure: chunk exceeds ring buffer total size.
|
||||
ALLOC_OVERSIZED = -2
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
total_size_bytes: int,
|
||||
device: str,
|
||||
gpu_id: int,
|
||||
custom_mem_pool=None,
|
||||
):
|
||||
self.buffer = StagingBuffer(total_size_bytes, device, gpu_id, custom_mem_pool)
|
||||
self.total_size = total_size_bytes
|
||||
self.base_ptr = self.buffer.data_ptr
|
||||
self.head = 0
|
||||
self.round = 0
|
||||
self.allocations: dict = {} # alloc_id -> (offset, size, round)
|
||||
self.alloc_order: List[int] = []
|
||||
self.next_alloc_id = 0
|
||||
self.watermark_round = 0
|
||||
self.watermark_tail = 0
|
||||
self.lock = threading.Lock()
|
||||
|
||||
logger.info(
|
||||
f"StagingAllocator (ring+overcommit): "
|
||||
f"{total_size_bytes / (1024*1024):.1f} MB "
|
||||
f"on {device}, ptr=0x{self.base_ptr:x}"
|
||||
)
|
||||
|
||||
def assign(self, required_bytes: int) -> Optional[Tuple[int, int, int]]:
|
||||
"""Allocate a region. Returns (alloc_id, offset, round) or None."""
|
||||
with self.lock:
|
||||
if required_bytes > self.total_size:
|
||||
return None
|
||||
|
||||
space_at_end = self.total_size - self.head
|
||||
if required_bytes <= space_at_end:
|
||||
offset = self.head
|
||||
self.head += required_bytes
|
||||
else:
|
||||
self.round += 1
|
||||
offset = 0
|
||||
self.head = required_bytes
|
||||
|
||||
alloc_id = self.next_alloc_id
|
||||
self.next_alloc_id += 1
|
||||
self.allocations[alloc_id] = (offset, required_bytes, self.round)
|
||||
self.alloc_order.append(alloc_id)
|
||||
return (alloc_id, offset, self.round)
|
||||
|
||||
def free(self, alloc_id: int):
|
||||
"""Free an allocation and advance watermark past consecutive freed entries."""
|
||||
with self.lock:
|
||||
if alloc_id not in self.allocations:
|
||||
return
|
||||
self.allocations.pop(alloc_id)
|
||||
|
||||
while self.alloc_order and self.alloc_order[0] not in self.allocations:
|
||||
self.alloc_order.pop(0)
|
||||
|
||||
if not self.allocations:
|
||||
self.watermark_round = self.round
|
||||
self.watermark_tail = self.head
|
||||
elif self.alloc_order:
|
||||
off, _, rnd = self.allocations[self.alloc_order[0]]
|
||||
self.watermark_round = rnd
|
||||
self.watermark_tail = off
|
||||
|
||||
def get_watermark(self) -> Tuple[int, int]:
|
||||
"""Return (round, tail_offset). Everything before this is safe to write."""
|
||||
with self.lock:
|
||||
return (self.watermark_round, self.watermark_tail)
|
||||
|
||||
def get_ptr(self, alloc_id: int) -> int:
|
||||
offset, _, _ = self.allocations[alloc_id]
|
||||
return self.base_ptr + offset
|
||||
|
||||
def get_offset(self, alloc_id: int) -> int:
|
||||
offset, _, _ = self.allocations[alloc_id]
|
||||
return offset
|
||||
|
||||
def get_round(self, alloc_id: int) -> int:
|
||||
_, _, rnd = self.allocations[alloc_id]
|
||||
return rnd
|
||||
|
||||
def get_base_ptr(self) -> int:
|
||||
return self.base_ptr
|
||||
|
||||
def get_total_size(self) -> int:
|
||||
return self.total_size
|
||||
|
||||
|
||||
def gather_kv_head_slices(
|
||||
kv_buffer_tensor: torch.Tensor,
|
||||
gather_idx: torch.Tensor,
|
||||
head_start: int,
|
||||
num_heads: int,
|
||||
staging_tensor: torch.Tensor,
|
||||
):
|
||||
"""Gather KV head slices from scattered pages into contiguous staging buffer.
|
||||
|
||||
Uses torch.gather(out=) to write directly into staging_tensor without
|
||||
allocating temporary tensors (avoids CUDA caching allocator stalls).
|
||||
|
||||
Args:
|
||||
kv_buffer_tensor: [pool_size, head_num, head_dim], one layer.
|
||||
gather_idx: [num_tokens, num_heads, head_dim] int64, pre-computed
|
||||
token indices expanded for gather on dim=0.
|
||||
head_start: Starting head index for the slice.
|
||||
num_heads: Number of heads to gather.
|
||||
staging_tensor: Output tensor, shape [num_tokens, num_heads, head_dim].
|
||||
"""
|
||||
src = kv_buffer_tensor[:, head_start : head_start + num_heads, :]
|
||||
torch.gather(src, 0, gather_idx, out=staging_tensor)
|
||||
|
||||
|
||||
def scatter_kv_head_slices(
|
||||
staging_tensor: torch.Tensor,
|
||||
kv_buffer_tensor: torch.Tensor,
|
||||
page_indices: torch.Tensor,
|
||||
head_start: int,
|
||||
num_heads: int,
|
||||
page_size: int = 1,
|
||||
):
|
||||
"""Scatter KV head slices from contiguous staging buffer to KV cache.
|
||||
|
||||
Args:
|
||||
staging_tensor: Input tensor from staging buffer (contiguous packed data).
|
||||
kv_buffer_tensor: The KV buffer for one layer, shape [pool_size, head_num, head_dim].
|
||||
page_indices: [num_pages] int32/int64 tensor of page indices.
|
||||
head_start: Starting head index for the slice.
|
||||
num_heads: Number of heads to scatter.
|
||||
page_size: Number of tokens per page.
|
||||
"""
|
||||
head_dim = kv_buffer_tensor.shape[-1]
|
||||
if page_size == 1:
|
||||
num_tokens = page_indices.shape[0]
|
||||
data = staging_tensor.reshape(num_tokens, num_heads, head_dim)
|
||||
kv_buffer_tensor[page_indices, head_start : head_start + num_heads, :] = data
|
||||
else:
|
||||
num_tokens = page_indices.shape[0] * page_size
|
||||
offsets = torch.arange(page_size, device=page_indices.device)
|
||||
token_indices = (page_indices.unsqueeze(1) * page_size + offsets).reshape(-1)
|
||||
data = staging_tensor.reshape(num_tokens, num_heads, head_dim)
|
||||
kv_buffer_tensor[token_indices, head_start : head_start + num_heads, :] = data
|
||||
|
||||
|
||||
def _gather_all_layers_torch(
|
||||
k_buffers: list,
|
||||
v_buffers: list,
|
||||
page_indices_np,
|
||||
staging_buffer: StagingBuffer,
|
||||
src_head_start: int,
|
||||
num_heads: int,
|
||||
page_size: int,
|
||||
gpu_id: int,
|
||||
) -> int:
|
||||
"""torch.gather path: zero per-layer allocation, one kernel per layer."""
|
||||
import numpy as np
|
||||
|
||||
num_layers = len(k_buffers)
|
||||
head_dim = k_buffers[0].shape[-1]
|
||||
dtype_size = k_buffers[0].element_size()
|
||||
num_tokens = len(page_indices_np) * page_size
|
||||
per_layer_bytes = num_tokens * num_heads * head_dim * dtype_size
|
||||
|
||||
device = f"cuda:{gpu_id}"
|
||||
torch.cuda.set_device(gpu_id)
|
||||
page_idx_tensor = torch.from_numpy(page_indices_np.astype(np.int64)).to(device)
|
||||
|
||||
if page_size == 1:
|
||||
token_indices = page_idx_tensor
|
||||
else:
|
||||
offsets = torch.arange(page_size, device=device)
|
||||
token_indices = (page_idx_tensor.unsqueeze(1) * page_size + offsets).reshape(-1)
|
||||
|
||||
gather_idx = token_indices.view(-1, 1, 1).expand(num_tokens, num_heads, head_dim)
|
||||
|
||||
if not hasattr(staging_buffer, "_gather_stream"):
|
||||
staging_buffer._gather_stream = torch.cuda.Stream(device=device)
|
||||
|
||||
staging_buffer._gather_stream.wait_stream(
|
||||
torch.cuda.default_stream(torch.device(device))
|
||||
)
|
||||
|
||||
staging_view = staging_buffer.buffer
|
||||
offset = 0
|
||||
with torch.cuda.stream(staging_buffer._gather_stream):
|
||||
for layer_id in range(num_layers):
|
||||
dst = (
|
||||
staging_view[offset : offset + per_layer_bytes]
|
||||
.view(k_buffers[layer_id].dtype)
|
||||
.reshape(num_tokens, num_heads, head_dim)
|
||||
)
|
||||
gather_kv_head_slices(
|
||||
k_buffers[layer_id],
|
||||
gather_idx,
|
||||
src_head_start,
|
||||
num_heads,
|
||||
dst,
|
||||
)
|
||||
offset += per_layer_bytes
|
||||
for layer_id in range(num_layers):
|
||||
dst = (
|
||||
staging_view[offset : offset + per_layer_bytes]
|
||||
.view(v_buffers[layer_id].dtype)
|
||||
.reshape(num_tokens, num_heads, head_dim)
|
||||
)
|
||||
gather_kv_head_slices(
|
||||
v_buffers[layer_id],
|
||||
gather_idx,
|
||||
src_head_start,
|
||||
num_heads,
|
||||
dst,
|
||||
)
|
||||
offset += per_layer_bytes
|
||||
|
||||
staging_buffer._gather_stream.synchronize()
|
||||
return offset
|
||||
|
||||
|
||||
def _gather_all_layers_triton(
|
||||
k_buffers: list,
|
||||
v_buffers: list,
|
||||
page_indices_np,
|
||||
staging_buffer: StagingBuffer,
|
||||
src_head_start: int,
|
||||
num_heads: int,
|
||||
page_size: int,
|
||||
gpu_id: int,
|
||||
) -> int:
|
||||
"""Triton fused kernel path: single kernel launch for all layers."""
|
||||
import numpy as np
|
||||
|
||||
num_layers = len(k_buffers)
|
||||
head_dim = k_buffers[0].shape[-1]
|
||||
total_heads = k_buffers[0].shape[1]
|
||||
dtype_size = k_buffers[0].element_size()
|
||||
num_tokens = len(page_indices_np) * page_size
|
||||
elems_per_token = num_heads * head_dim
|
||||
per_layer_elems = num_tokens * elems_per_token
|
||||
per_layer_bytes = per_layer_elems * dtype_size
|
||||
total_bytes = per_layer_bytes * num_layers * 2
|
||||
|
||||
device = f"cuda:{gpu_id}"
|
||||
torch.cuda.set_device(gpu_id)
|
||||
page_idx_tensor = torch.from_numpy(page_indices_np.astype(np.int64)).to(device)
|
||||
|
||||
layer_ptrs = torch.tensor(
|
||||
[buf.data_ptr() for buf in k_buffers] + [buf.data_ptr() for buf in v_buffers],
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
)
|
||||
# Use integer dtype matching element size for bit-preserving copy
|
||||
int_dtype_map = {1: torch.int8, 2: torch.int16, 4: torch.int32}
|
||||
int_dtype = int_dtype_map.get(dtype_size, torch.int16)
|
||||
staging_typed = staging_buffer.buffer[:total_bytes].view(int_dtype)
|
||||
|
||||
if not hasattr(staging_buffer, "_gather_stream"):
|
||||
staging_buffer._gather_stream = torch.cuda.Stream(device=device)
|
||||
|
||||
staging_buffer._gather_stream.wait_stream(
|
||||
torch.cuda.default_stream(torch.device(device))
|
||||
)
|
||||
|
||||
BLOCK_SIZE = 1024
|
||||
grid = (2 * num_layers, triton.cdiv(per_layer_elems, BLOCK_SIZE))
|
||||
|
||||
with torch.cuda.stream(staging_buffer._gather_stream):
|
||||
_fused_gather_to_staging_kernel[grid](
|
||||
layer_ptrs,
|
||||
page_idx_tensor,
|
||||
staging_typed,
|
||||
num_tokens,
|
||||
total_heads * head_dim,
|
||||
src_head_start * head_dim,
|
||||
per_layer_elems,
|
||||
elems_per_token,
|
||||
page_size,
|
||||
BLOCK_SIZE,
|
||||
)
|
||||
|
||||
staging_buffer._gather_stream.synchronize()
|
||||
return total_bytes
|
||||
|
||||
|
||||
def gather_all_layers_to_staging(
|
||||
k_buffers: list,
|
||||
v_buffers: list,
|
||||
page_indices_np,
|
||||
staging_buffer: StagingBuffer,
|
||||
src_head_start: int,
|
||||
num_heads: int,
|
||||
page_size: int,
|
||||
gpu_id: int,
|
||||
) -> int:
|
||||
"""Gather all layers' K and V head slices into a staging buffer.
|
||||
|
||||
Returns total bytes written.
|
||||
Dispatches to Triton fused kernel when available, falls back to torch.gather.
|
||||
"""
|
||||
if _USE_TRITON_STAGING:
|
||||
return _gather_all_layers_triton(
|
||||
k_buffers,
|
||||
v_buffers,
|
||||
page_indices_np,
|
||||
staging_buffer,
|
||||
src_head_start,
|
||||
num_heads,
|
||||
page_size,
|
||||
gpu_id,
|
||||
)
|
||||
return _gather_all_layers_torch(
|
||||
k_buffers,
|
||||
v_buffers,
|
||||
page_indices_np,
|
||||
staging_buffer,
|
||||
src_head_start,
|
||||
num_heads,
|
||||
page_size,
|
||||
gpu_id,
|
||||
)
|
||||
|
||||
|
||||
def _scatter_staging_to_kv_torch(
|
||||
staging_buffer_view: torch.Tensor,
|
||||
k_buffers: list,
|
||||
v_buffers: list,
|
||||
page_idx_tensor: torch.Tensor,
|
||||
page_size: int,
|
||||
prefill_attn_tp_size: int,
|
||||
decode_attn_tp_size: int,
|
||||
dst_tp_rank: int,
|
||||
total_kv_heads: int,
|
||||
) -> None:
|
||||
"""torch path for scatter."""
|
||||
num_layers = len(k_buffers)
|
||||
head_dim = k_buffers[0].shape[-1]
|
||||
dtype_size = k_buffers[0].element_size()
|
||||
num_tokens = page_idx_tensor.shape[0] * page_size
|
||||
|
||||
if prefill_attn_tp_size > decode_attn_tp_size:
|
||||
num_writers = prefill_attn_tp_size // max(1, decode_attn_tp_size)
|
||||
else:
|
||||
num_writers = 1
|
||||
|
||||
for writer_rank in range(num_writers):
|
||||
_, num_heads, dst_head_start, _ = compute_head_slice_params(
|
||||
prefill_attn_tp_size,
|
||||
decode_attn_tp_size,
|
||||
writer_rank,
|
||||
dst_tp_rank,
|
||||
total_kv_heads,
|
||||
)
|
||||
per_layer_bytes = num_tokens * num_heads * head_dim * dtype_size
|
||||
per_rank_bytes = per_layer_bytes * num_layers * 2
|
||||
rank_base = writer_rank * per_rank_bytes
|
||||
|
||||
offset = rank_base
|
||||
for layer_id in range(num_layers):
|
||||
layer_data = (
|
||||
staging_buffer_view[offset : offset + per_layer_bytes]
|
||||
.view(k_buffers[layer_id].dtype)
|
||||
.reshape(num_tokens, num_heads, head_dim)
|
||||
)
|
||||
scatter_kv_head_slices(
|
||||
layer_data,
|
||||
k_buffers[layer_id],
|
||||
page_idx_tensor,
|
||||
dst_head_start,
|
||||
num_heads,
|
||||
page_size,
|
||||
)
|
||||
offset += per_layer_bytes
|
||||
for layer_id in range(num_layers):
|
||||
layer_data = (
|
||||
staging_buffer_view[offset : offset + per_layer_bytes]
|
||||
.view(v_buffers[layer_id].dtype)
|
||||
.reshape(num_tokens, num_heads, head_dim)
|
||||
)
|
||||
scatter_kv_head_slices(
|
||||
layer_data,
|
||||
v_buffers[layer_id],
|
||||
page_idx_tensor,
|
||||
dst_head_start,
|
||||
num_heads,
|
||||
page_size,
|
||||
)
|
||||
offset += per_layer_bytes
|
||||
|
||||
|
||||
def _scatter_staging_to_kv_triton(
|
||||
staging_buffer_view: torch.Tensor,
|
||||
k_buffers: list,
|
||||
v_buffers: list,
|
||||
page_idx_tensor: torch.Tensor,
|
||||
page_size: int,
|
||||
prefill_attn_tp_size: int,
|
||||
decode_attn_tp_size: int,
|
||||
dst_tp_rank: int,
|
||||
total_kv_heads: int,
|
||||
) -> None:
|
||||
"""Triton fused kernel path for scatter."""
|
||||
num_layers = len(k_buffers)
|
||||
head_dim = k_buffers[0].shape[-1]
|
||||
total_heads = k_buffers[0].shape[1]
|
||||
dtype_size = k_buffers[0].element_size()
|
||||
num_tokens = page_idx_tensor.shape[0] * page_size
|
||||
device = page_idx_tensor.device
|
||||
|
||||
if prefill_attn_tp_size > decode_attn_tp_size:
|
||||
num_writers = prefill_attn_tp_size // max(1, decode_attn_tp_size)
|
||||
else:
|
||||
num_writers = 1
|
||||
|
||||
# All writers share the same num_heads; only dst_head_start differs
|
||||
_, num_heads, _, _ = compute_head_slice_params(
|
||||
prefill_attn_tp_size,
|
||||
decode_attn_tp_size,
|
||||
0,
|
||||
dst_tp_rank,
|
||||
total_kv_heads,
|
||||
)
|
||||
elems_per_token = num_heads * head_dim
|
||||
per_layer_elems = num_tokens * elems_per_token
|
||||
|
||||
layer_ptrs = torch.tensor(
|
||||
[buf.data_ptr() for buf in k_buffers] + [buf.data_ptr() for buf in v_buffers],
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
)
|
||||
|
||||
writer_head_offsets = torch.tensor(
|
||||
[
|
||||
compute_head_slice_params(
|
||||
prefill_attn_tp_size,
|
||||
decode_attn_tp_size,
|
||||
wr,
|
||||
dst_tp_rank,
|
||||
total_kv_heads,
|
||||
)[2]
|
||||
* head_dim
|
||||
for wr in range(num_writers)
|
||||
],
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
)
|
||||
|
||||
int_dtype_map = {1: torch.int8, 2: torch.int16, 4: torch.int32}
|
||||
int_dtype = int_dtype_map.get(dtype_size, torch.int16)
|
||||
total_staging_bytes = (
|
||||
num_tokens * elems_per_token * dtype_size * num_layers * 2 * num_writers
|
||||
)
|
||||
staging_typed = staging_buffer_view[:total_staging_bytes].view(int_dtype)
|
||||
|
||||
BLOCK_SIZE = 1024
|
||||
num_layers_x2 = 2 * num_layers
|
||||
grid = (num_writers * num_layers_x2, triton.cdiv(per_layer_elems, BLOCK_SIZE))
|
||||
|
||||
_fused_scatter_from_staging_kernel[grid](
|
||||
layer_ptrs,
|
||||
page_idx_tensor,
|
||||
staging_typed,
|
||||
writer_head_offsets,
|
||||
num_tokens,
|
||||
total_heads * head_dim,
|
||||
per_layer_elems,
|
||||
elems_per_token,
|
||||
page_size,
|
||||
num_layers_x2,
|
||||
BLOCK_SIZE,
|
||||
)
|
||||
|
||||
|
||||
def scatter_staging_to_kv(
|
||||
staging_buffer_view: torch.Tensor,
|
||||
k_buffers: list,
|
||||
v_buffers: list,
|
||||
page_idx_tensor: torch.Tensor,
|
||||
page_size: int,
|
||||
prefill_attn_tp_size: int,
|
||||
decode_attn_tp_size: int,
|
||||
dst_tp_rank: int,
|
||||
total_kv_heads: int,
|
||||
) -> None:
|
||||
"""Scatter data from a contiguous staging region into KV cache buffers."""
|
||||
if _USE_TRITON_STAGING:
|
||||
return _scatter_staging_to_kv_triton(
|
||||
staging_buffer_view,
|
||||
k_buffers,
|
||||
v_buffers,
|
||||
page_idx_tensor,
|
||||
page_size,
|
||||
prefill_attn_tp_size,
|
||||
decode_attn_tp_size,
|
||||
dst_tp_rank,
|
||||
total_kv_heads,
|
||||
)
|
||||
return _scatter_staging_to_kv_torch(
|
||||
staging_buffer_view,
|
||||
k_buffers,
|
||||
v_buffers,
|
||||
page_idx_tensor,
|
||||
page_size,
|
||||
prefill_attn_tp_size,
|
||||
decode_attn_tp_size,
|
||||
dst_tp_rank,
|
||||
total_kv_heads,
|
||||
)
|
||||
|
||||
|
||||
def compute_head_slice_params(
|
||||
src_attn_tp_size: int,
|
||||
dst_attn_tp_size: int,
|
||||
src_tp_rank: int,
|
||||
dst_tp_rank: int,
|
||||
total_kv_heads: int,
|
||||
) -> Tuple[int, int, int, int]:
|
||||
"""Compute head slicing parameters for heterogeneous TP transfer.
|
||||
|
||||
Returns:
|
||||
(src_head_start, num_heads_to_send, dst_head_start, num_heads_to_send)
|
||||
"""
|
||||
src_heads_per_rank = max(1, total_kv_heads // src_attn_tp_size)
|
||||
dst_heads_per_rank = max(1, total_kv_heads // dst_attn_tp_size)
|
||||
|
||||
local_tp_rank = src_tp_rank % src_attn_tp_size
|
||||
dst_tp_rank_in_group = dst_tp_rank % dst_attn_tp_size
|
||||
|
||||
if src_attn_tp_size > dst_attn_tp_size:
|
||||
src_head_start = 0
|
||||
num_heads_to_send = src_heads_per_rank
|
||||
src_replication = max(1, src_attn_tp_size // total_kv_heads)
|
||||
unique_head_idx = local_tp_rank // src_replication
|
||||
dst_head_start = (unique_head_idx * src_heads_per_rank) % dst_heads_per_rank
|
||||
else:
|
||||
src_head_start = (
|
||||
dst_tp_rank_in_group * dst_heads_per_rank
|
||||
) % src_heads_per_rank
|
||||
num_heads_to_send = dst_heads_per_rank
|
||||
dst_head_start = 0
|
||||
|
||||
return src_head_start, num_heads_to_send, dst_head_start, num_heads_to_send
|
||||
|
||||
|
||||
def compute_staging_layout(
|
||||
src_attn_tp_size: int,
|
||||
dst_attn_tp_size: int,
|
||||
dst_tp_rank: int,
|
||||
total_kv_heads: int,
|
||||
num_tokens: int,
|
||||
bytes_per_head_token: int,
|
||||
num_layers: int,
|
||||
) -> Tuple[int, List[int], int]:
|
||||
"""Compute per-writer byte layout for a staging region.
|
||||
|
||||
Returns:
|
||||
(num_writers, writer_bytes_list, total_bytes)
|
||||
where writer_bytes_list[i] = bytes for writer i covering all layers (K+V).
|
||||
"""
|
||||
if src_attn_tp_size > dst_attn_tp_size:
|
||||
num_writers = src_attn_tp_size // max(1, dst_attn_tp_size)
|
||||
else:
|
||||
num_writers = 1
|
||||
|
||||
writer_bytes = []
|
||||
for wr in range(num_writers):
|
||||
_, nh, _, _ = compute_head_slice_params(
|
||||
src_attn_tp_size,
|
||||
dst_attn_tp_size,
|
||||
wr,
|
||||
dst_tp_rank,
|
||||
total_kv_heads,
|
||||
)
|
||||
writer_bytes.append(num_tokens * nh * bytes_per_head_token * num_layers * 2)
|
||||
return num_writers, writer_bytes, sum(writer_bytes)
|
||||
|
||||
|
||||
def resolve_total_kv_heads(
|
||||
kv_args,
|
||||
attn_tp_size: int,
|
||||
) -> int:
|
||||
"""Resolve the global total KV head count from kv_args metadata."""
|
||||
total = getattr(kv_args, "total_kv_head_num", 0)
|
||||
if total > 0:
|
||||
return total
|
||||
per_rank = getattr(kv_args, "kv_head_num", 0)
|
||||
if per_rank > 0:
|
||||
return per_rank * attn_tp_size
|
||||
raise ValueError(
|
||||
"Cannot resolve total_kv_heads: kv_args has neither total_kv_head_num "
|
||||
"nor kv_head_num. "
|
||||
"Ensure DecodePreallocQueue._init_kv_manager sets kv_args.kv_head_num."
|
||||
)
|
||||
@@ -0,0 +1,732 @@
|
||||
"""
|
||||
Staging handler for heterogeneous TP KV cache transfer.
|
||||
|
||||
Isolates staging scatter lifecycle from decode.py and conn.py.
|
||||
Generic (backend-agnostic) code is at the top; mooncake-specific
|
||||
protocol code is at the bottom.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.disaggregation.decode import DecodeRequest
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Generic staging state and handler (backend-agnostic)
|
||||
# ======================================================================
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DecodeStagingContext:
|
||||
"""Staging-specific context for decode mode."""
|
||||
|
||||
allocator: object = None
|
||||
room_bootstrap: dict = dataclasses.field(default_factory=dict)
|
||||
room_receivers: dict = dataclasses.field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PrefillStagingContext:
|
||||
"""Staging-specific context for prefill mode."""
|
||||
|
||||
buffers: list = dataclasses.field(default_factory=list)
|
||||
remote_watermarks: dict = dataclasses.field(default_factory=dict)
|
||||
watermark_cv: threading.Condition = dataclasses.field(
|
||||
default_factory=threading.Condition
|
||||
)
|
||||
prefetch_requested: set = dataclasses.field(default_factory=set)
|
||||
prefetch_sockets: dict = dataclasses.field(default_factory=dict)
|
||||
|
||||
|
||||
class DecodeStagingHandler:
|
||||
"""Decode-side staging scatter lifecycle manager.
|
||||
|
||||
Scatter submission can be called from the decode_thread (background) as
|
||||
soon as all writers/ranks have arrived, while event checking and freeing
|
||||
always run on the scheduler main thread.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
kv_manager,
|
||||
staging_allocator,
|
||||
kv_buffer_info: dict,
|
||||
decode_tp: int,
|
||||
total_kv_heads: int,
|
||||
tp_rank: int,
|
||||
scheduler,
|
||||
):
|
||||
self.kv_manager = kv_manager
|
||||
self.staging_allocator = staging_allocator
|
||||
self.kv_buffer_info = kv_buffer_info
|
||||
self.decode_tp = decode_tp
|
||||
self.total_kv_heads = total_kv_heads
|
||||
self.tp_rank = tp_rank
|
||||
self.scheduler = scheduler
|
||||
self._room_to_decode_req: dict = {}
|
||||
self._wm_subscribers: dict = {}
|
||||
|
||||
def register_wm_subscriber(self, receiver, session_id: str) -> None:
|
||||
"""Register a prefill's bootstrap connection for watermark broadcasts."""
|
||||
if receiver is None or not getattr(receiver, "bootstrap_infos", None):
|
||||
return
|
||||
key = tuple(str(bi) for bi in receiver.bootstrap_infos)
|
||||
if key not in self._wm_subscribers:
|
||||
self._wm_subscribers[key] = (receiver, session_id)
|
||||
|
||||
def num_writers_for(self, decode_req) -> int:
|
||||
"""Compute num_writers for a specific request based on its prefill TP."""
|
||||
prefill_tp = decode_req.kv_receiver.prefill_info.attn_tp_size
|
||||
if prefill_tp > self.decode_tp:
|
||||
return prefill_tp // max(1, self.decode_tp)
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def create(cls, kv_manager, scheduler, tp_rank: int) -> "DecodeStagingHandler":
|
||||
"""Factory: create handler. Raises if staging infra is missing."""
|
||||
staging_allocator = kv_manager._staging_ctx.allocator
|
||||
if staging_allocator is None:
|
||||
raise RuntimeError(
|
||||
"Staging is enabled but kv_manager._staging_ctx.allocator is None. "
|
||||
"Check that the transfer backend correctly initializes the staging allocator."
|
||||
)
|
||||
kv_buffer_info = kv_manager.kv_buffer_tensors
|
||||
if kv_buffer_info is None:
|
||||
raise RuntimeError(
|
||||
"Staging is enabled but kv_manager.kv_buffer_tensors is None. "
|
||||
"Check that set_kv_buffer_tensors() was called during kv_manager init."
|
||||
)
|
||||
decode_tp = kv_manager.attn_tp_size
|
||||
|
||||
from sglang.srt.disaggregation.common.staging_buffer import (
|
||||
resolve_total_kv_heads,
|
||||
)
|
||||
|
||||
total_kv_heads = resolve_total_kv_heads(kv_manager.kv_args, decode_tp)
|
||||
return cls(
|
||||
kv_manager=kv_manager,
|
||||
staging_allocator=staging_allocator,
|
||||
kv_buffer_info=kv_buffer_info,
|
||||
decode_tp=decode_tp,
|
||||
total_kv_heads=total_kv_heads,
|
||||
tp_rank=tp_rank,
|
||||
scheduler=scheduler,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Registration: called from main thread (DecodeTransferQueue)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def register_decode_req(self, room: int, decode_req: "DecodeRequest") -> None:
|
||||
self._room_to_decode_req[room] = decode_req
|
||||
|
||||
def unregister_decode_req(self, room: int) -> None:
|
||||
self._room_to_decode_req.pop(room, None)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Scatter submission: called from decode_thread (background)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def submit_chunk_scatter(
|
||||
self, room: int, chunk_idx: int, page_start: int, num_pages: int
|
||||
) -> bool:
|
||||
"""Submit scatter for an intermediate chunk whose writers all arrived.
|
||||
|
||||
Called from decode_thread. Records a CUDA event on decode_req so
|
||||
the main thread can later check completion and free the allocation.
|
||||
"""
|
||||
decode_req = self._room_to_decode_req.get(room)
|
||||
if decode_req is None:
|
||||
logger.warning(
|
||||
"[STAGING] submit_chunk_scatter: room=%s not registered, "
|
||||
"chunk_idx=%s. This should not happen if register_decode_req "
|
||||
"is called at kv_receiver.init() time.",
|
||||
room,
|
||||
chunk_idx,
|
||||
)
|
||||
return False
|
||||
chunk_infos = getattr(decode_req.kv_receiver, "chunk_staging_infos", [])
|
||||
if chunk_idx >= len(chunk_infos):
|
||||
return False
|
||||
alloc_id, staging_offset, _, _, _ = chunk_infos[chunk_idx]
|
||||
if staging_offset < 0 or alloc_id < 0:
|
||||
return False
|
||||
|
||||
ok = self._scatter_region(staging_offset, page_start, num_pages, decode_req)
|
||||
if ok:
|
||||
event = torch.cuda.Event()
|
||||
event.record(self.staging_allocator._scatter_stream)
|
||||
if not hasattr(decode_req, "_chunk_events"):
|
||||
decode_req._chunk_events = []
|
||||
decode_req._chunk_events.append((event, alloc_id))
|
||||
chunk_infos[chunk_idx] = (-1, -1, 0, -1, 0)
|
||||
else:
|
||||
logger.warning(
|
||||
"submit_chunk_scatter failed room=%s chunk_idx=%s tp_rank=%s",
|
||||
room,
|
||||
chunk_idx,
|
||||
self.tp_rank,
|
||||
)
|
||||
return ok
|
||||
|
||||
def is_staging_room(self, room: int) -> bool:
|
||||
"""Check if a room is registered for staging scatter."""
|
||||
return room in self._room_to_decode_req
|
||||
|
||||
def submit_last_scatter_async(self, room: int) -> bool:
|
||||
"""Submit scatter for the last chunk when all ranks report Success.
|
||||
|
||||
Called from decode_thread. Sets ``_scatter_event`` **before**
|
||||
``_staging_last_scatter_submitted`` so the main thread sees the
|
||||
event when it checks the flag (CPython GIL guarantees ordering).
|
||||
"""
|
||||
decode_req = self._room_to_decode_req.get(room)
|
||||
if decode_req is None:
|
||||
logger.warning(
|
||||
"[STAGING] submit_last_scatter_async: room=%s not registered. "
|
||||
"This should not happen if register_decode_req is called at "
|
||||
"kv_receiver.init() time.",
|
||||
room,
|
||||
)
|
||||
return False
|
||||
alloc_id = self._submit_last_scatter(decode_req)
|
||||
if alloc_id >= 0:
|
||||
event = torch.cuda.Event()
|
||||
event.record(self.staging_allocator._scatter_stream)
|
||||
decode_req._scatter_event = event
|
||||
decode_req._scatter_alloc_id = alloc_id
|
||||
decode_req._staging_last_scatter_submitted = True
|
||||
else:
|
||||
decode_req._staging_scatter_done = True
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Event check + free: called from main thread (pop_transferred)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def is_done(self, decode_req: "DecodeRequest") -> bool:
|
||||
"""Return True if staging scatter is complete for this request."""
|
||||
if not getattr(decode_req, "_staging_scatter_done", False):
|
||||
return False
|
||||
return not getattr(decode_req, "_chunk_events", None)
|
||||
|
||||
def advance_scatter(self, decode_req: "DecodeRequest") -> None:
|
||||
"""Check CUDA events and free completed staging allocations.
|
||||
|
||||
Scatter kernels have already been submitted by the decode_thread
|
||||
(via submit_chunk_scatter / submit_last_scatter_async). This
|
||||
method only polls the recorded events and releases staging memory.
|
||||
"""
|
||||
room = decode_req.req.bootstrap_room
|
||||
chunk_events = getattr(decode_req, "_chunk_events", None)
|
||||
if chunk_events:
|
||||
for i in range(len(chunk_events) - 1, -1, -1):
|
||||
event, alloc_id = chunk_events[i]
|
||||
if event.query():
|
||||
chunk_events.pop(i)
|
||||
self._free_and_send_watermark(alloc_id, decode_req)
|
||||
|
||||
if not getattr(decode_req, "_staging_last_scatter_submitted", False):
|
||||
return
|
||||
|
||||
event = getattr(decode_req, "_scatter_event", None)
|
||||
if event is not None and event.query():
|
||||
self._free_and_send_watermark(decode_req._scatter_alloc_id, decode_req)
|
||||
decode_req._scatter_event = None
|
||||
decode_req._scatter_alloc_id = -1
|
||||
decode_req._staging_scatter_done = True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal methods
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _scatter_region(
|
||||
self,
|
||||
staging_offset: int,
|
||||
page_start: int,
|
||||
num_pages: int,
|
||||
decode_req: "DecodeRequest",
|
||||
) -> bool:
|
||||
"""Submit scatter kernels for a staging region to scatter_stream.
|
||||
|
||||
May be called from the decode_thread (background). All GPU work
|
||||
runs on scatter_stream so that the decode_thread never blocks on
|
||||
the default stream (which carries the main-thread forward pass).
|
||||
"""
|
||||
from sglang.srt.disaggregation.common.staging_buffer import (
|
||||
scatter_staging_to_kv,
|
||||
)
|
||||
|
||||
k_buffers = self.kv_buffer_info["k_buffers"]
|
||||
v_buffers = self.kv_buffer_info["v_buffers"]
|
||||
page_size = self.kv_buffer_info["page_size"]
|
||||
dst_tp_rank = self.kv_manager.kv_args.engine_rank % self.decode_tp
|
||||
|
||||
device = k_buffers[0].device
|
||||
torch.cuda.set_device(device)
|
||||
|
||||
if not hasattr(self.staging_allocator, "_scatter_stream"):
|
||||
self.staging_allocator._scatter_stream = torch.cuda.Stream(device=device)
|
||||
|
||||
scatter_stream = self.staging_allocator._scatter_stream
|
||||
|
||||
staging_view = self.staging_allocator.buffer.buffer[staging_offset:]
|
||||
|
||||
req_pool_idx = decode_req.req.req_pool_idx
|
||||
token_start = page_start * page_size
|
||||
token_end = token_start + num_pages * page_size
|
||||
prefill_tp = decode_req.kv_receiver.prefill_info.attn_tp_size
|
||||
|
||||
with torch.cuda.stream(scatter_stream):
|
||||
kv_indices = self.scheduler.req_to_token_pool.req_to_token[
|
||||
req_pool_idx, token_start:token_end
|
||||
]
|
||||
if page_size > 1:
|
||||
page_idx_tensor = kv_indices[::page_size] // page_size
|
||||
else:
|
||||
page_idx_tensor = kv_indices
|
||||
|
||||
scatter_staging_to_kv(
|
||||
staging_view,
|
||||
k_buffers,
|
||||
v_buffers,
|
||||
page_idx_tensor,
|
||||
page_size,
|
||||
prefill_tp,
|
||||
self.decode_tp,
|
||||
dst_tp_rank,
|
||||
self.total_kv_heads,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def _submit_last_scatter(self, decode_req: "DecodeRequest") -> int:
|
||||
"""Submit scatter for the last chunk. Returns alloc_id >= 0, or -1."""
|
||||
receiver = decode_req.kv_receiver
|
||||
chunk_infos = getattr(receiver, "chunk_staging_infos", [])
|
||||
if not chunk_infos:
|
||||
return -1
|
||||
|
||||
last_info = chunk_infos[-1]
|
||||
alloc_id, staging_offset, _, _, last_num_pages = last_info
|
||||
if staging_offset < 0 or alloc_id < 0:
|
||||
return -1
|
||||
|
||||
seq_len = len(decode_req.req.origin_input_ids)
|
||||
ps = self.scheduler.token_to_kv_pool_allocator.page_size
|
||||
total_pages = (seq_len + ps - 1) // ps
|
||||
page_start = total_pages - last_num_pages
|
||||
|
||||
ok = self._scatter_region(
|
||||
staging_offset, page_start, last_num_pages, decode_req
|
||||
)
|
||||
return alloc_id if ok else -1
|
||||
|
||||
def _free_and_send_watermark(
|
||||
self, alloc_id: int, decode_req: "DecodeRequest"
|
||||
) -> None:
|
||||
"""Free a staging allocation and broadcast watermark to all prefills."""
|
||||
self.staging_allocator.free(alloc_id)
|
||||
post_wm = self.staging_allocator.get_watermark()
|
||||
room = decode_req.req.bootstrap_room
|
||||
wm_round, wm_tail = post_wm
|
||||
wm_round_b = str(wm_round).encode("ascii")
|
||||
wm_tail_b = str(wm_tail).encode("ascii")
|
||||
for _key, (receiver, session_id) in list(self._wm_subscribers.items()):
|
||||
sid_b = session_id.encode("ascii")
|
||||
for bootstrap_info in receiver.bootstrap_infos:
|
||||
try:
|
||||
sock, lock = receiver._connect_to_bootstrap_server(bootstrap_info)
|
||||
with lock:
|
||||
sock.send_multipart(
|
||||
[b"WATERMARK", wm_round_b, wm_tail_b, sid_b]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def is_watermark_ready(
|
||||
staging_state, session_id: str, alloc_round: int, alloc_end: int
|
||||
) -> bool:
|
||||
"""Non-blocking check: is the staging region safe to write?"""
|
||||
if alloc_round <= 0:
|
||||
return True
|
||||
prev_round = alloc_round - 1
|
||||
wm_round, wm_tail = staging_state.remote_watermarks.get(session_id, (0, 0))
|
||||
return prev_round < wm_round or (prev_round == wm_round and alloc_end <= wm_tail)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Mooncake-specific staging protocol and utilities
|
||||
# ======================================================================
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class StagingTransferInfo:
|
||||
"""Per-chunk staging allocation info attached to a TransferInfo."""
|
||||
|
||||
offsets: List[int] = dataclasses.field(default_factory=lambda: [-1])
|
||||
rounds: List[int] = dataclasses.field(default_factory=lambda: [0])
|
||||
ends: List[int] = dataclasses.field(default_factory=lambda: [-1])
|
||||
|
||||
def set_chunk(self, idx: int, offset: int, rnd: int, end: int):
|
||||
while len(self.offsets) <= idx:
|
||||
self.offsets.append(-1)
|
||||
self.rounds.append(0)
|
||||
self.ends.append(-1)
|
||||
self.offsets[idx] = offset
|
||||
self.rounds[idx] = rnd
|
||||
self.ends[idx] = end
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class StagingRegisterInfo:
|
||||
"""Staging buffer registration info attached to a KVArgsRegisterInfo."""
|
||||
|
||||
base_ptr: int = 0
|
||||
total_size: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_zmq_fields(
|
||||
cls, msg: list, msg_start_offset: int
|
||||
) -> Optional["StagingRegisterInfo"]:
|
||||
i = msg_start_offset
|
||||
base_ptr = (
|
||||
struct.unpack("Q", msg[i])[0] if len(msg) > i and len(msg[i]) == 8 else 0
|
||||
)
|
||||
total_size = (
|
||||
int(msg[i + 1].decode("ascii"))
|
||||
if len(msg) > i + 1 and len(msg[i + 1]) > 0
|
||||
else 0
|
||||
)
|
||||
if base_ptr == 0 and total_size == 0:
|
||||
return None
|
||||
return cls(base_ptr=base_ptr, total_size=total_size)
|
||||
|
||||
|
||||
class PrefillStagingStrategy:
|
||||
"""Prefill-side staging transfer: readiness check + gather-RDMA execution.
|
||||
|
||||
Encapsulates the decision logic (chunk index calculation, staging offset
|
||||
lookup, watermark readiness) and delegates actual RDMA to the kv_manager.
|
||||
"""
|
||||
|
||||
def __init__(self, kv_manager, staging_buffer):
|
||||
self.kv_manager = kv_manager
|
||||
self.staging_buffer = staging_buffer
|
||||
page_size = kv_manager.kv_buffer_tensors["page_size"]
|
||||
cps = kv_manager.server_args.chunked_prefill_size or 8192
|
||||
self.full_chunk_pages = max(1, cps // page_size)
|
||||
|
||||
def check_ready(
|
||||
self,
|
||||
req,
|
||||
kv_chunk_index_start: int,
|
||||
num_chunk_pages: int,
|
||||
) -> Tuple[bool, int, int, int, int]:
|
||||
"""Check if staging offset and watermark are ready for this chunk.
|
||||
|
||||
Returns (ready, chunk_idx, offset, round, end).
|
||||
offset == ALLOC_OVERSIZED means permanent failure (fall back to slice).
|
||||
offset == -1 means allocation pending (re-enqueue).
|
||||
"""
|
||||
from sglang.srt.disaggregation.common.staging_buffer import StagingAllocator
|
||||
|
||||
chunk_idx = (
|
||||
kv_chunk_index_start // self.full_chunk_pages
|
||||
if self.full_chunk_pages > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
stg = req.staging
|
||||
if stg is None or chunk_idx >= len(stg.offsets):
|
||||
return (False, chunk_idx, -1, 0, -1)
|
||||
|
||||
c_offset = stg.offsets[chunk_idx]
|
||||
if c_offset == StagingAllocator.ALLOC_OVERSIZED:
|
||||
return (False, chunk_idx, StagingAllocator.ALLOC_OVERSIZED, 0, -1)
|
||||
if c_offset < 0:
|
||||
return (False, chunk_idx, -1, 0, -1)
|
||||
|
||||
c_round = stg.rounds[chunk_idx]
|
||||
c_end = stg.ends[chunk_idx]
|
||||
|
||||
if not self.kv_manager._is_watermark_ready(
|
||||
req.mooncake_session_id, c_round, c_end
|
||||
):
|
||||
return (False, chunk_idx, c_offset, c_round, c_end)
|
||||
|
||||
return (True, chunk_idx, c_offset, c_round, c_end)
|
||||
|
||||
def transfer(
|
||||
self,
|
||||
session_id: str,
|
||||
prefill_kv_indices,
|
||||
dst_staging_ptr: int,
|
||||
dst_staging_size: int,
|
||||
target_info,
|
||||
) -> int:
|
||||
"""Execute staged transfer (gather + RDMA).
|
||||
|
||||
Returns 0 on success, -1 to signal fallback to slice path.
|
||||
"""
|
||||
try:
|
||||
return self.kv_manager.send_kvcache_staged(
|
||||
session_id,
|
||||
prefill_kv_indices,
|
||||
dst_staging_ptr,
|
||||
dst_staging_size,
|
||||
target_info.dst_tp_rank,
|
||||
target_info.dst_attn_tp_size,
|
||||
target_info.dst_kv_item_len,
|
||||
staging_buffer=self.staging_buffer,
|
||||
)
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"[Staging] KV transfer via staging buffer failed: {e}. "
|
||||
f"session={session_id}"
|
||||
) from e
|
||||
|
||||
|
||||
def init_staging_buffers(engine, kv_args, count: int) -> list:
|
||||
"""Create prefill-side staging buffers and register them with the engine.
|
||||
|
||||
Returns list of StagingBuffer instances.
|
||||
"""
|
||||
from sglang.srt.disaggregation.common.staging_buffer import StagingBuffer
|
||||
from sglang.srt.disaggregation.mooncake.utils import (
|
||||
init_mooncake_custom_mem_pool,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
size_mb = envs.SGLANG_DISAGG_STAGING_BUFFER_SIZE_MB.get()
|
||||
size_bytes = size_mb * 1024 * 1024
|
||||
gpu_id = kv_args.gpu_id
|
||||
device = f"cuda:{gpu_id}"
|
||||
|
||||
_, custom_mem_pool, pool_type = init_mooncake_custom_mem_pool(device)
|
||||
if custom_mem_pool is None:
|
||||
logger.warning(
|
||||
"No mooncake custom mem pool available for staging buffer. "
|
||||
"NVLink transport will NOT work. Set SGLANG_MOONCAKE_CUSTOM_MEM_POOL."
|
||||
)
|
||||
|
||||
buffers = []
|
||||
for _ in range(count):
|
||||
buf = StagingBuffer(size_bytes, device, gpu_id, custom_mem_pool=custom_mem_pool)
|
||||
engine.batch_register([buf.get_ptr()], [buf.get_size()])
|
||||
buffers.append(buf)
|
||||
return buffers
|
||||
|
||||
|
||||
def init_staging_allocator(engine, kv_args):
|
||||
"""Create decode-side staging ring-buffer allocator and register with engine.
|
||||
|
||||
Returns a StagingAllocator instance.
|
||||
"""
|
||||
from sglang.srt.disaggregation.common.staging_buffer import StagingAllocator
|
||||
from sglang.srt.disaggregation.mooncake.utils import (
|
||||
init_mooncake_custom_mem_pool,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
pool_size_mb = envs.SGLANG_DISAGG_STAGING_POOL_SIZE_MB.get()
|
||||
pool_size_bytes = pool_size_mb * 1024 * 1024
|
||||
gpu_id = kv_args.gpu_id
|
||||
device = f"cuda:{gpu_id}"
|
||||
|
||||
_, custom_mem_pool, _ = init_mooncake_custom_mem_pool(device)
|
||||
allocator = StagingAllocator(pool_size_bytes, device, gpu_id, custom_mem_pool)
|
||||
engine.batch_register([allocator.get_base_ptr()], [allocator.get_total_size()])
|
||||
return allocator
|
||||
|
||||
|
||||
def handle_staging_req(
|
||||
msg,
|
||||
staging_allocator,
|
||||
kv_args,
|
||||
attn_tp_size: int,
|
||||
prefill_attn_tp_size: int,
|
||||
kv_buffer_tensors,
|
||||
room_receivers: dict,
|
||||
room_bootstrap: dict,
|
||||
):
|
||||
"""Allocate staging for a chunk on-demand and send STAGING_RSP to prefill.
|
||||
|
||||
Deduplicates: multiple prefill TP ranks requesting the same (room, chunk_idx)
|
||||
only allocate once. Sends ALLOC_OVERSIZED on permanent failure.
|
||||
"""
|
||||
from sglang.srt.disaggregation.common.staging_buffer import StagingAllocator
|
||||
|
||||
room = int(msg[1].decode("ascii"))
|
||||
chunk_idx = int(msg[2].decode("ascii"))
|
||||
chunk_num_pages = int(msg[3].decode("ascii"))
|
||||
session_id = msg[4].decode("ascii")
|
||||
|
||||
if staging_allocator is None:
|
||||
logger.warning(
|
||||
"STAGING_REQ ignored: allocator is None room=%s chunk=%s",
|
||||
room,
|
||||
chunk_idx,
|
||||
)
|
||||
return
|
||||
|
||||
receiver = room_receivers.get(room)
|
||||
if receiver is None:
|
||||
logger.warning(
|
||||
"STAGING_REQ dropped: no receiver for room=%s chunk=%s session=%s",
|
||||
room,
|
||||
chunk_idx,
|
||||
session_id,
|
||||
)
|
||||
return
|
||||
infos = getattr(receiver, "chunk_staging_infos", [])
|
||||
|
||||
if chunk_idx < len(infos) and infos[chunk_idx][0] >= 0:
|
||||
_, offset, rnd, end, _ = infos[chunk_idx]
|
||||
elif (
|
||||
chunk_idx < len(infos)
|
||||
and infos[chunk_idx][1] == StagingAllocator.ALLOC_OVERSIZED
|
||||
):
|
||||
offset, rnd, end = StagingAllocator.ALLOC_OVERSIZED, 0, -1
|
||||
else:
|
||||
from sglang.srt.disaggregation.common.staging_buffer import (
|
||||
compute_staging_layout,
|
||||
resolve_total_kv_heads,
|
||||
)
|
||||
|
||||
page_size = kv_args.page_size
|
||||
kv_item_lens = kv_args.kv_item_lens
|
||||
num_kv_layers = len(kv_item_lens) // 2
|
||||
decode_bytes_per_token = kv_item_lens[0] // page_size
|
||||
total_kv_heads = resolve_total_kv_heads(kv_args, attn_tp_size)
|
||||
dst_heads_per_rank = max(1, total_kv_heads // max(1, attn_tp_size))
|
||||
bytes_per_head_per_token = decode_bytes_per_token // dst_heads_per_rank
|
||||
dst_tp_rank = kv_args.engine_rank % max(1, attn_tp_size)
|
||||
|
||||
chunk_tokens = chunk_num_pages * page_size
|
||||
_, _, required = compute_staging_layout(
|
||||
prefill_attn_tp_size,
|
||||
attn_tp_size,
|
||||
dst_tp_rank,
|
||||
total_kv_heads,
|
||||
chunk_tokens,
|
||||
bytes_per_head_per_token,
|
||||
num_kv_layers,
|
||||
)
|
||||
result = staging_allocator.assign(required)
|
||||
if result is None:
|
||||
logger.error(
|
||||
"[STAGING_REQ] alloc failed room=%s chunk=%d (need %d bytes, "
|
||||
"buffer total=%d bytes). Increase SGLANG_DISAGG_STAGING_POOL_SIZE_MB.",
|
||||
room,
|
||||
chunk_idx,
|
||||
required,
|
||||
staging_allocator.total_size,
|
||||
)
|
||||
offset, rnd, end = StagingAllocator.ALLOC_OVERSIZED, 0, -1
|
||||
while len(infos) <= chunk_idx:
|
||||
infos.append((-1, -1, 0, -1, 0))
|
||||
infos[chunk_idx] = (
|
||||
-1,
|
||||
StagingAllocator.ALLOC_OVERSIZED,
|
||||
0,
|
||||
-1,
|
||||
chunk_num_pages,
|
||||
)
|
||||
else:
|
||||
alloc_id, offset, rnd = result
|
||||
end = offset + required
|
||||
while len(infos) <= chunk_idx:
|
||||
infos.append((-1, -1, 0, -1, 0))
|
||||
infos[chunk_idx] = (alloc_id, offset, rnd, end, chunk_num_pages)
|
||||
|
||||
bootstrap_infos = room_bootstrap.get(room)
|
||||
if bootstrap_infos:
|
||||
for bi in bootstrap_infos:
|
||||
try:
|
||||
sock, lock = receiver._connect_to_bootstrap_server(bi)
|
||||
with lock:
|
||||
sock.send_multipart(
|
||||
[
|
||||
b"STAGING_RSP",
|
||||
str(room).encode("ascii"),
|
||||
str(chunk_idx).encode("ascii"),
|
||||
str(offset).encode("ascii"),
|
||||
str(rnd).encode("ascii"),
|
||||
str(end).encode("ascii"),
|
||||
session_id.encode("ascii"),
|
||||
]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def prefetch_staging_reqs(
|
||||
room: int,
|
||||
transfer_infos: dict,
|
||||
kv_buffer_tensors: dict,
|
||||
chunked_prefill_size: int,
|
||||
staging_requested: set,
|
||||
prefetch_sockets: dict,
|
||||
) -> None:
|
||||
"""Send STAGING_REQ for all chunks before the prefill forward starts.
|
||||
|
||||
Called from the scheduler right after batch formation, so that decode
|
||||
allocates staging during the GPU forward pass.
|
||||
"""
|
||||
import zmq
|
||||
|
||||
from sglang.srt.utils.network import NetworkAddress
|
||||
|
||||
page_size = kv_buffer_tensors["page_size"]
|
||||
cps = chunked_prefill_size or 8192
|
||||
full_chunk_pages = max(1, cps // page_size)
|
||||
|
||||
for session_id, tinfo in transfer_infos[room].items():
|
||||
if tinfo.is_dummy:
|
||||
continue
|
||||
total_pages = len(tinfo.dst_kv_indices)
|
||||
if total_pages == 0:
|
||||
continue
|
||||
num_chunks = (total_pages + full_chunk_pages - 1) // full_chunk_pages
|
||||
|
||||
for chunk_idx in range(num_chunks):
|
||||
stg_key = (room, chunk_idx, session_id)
|
||||
if stg_key in staging_requested:
|
||||
continue
|
||||
staging_requested.add(stg_key)
|
||||
|
||||
remaining = total_pages - chunk_idx * full_chunk_pages
|
||||
chunk_pages = min(full_chunk_pages, remaining)
|
||||
try:
|
||||
na = NetworkAddress(tinfo.endpoint, tinfo.dst_port)
|
||||
ep = na.to_tcp()
|
||||
if ep not in prefetch_sockets:
|
||||
sock = zmq.Context().socket(zmq.PUSH)
|
||||
if na.is_ipv6:
|
||||
sock.setsockopt(zmq.IPV6, 1)
|
||||
sock.connect(ep)
|
||||
prefetch_sockets[ep] = sock
|
||||
prefetch_sockets[ep].send_multipart(
|
||||
[
|
||||
b"STAGING_REQ",
|
||||
str(room).encode("ascii"),
|
||||
str(chunk_idx).encode("ascii"),
|
||||
str(chunk_pages).encode("ascii"),
|
||||
session_id.encode("ascii"),
|
||||
]
|
||||
)
|
||||
except Exception:
|
||||
staging_requested.discard(stg_key)
|
||||
@@ -45,6 +45,7 @@ from sglang.srt.disaggregation.utils import (
|
||||
is_mla_backend,
|
||||
kv_to_page_indices,
|
||||
poll_and_all_reduce,
|
||||
poll_and_all_reduce_with_staging,
|
||||
prepare_abort,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
@@ -290,7 +291,10 @@ class DecodePreallocQueue:
|
||||
self._max_ensure_retries: int = 15 # scheduling cycles
|
||||
self._ensure_last_attempt_time: Dict[str, float] = {}
|
||||
self._ensure_retry_interval: float = 1.0 # seconds
|
||||
self.enable_staging = envs.SGLANG_DISAGG_STAGING_BUFFER.get()
|
||||
self.kv_manager = self._init_kv_manager()
|
||||
if self.enable_staging:
|
||||
self.transfer_queue._init_staging_handler(self.kv_manager)
|
||||
|
||||
if self.scheduler.tp_worker.is_hybrid_swa:
|
||||
# FIXME: current SWA allocation allocate full kv cache size in prefill
|
||||
@@ -366,6 +370,21 @@ class DecodePreallocQueue:
|
||||
self.scheduler.server_args,
|
||||
self.is_mla_backend,
|
||||
)
|
||||
# Staging buffer setup (only when heterogeneous TP staging is enabled)
|
||||
if self.enable_staging and not self.is_mla_backend:
|
||||
kv_pool_for_heads = self.token_to_kv_pool
|
||||
if hasattr(kv_pool_for_heads, "full_kv_pool"):
|
||||
kv_pool_for_heads = kv_pool_for_heads.full_kv_pool
|
||||
per_rank_kv_heads = getattr(kv_pool_for_heads, "head_num", 0)
|
||||
if per_rank_kv_heads > 0:
|
||||
kv_args.kv_head_num = per_rank_kv_heads
|
||||
kv_args.total_kv_head_num = per_rank_kv_heads * attn_tp_size
|
||||
if hasattr(kv_manager, "set_kv_buffer_tensors"):
|
||||
kv_pool = kv_pool_for_heads
|
||||
if hasattr(kv_pool, "k_buffer") and hasattr(kv_pool, "v_buffer"):
|
||||
kv_manager.set_kv_buffer_tensors(
|
||||
kv_pool.k_buffer, kv_pool.v_buffer, kv_pool.page_size
|
||||
)
|
||||
return kv_manager
|
||||
|
||||
def add(self, req: Req, is_retracted: bool = False) -> None:
|
||||
@@ -737,6 +756,13 @@ class DecodePreallocQueue:
|
||||
decode_req.kv_receiver.send_metadata(
|
||||
page_indices, decode_req.metadata_buffer_index, state_indices
|
||||
)
|
||||
if (
|
||||
self.transfer_queue.enable_staging
|
||||
and decode_req.kv_receiver.require_staging
|
||||
):
|
||||
self.transfer_queue.staging_handler.register_decode_req(
|
||||
decode_req.req.bootstrap_room, decode_req
|
||||
)
|
||||
preallocated_reqs.append(decode_req)
|
||||
indices_to_remove.add(i)
|
||||
decode_req.req.time_stats.set_decode_transfer_queue_entry_time()
|
||||
@@ -863,12 +889,18 @@ class DecodeTransferQueue:
|
||||
self.scheduler = scheduler
|
||||
self.tree_cache = tree_cache
|
||||
self.spec_algorithm = scheduler.spec_algorithm
|
||||
self.enable_staging = envs.SGLANG_DISAGG_STAGING_BUFFER.get()
|
||||
self.staging_handler = None
|
||||
|
||||
def add(self, decode_req: DecodeRequest) -> None:
|
||||
self.queue.append(decode_req)
|
||||
|
||||
def extend(self, decode_reqs: List[DecodeRequest]) -> None:
|
||||
self.queue.extend(decode_reqs)
|
||||
if self.enable_staging:
|
||||
for dr in decode_reqs:
|
||||
if dr.kv_receiver.require_staging:
|
||||
self.staging_handler.register_decode_req(dr.req.bootstrap_room, dr)
|
||||
|
||||
def _commit_transfer_to_req(self, decode_req: DecodeRequest) -> bool:
|
||||
"""
|
||||
@@ -951,18 +983,39 @@ class DecodeTransferQueue:
|
||||
decode_req.req.time_stats.set_wait_queue_entry_time()
|
||||
return True
|
||||
|
||||
def _poll_with_staging(self) -> list:
|
||||
return poll_and_all_reduce_with_staging(
|
||||
self.queue, self.staging_handler, self.gloo_group
|
||||
)
|
||||
|
||||
def _init_staging_handler(self, kv_manager):
|
||||
"""Create staging handler from kv_manager. Must be called exactly once."""
|
||||
from sglang.srt.disaggregation.common.staging_handler import (
|
||||
DecodeStagingHandler,
|
||||
)
|
||||
|
||||
self.staging_handler = DecodeStagingHandler.create(
|
||||
kv_manager, self.scheduler, self.tp_rank
|
||||
)
|
||||
kv_manager._staging_handler = self.staging_handler
|
||||
|
||||
def pop_transferred(self, rids_to_check: Optional[List[str]] = None) -> List[Req]:
|
||||
if not self.queue:
|
||||
return []
|
||||
polls = poll_and_all_reduce(
|
||||
[decode_req.kv_receiver for decode_req in self.queue], self.gloo_group
|
||||
)
|
||||
|
||||
if self.enable_staging:
|
||||
polls = self._poll_with_staging()
|
||||
else:
|
||||
polls = poll_and_all_reduce(
|
||||
[dr.kv_receiver for dr in self.queue], self.gloo_group
|
||||
)
|
||||
|
||||
transferred_reqs = []
|
||||
indices_to_remove = set()
|
||||
for i, (decode_req, poll) in enumerate(zip(self.queue, polls)):
|
||||
if rids_to_check is not None and decode_req.req.rid not in rids_to_check:
|
||||
continue
|
||||
|
||||
if poll == KVPoll.Failed:
|
||||
error_message = f"Decode transfer failed for request rank={self.tp_rank} {decode_req.req.rid=} {decode_req.req.bootstrap_room=}"
|
||||
try:
|
||||
@@ -1010,6 +1063,12 @@ class DecodeTransferQueue:
|
||||
raise ValueError(f"Unexpected poll case: {poll}")
|
||||
|
||||
for i in indices_to_remove:
|
||||
if self.enable_staging and self.staging_handler.is_staging_room(
|
||||
self.queue[i].req.bootstrap_room
|
||||
):
|
||||
self.staging_handler.unregister_decode_req(
|
||||
self.queue[i].req.bootstrap_room
|
||||
)
|
||||
idx = self.queue[i].metadata_buffer_index
|
||||
assert idx != -1
|
||||
self.req_to_metadata_buffer_idx_allocator.free(idx)
|
||||
|
||||
@@ -61,6 +61,14 @@ class TransferKVChunk:
|
||||
state_indices: Optional[List[int]]
|
||||
|
||||
|
||||
from sglang.srt.disaggregation.common.staging_handler import (
|
||||
DecodeStagingContext,
|
||||
PrefillStagingContext,
|
||||
StagingRegisterInfo,
|
||||
StagingTransferInfo,
|
||||
)
|
||||
|
||||
|
||||
# decode
|
||||
@dataclasses.dataclass
|
||||
class TransferInfo:
|
||||
@@ -73,6 +81,7 @@ class TransferInfo:
|
||||
dst_state_indices: List[int]
|
||||
required_dst_info_num: int
|
||||
is_dummy: bool
|
||||
staging: Optional[StagingTransferInfo] = None
|
||||
|
||||
@classmethod
|
||||
def from_zmq(cls, msg: List[bytes]):
|
||||
@@ -118,6 +127,7 @@ class KVArgsRegisterInfo:
|
||||
# for mamba state different tp slice transfer
|
||||
dst_state_item_lens: list[int]
|
||||
dst_state_dim_per_tensor: list[int]
|
||||
staging: Optional[StagingRegisterInfo] = None
|
||||
|
||||
@classmethod
|
||||
def from_zmq(cls, msg: List[bytes]):
|
||||
@@ -142,6 +152,7 @@ class KVArgsRegisterInfo:
|
||||
if len(msg) > 11 and len(msg[11]) > 0
|
||||
else []
|
||||
),
|
||||
staging=StagingRegisterInfo.from_zmq_fields(msg, 12),
|
||||
)
|
||||
|
||||
|
||||
@@ -178,6 +189,7 @@ class MooncakeKVManager(CommonKVManager):
|
||||
super().__init__(args, disaggregation_mode, server_args, is_mla_backend)
|
||||
self.init_engine()
|
||||
self.register_buffer_to_engine()
|
||||
self.enable_staging = envs.SGLANG_DISAGG_STAGING_BUFFER.get()
|
||||
if self.disaggregation_mode == DisaggregationMode.PREFILL:
|
||||
self.start_prefill_thread()
|
||||
self.session_failures = defaultdict(int)
|
||||
@@ -204,14 +216,34 @@ class MooncakeKVManager(CommonKVManager):
|
||||
)
|
||||
for _ in range(transfer_queue_size)
|
||||
]
|
||||
for queue, executor in zip(self.transfer_queues, self.executors):
|
||||
threading.Thread(
|
||||
target=self.transfer_worker, args=(queue, executor), daemon=True
|
||||
).start()
|
||||
self.enable_custom_mem_pool, self.custom_mem_pool_type = (
|
||||
check_mooncake_custom_mem_pool_enabled()
|
||||
)
|
||||
self._staging_ctx = PrefillStagingContext() if self.enable_staging else None
|
||||
if self.enable_staging:
|
||||
self._init_staging_buffers(len(self.transfer_queues))
|
||||
for i, (queue, executor) in enumerate(
|
||||
zip(self.transfer_queues, self.executors)
|
||||
):
|
||||
threading.Thread(
|
||||
target=self.transfer_worker,
|
||||
args=(
|
||||
queue,
|
||||
executor,
|
||||
(
|
||||
self._staging_ctx.buffers[i]
|
||||
if self.enable_staging and self._staging_ctx.buffers
|
||||
else None
|
||||
),
|
||||
),
|
||||
daemon=True,
|
||||
).start()
|
||||
elif self.disaggregation_mode == DisaggregationMode.DECODE:
|
||||
self._staging_ctx = DecodeStagingContext() if self.enable_staging else None
|
||||
if self.enable_staging:
|
||||
self._init_staging_allocator()
|
||||
self._staging_handler = None
|
||||
self._chunk_writer_counts: dict = defaultdict(lambda: defaultdict(list))
|
||||
self.start_decode_thread()
|
||||
|
||||
def init_engine(self):
|
||||
@@ -236,6 +268,297 @@ class MooncakeKVManager(CommonKVManager):
|
||||
self.kv_args.state_data_ptrs, self.kv_args.state_data_lens
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Staging buffer methods (all delegate to staging_handler.py)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def register_staging_room_bootstrap(self, room, bootstrap_infos, receiver):
|
||||
self._staging_ctx.room_bootstrap[room] = bootstrap_infos
|
||||
self._staging_ctx.room_receivers[room] = receiver
|
||||
|
||||
def set_kv_buffer_tensors(self, k_buffers: list, v_buffers: list, page_size: int):
|
||||
self.kv_buffer_tensors = {
|
||||
"k_buffers": k_buffers,
|
||||
"v_buffers": v_buffers,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
def _init_staging_buffers(self, count: int):
|
||||
from sglang.srt.disaggregation.common.staging_handler import (
|
||||
init_staging_buffers,
|
||||
)
|
||||
|
||||
self._staging_ctx.buffers = init_staging_buffers(
|
||||
self.engine, self.kv_args, count
|
||||
)
|
||||
self.kv_buffer_tensors = None
|
||||
|
||||
def _init_staging_allocator(self):
|
||||
from sglang.srt.disaggregation.common.staging_handler import (
|
||||
init_staging_allocator,
|
||||
)
|
||||
|
||||
self._staging_ctx.allocator = init_staging_allocator(self.engine, self.kv_args)
|
||||
self.kv_buffer_tensors = None
|
||||
|
||||
def _handle_staging_req(self, msg):
|
||||
from sglang.srt.disaggregation.common.staging_handler import (
|
||||
handle_staging_req,
|
||||
)
|
||||
|
||||
room = int(msg[1].decode("ascii"))
|
||||
session_id = msg[4].decode("ascii")
|
||||
handler = self._staging_handler
|
||||
assert (
|
||||
handler is not None
|
||||
), "STAGING_REQ received before staging handler initialized"
|
||||
decode_req = handler._room_to_decode_req.get(room)
|
||||
if decode_req is None:
|
||||
logger.warning(
|
||||
"STAGING_REQ received for unregistered room=%s, skipping",
|
||||
room,
|
||||
)
|
||||
return
|
||||
prefill_tp = decode_req.kv_receiver.prefill_info.attn_tp_size
|
||||
handle_staging_req(
|
||||
msg,
|
||||
self._staging_ctx.allocator,
|
||||
self.kv_args,
|
||||
self.attn_tp_size,
|
||||
prefill_tp,
|
||||
getattr(self, "kv_buffer_tensors", None),
|
||||
self._staging_ctx.room_receivers,
|
||||
self._staging_ctx.room_bootstrap,
|
||||
)
|
||||
|
||||
receiver = self._staging_ctx.room_receivers.get(room)
|
||||
if receiver is not None:
|
||||
handler.register_wm_subscriber(receiver, session_id)
|
||||
|
||||
def _is_watermark_ready(
|
||||
self, session_id: str, alloc_round: int, alloc_end: int
|
||||
) -> bool:
|
||||
from sglang.srt.disaggregation.common.staging_handler import (
|
||||
is_watermark_ready,
|
||||
)
|
||||
|
||||
return is_watermark_ready(self._staging_ctx, session_id, alloc_round, alloc_end)
|
||||
|
||||
def _try_create_staging_strategy(self, staging_buffer):
|
||||
if not self.enable_staging or self.kv_buffer_tensors is None:
|
||||
return None
|
||||
from sglang.srt.disaggregation.common.staging_handler import (
|
||||
PrefillStagingStrategy,
|
||||
)
|
||||
|
||||
return PrefillStagingStrategy(self, staging_buffer)
|
||||
|
||||
def _send_chunk_ready(self, req, chunk_idx, kv_chunk, prefill_unique_rank):
|
||||
"""Notify decode that a non-last staging chunk RDMA is complete."""
|
||||
try:
|
||||
na = NetworkAddress(req.endpoint, req.dst_port)
|
||||
self._connect(
|
||||
na.to_tcp(),
|
||||
is_ipv6=na.is_ipv6,
|
||||
).send_multipart(
|
||||
[
|
||||
b"CHUNK_READY",
|
||||
str(req.room).encode("ascii"),
|
||||
str(chunk_idx).encode("ascii"),
|
||||
str(kv_chunk.index_slice.start).encode("ascii"),
|
||||
str(len(kv_chunk.prefill_kv_indices)).encode("ascii"),
|
||||
req.mooncake_session_id.encode("ascii"),
|
||||
str(prefill_unique_rank).encode("ascii"),
|
||||
]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _do_staging_transfer(
|
||||
self,
|
||||
staging_strategy,
|
||||
kv_chunk,
|
||||
req,
|
||||
target_info,
|
||||
chunked_dst_kv_indice,
|
||||
executor,
|
||||
queue,
|
||||
prefill_unique_rank,
|
||||
):
|
||||
"""Execute staging transfer for one chunk. Returns (ret, deferred).
|
||||
|
||||
Handles readiness check, transfer, fallback, and CHUNK_READY notification.
|
||||
deferred=True means caller should re-enqueue and break.
|
||||
"""
|
||||
_tp = self.attn_tp_rank
|
||||
ready, chunk_idx, c_offset, _, _ = staging_strategy.check_ready(
|
||||
req,
|
||||
kv_chunk.index_slice.start,
|
||||
len(kv_chunk.prefill_kv_indices),
|
||||
)
|
||||
if not ready:
|
||||
from sglang.srt.disaggregation.common.staging_buffer import StagingAllocator
|
||||
|
||||
if c_offset == StagingAllocator.ALLOC_OVERSIZED:
|
||||
raise RuntimeError(
|
||||
f"[Staging] Chunk staging allocation permanently failed: "
|
||||
f"chunk exceeds ring buffer total size (room={kv_chunk.room}). "
|
||||
f"Increase SGLANG_DISAGG_STAGING_POOL_SIZE_MB."
|
||||
)
|
||||
queue.put(kv_chunk)
|
||||
return (-1, True)
|
||||
|
||||
ret = staging_strategy.transfer(
|
||||
req.mooncake_session_id,
|
||||
kv_chunk.prefill_kv_indices,
|
||||
target_info.staging.base_ptr + c_offset,
|
||||
target_info.staging.total_size - c_offset,
|
||||
target_info,
|
||||
)
|
||||
if ret == -1:
|
||||
logger.warning(
|
||||
f"[Staging][tp{_tp}] Falling back to per-token slice path "
|
||||
f"(room={kv_chunk.room})"
|
||||
)
|
||||
ret = self.send_kvcache_slice(
|
||||
req.mooncake_session_id,
|
||||
kv_chunk.prefill_kv_indices,
|
||||
target_info.dst_kv_ptrs,
|
||||
chunked_dst_kv_indice,
|
||||
target_info.dst_tp_rank,
|
||||
target_info.dst_attn_tp_size,
|
||||
target_info.dst_kv_item_len,
|
||||
executor,
|
||||
)
|
||||
elif ret == 0 and not kv_chunk.is_last_chunk:
|
||||
self._send_chunk_ready(req, chunk_idx, kv_chunk, prefill_unique_rank)
|
||||
return (ret, False)
|
||||
|
||||
def _prefetch_staging_reqs(self, room: int):
|
||||
if not self.enable_staging or self.kv_buffer_tensors is None:
|
||||
return
|
||||
|
||||
room_infos = self.transfer_infos.get(room, {})
|
||||
needs_staging = any(
|
||||
not tinfo.is_dummy
|
||||
and self.decode_kv_args_table.get(tinfo.mooncake_session_id) is not None
|
||||
and self.decode_kv_args_table[tinfo.mooncake_session_id].dst_attn_tp_size
|
||||
!= self.attn_tp_size
|
||||
for tinfo in room_infos.values()
|
||||
)
|
||||
if not needs_staging:
|
||||
return
|
||||
|
||||
from sglang.srt.disaggregation.common.staging_handler import (
|
||||
prefetch_staging_reqs,
|
||||
)
|
||||
|
||||
prefetch_staging_reqs(
|
||||
room,
|
||||
self.transfer_infos,
|
||||
self.kv_buffer_tensors,
|
||||
self.server_args.chunked_prefill_size,
|
||||
self._staging_ctx.prefetch_requested,
|
||||
self._staging_ctx.prefetch_sockets,
|
||||
)
|
||||
|
||||
def send_kvcache_staged(
|
||||
self,
|
||||
mooncake_session_id: str,
|
||||
prefill_kv_indices: npt.NDArray[np.int32],
|
||||
dst_staging_ptr: int,
|
||||
dst_staging_size: int,
|
||||
dst_tp_rank: int,
|
||||
dst_attn_tp_size: int,
|
||||
dst_kv_item_len: int,
|
||||
staging_buffer=None,
|
||||
) -> int:
|
||||
"""Transfer KV cache via staging buffers (gather -> bulk RDMA -> scatter on decode)."""
|
||||
from sglang.srt.disaggregation.common.staging_buffer import (
|
||||
compute_head_slice_params,
|
||||
compute_staging_layout,
|
||||
resolve_total_kv_heads,
|
||||
)
|
||||
|
||||
if self.kv_buffer_tensors is None or staging_buffer is None:
|
||||
return -1
|
||||
|
||||
k_buffers = self.kv_buffer_tensors["k_buffers"]
|
||||
v_buffers = self.kv_buffer_tensors["v_buffers"]
|
||||
page_size = self.kv_buffer_tensors["page_size"]
|
||||
num_layers = len(k_buffers)
|
||||
head_dim = k_buffers[0].shape[-1]
|
||||
dtype_size = k_buffers[0].element_size()
|
||||
|
||||
total_kv_heads = resolve_total_kv_heads(self.kv_args, self.attn_tp_size)
|
||||
|
||||
local_tp_rank = self.kv_args.engine_rank % self.attn_tp_size
|
||||
src_head_start, num_heads_to_send, _, _ = compute_head_slice_params(
|
||||
self.attn_tp_size,
|
||||
dst_attn_tp_size,
|
||||
local_tp_rank,
|
||||
dst_tp_rank,
|
||||
total_kv_heads,
|
||||
)
|
||||
|
||||
num_tokens = len(prefill_kv_indices) * page_size
|
||||
per_layer_bytes = num_tokens * num_heads_to_send * head_dim * dtype_size
|
||||
per_rank_bytes = per_layer_bytes * num_layers * 2
|
||||
|
||||
num_writers, writer_rank_bytes, total_staging_needed = compute_staging_layout(
|
||||
self.attn_tp_size,
|
||||
dst_attn_tp_size,
|
||||
dst_tp_rank,
|
||||
total_kv_heads,
|
||||
num_tokens,
|
||||
head_dim * dtype_size,
|
||||
num_layers,
|
||||
)
|
||||
writer_idx = local_tp_rank % num_writers if num_writers > 1 else 0
|
||||
rank_offset = sum(writer_rank_bytes[:writer_idx])
|
||||
|
||||
if not staging_buffer.fits(per_rank_bytes):
|
||||
logger.warning(
|
||||
f"Prefill staging too small for {per_rank_bytes} bytes, falling back"
|
||||
)
|
||||
return -1
|
||||
if dst_staging_size < total_staging_needed:
|
||||
logger.warning(
|
||||
f"Decode staging too small: need {total_staging_needed} bytes "
|
||||
f"({num_writers if self.attn_tp_size > dst_attn_tp_size else 1} writers "
|
||||
f"x {per_rank_bytes} bytes/rank), have {dst_staging_size}, falling back"
|
||||
)
|
||||
return -1
|
||||
|
||||
from sglang.srt.disaggregation.common.staging_buffer import (
|
||||
gather_all_layers_to_staging,
|
||||
)
|
||||
|
||||
gather_all_layers_to_staging(
|
||||
k_buffers,
|
||||
v_buffers,
|
||||
prefill_kv_indices,
|
||||
staging_buffer,
|
||||
src_head_start,
|
||||
num_heads_to_send,
|
||||
page_size,
|
||||
self.kv_args.gpu_id,
|
||||
)
|
||||
|
||||
dst_write_ptr = dst_staging_ptr + rank_offset
|
||||
ret = self._transfer_data(
|
||||
mooncake_session_id,
|
||||
[(staging_buffer.get_ptr(), dst_write_ptr, per_rank_bytes)],
|
||||
)
|
||||
if ret != 0:
|
||||
raise RuntimeError(
|
||||
f"[Staging] Bulk RDMA transfer failed with ret={ret}. "
|
||||
f"src_ptr=0x{staging_buffer.get_ptr():x}, "
|
||||
f"dst_ptr=0x{dst_write_ptr:x}, size={per_rank_bytes}. "
|
||||
f"The decode staging buffer may not be properly registered."
|
||||
)
|
||||
return ret
|
||||
|
||||
def _transfer_data(self, mooncake_session_id, transfer_blocks):
|
||||
if not transfer_blocks:
|
||||
return 0
|
||||
@@ -770,11 +1093,22 @@ class MooncakeKVManager(CommonKVManager):
|
||||
)
|
||||
|
||||
def transfer_worker(
|
||||
self, queue: FastQueue, executor: concurrent.futures.ThreadPoolExecutor
|
||||
self,
|
||||
queue: FastQueue,
|
||||
executor: concurrent.futures.ThreadPoolExecutor,
|
||||
staging_buffer=None,
|
||||
):
|
||||
staging_strategy = None
|
||||
|
||||
while True:
|
||||
try:
|
||||
kv_chunk: TransferKVChunk = queue.get()
|
||||
if (
|
||||
self.enable_staging
|
||||
and staging_strategy is None
|
||||
and staging_buffer is not None
|
||||
):
|
||||
staging_strategy = self._try_create_staging_strategy(staging_buffer)
|
||||
reqs_to_be_processed = (
|
||||
self.transfer_infos[kv_chunk.room].values()
|
||||
if kv_chunk.room in self.transfer_infos
|
||||
@@ -788,6 +1122,9 @@ class MooncakeKVManager(CommonKVManager):
|
||||
+ self.pp_rank * self.attn_cp_size
|
||||
+ self.attn_cp_rank
|
||||
)
|
||||
# When staging transfer is not yet ready (watermark/allocation pending),
|
||||
# the chunk is re-enqueued and we break out of the req loop to retry later.
|
||||
staging_deferred = False
|
||||
for req in reqs_to_be_processed:
|
||||
if not req.is_dummy:
|
||||
# Early exit if the request has failed
|
||||
@@ -835,6 +1172,25 @@ class MooncakeKVManager(CommonKVManager):
|
||||
chunked_dst_kv_indice,
|
||||
executor,
|
||||
)
|
||||
elif (
|
||||
self.enable_staging
|
||||
and staging_strategy is not None
|
||||
and target_rank_registration_info.staging is not None
|
||||
):
|
||||
ret, deferred = self._do_staging_transfer(
|
||||
staging_strategy,
|
||||
kv_chunk,
|
||||
req,
|
||||
target_rank_registration_info,
|
||||
chunked_dst_kv_indice,
|
||||
executor,
|
||||
queue,
|
||||
prefill_unique_rank,
|
||||
)
|
||||
if deferred:
|
||||
staging_deferred = True
|
||||
# Chunk re-enqueued; stop processing remaining reqs for this chunk
|
||||
break
|
||||
else:
|
||||
ret = self.send_kvcache_slice(
|
||||
req.mooncake_session_id,
|
||||
@@ -909,6 +1265,9 @@ class MooncakeKVManager(CommonKVManager):
|
||||
if kv_chunk.is_last_chunk and req.room in self.request_status:
|
||||
self.update_status(req.room, KVPoll.Success)
|
||||
|
||||
if staging_deferred:
|
||||
continue
|
||||
|
||||
if (
|
||||
kv_chunk.room not in self.request_status
|
||||
or self.check_status(kv_chunk.room) == KVPoll.Success
|
||||
@@ -929,6 +1288,50 @@ class MooncakeKVManager(CommonKVManager):
|
||||
while True:
|
||||
waiting_req_bytes = self.server_socket.recv_multipart()
|
||||
room = waiting_req_bytes[0].decode("ascii")
|
||||
# Staging: decode reports consumption watermark back to prefill
|
||||
if room == "WATERMARK":
|
||||
wm_round = int(waiting_req_bytes[1].decode("ascii"))
|
||||
wm_tail = int(waiting_req_bytes[2].decode("ascii"))
|
||||
wm_session = (
|
||||
waiting_req_bytes[3].decode("ascii")
|
||||
if len(waiting_req_bytes) > 3
|
||||
else ""
|
||||
)
|
||||
with self._staging_ctx.watermark_cv:
|
||||
prev = self._staging_ctx.remote_watermarks.get(
|
||||
wm_session, (0, 0)
|
||||
)
|
||||
if (wm_round, wm_tail) > prev:
|
||||
self._staging_ctx.remote_watermarks[wm_session] = (
|
||||
wm_round,
|
||||
wm_tail,
|
||||
)
|
||||
self._staging_ctx.watermark_cv.notify_all()
|
||||
continue
|
||||
# Staging: decode replies with allocated staging offset
|
||||
if room == "STAGING_RSP":
|
||||
stg_room = int(waiting_req_bytes[1].decode("ascii"))
|
||||
stg_chunk_idx = int(waiting_req_bytes[2].decode("ascii"))
|
||||
stg_offset = int(waiting_req_bytes[3].decode("ascii"))
|
||||
stg_round = int(waiting_req_bytes[4].decode("ascii"))
|
||||
stg_end = int(waiting_req_bytes[5].decode("ascii"))
|
||||
stg_session = waiting_req_bytes[6].decode("ascii")
|
||||
room_infos = self.transfer_infos.get(stg_room, {})
|
||||
tinfo = room_infos.get(stg_session)
|
||||
if tinfo is not None:
|
||||
if tinfo.staging is None:
|
||||
tinfo.staging = StagingTransferInfo()
|
||||
tinfo.staging.set_chunk(
|
||||
stg_chunk_idx, stg_offset, stg_round, stg_end
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"STAGING_RSP RECV but tinfo=None room=%s chunk=%d session=%s",
|
||||
stg_room,
|
||||
stg_chunk_idx,
|
||||
stg_session,
|
||||
)
|
||||
continue
|
||||
mooncake_session_id = waiting_req_bytes[3].decode("ascii")
|
||||
if room == "None":
|
||||
self.decode_kv_args_table[mooncake_session_id] = (
|
||||
@@ -966,6 +1369,42 @@ class MooncakeKVManager(CommonKVManager):
|
||||
self._handle_aux_data(msg)
|
||||
continue
|
||||
|
||||
# Staging: prefill notifies a chunk written to staging buffer
|
||||
if msg[0] == b"CHUNK_READY":
|
||||
room = int(msg[1].decode("ascii"))
|
||||
chunk_idx = int(msg[2].decode("ascii"))
|
||||
page_start = int(msg[3].decode("ascii"))
|
||||
num_pages = int(msg[4].decode("ascii"))
|
||||
session_id = msg[5].decode("ascii")
|
||||
self._chunk_writer_counts[room][chunk_idx].append(
|
||||
(page_start, num_pages, session_id)
|
||||
)
|
||||
handler = self._staging_handler
|
||||
assert (
|
||||
handler is not None
|
||||
), "CHUNK_READY received before staging handler initialized"
|
||||
writers_arrived = len(self._chunk_writer_counts[room][chunk_idx])
|
||||
decode_req = handler._room_to_decode_req.get(room)
|
||||
if decode_req is None:
|
||||
logger.warning(
|
||||
"CHUNK_READY received for unregistered room=%s chunk=%d, skipping",
|
||||
room,
|
||||
chunk_idx,
|
||||
)
|
||||
continue
|
||||
num_writers = handler.num_writers_for(decode_req)
|
||||
if writers_arrived >= num_writers:
|
||||
handler.submit_chunk_scatter(
|
||||
room, chunk_idx, page_start, num_pages
|
||||
)
|
||||
del self._chunk_writer_counts[room][chunk_idx]
|
||||
continue
|
||||
|
||||
# Staging: prefill pre-requests staging allocation before forward
|
||||
if msg[0] == b"STAGING_REQ":
|
||||
self._handle_staging_req(msg)
|
||||
continue
|
||||
|
||||
bootstrap_room, status, prefill_rank = msg
|
||||
status = int(status.decode("ascii"))
|
||||
bootstrap_room = int(bootstrap_room.decode("ascii"))
|
||||
@@ -981,6 +1420,11 @@ class MooncakeKVManager(CommonKVManager):
|
||||
self.prefill_response_tracker[bootstrap_room]
|
||||
)
|
||||
if arrived_response_num == expected_response_num:
|
||||
if self.enable_staging:
|
||||
handler = self._staging_handler
|
||||
if handler.is_staging_room(bootstrap_room):
|
||||
handler.submit_last_scatter_async(bootstrap_room)
|
||||
self._chunk_writer_counts.pop(bootstrap_room, None)
|
||||
self.update_status(bootstrap_room, KVPoll.Success)
|
||||
elif status == KVPoll.Failed:
|
||||
self.record_failure(
|
||||
@@ -1272,6 +1716,17 @@ class MooncakeKVReceiver(CommonKVReceiver):
|
||||
dst_attn_tp_size = str(self.kv_mgr.attn_tp_size).encode("ascii")
|
||||
dst_kv_item_len = str(kv_item_len).encode("ascii")
|
||||
|
||||
if (
|
||||
self.kv_mgr.enable_staging
|
||||
and self.kv_mgr._staging_ctx.allocator is not None
|
||||
):
|
||||
_alloc = self.kv_mgr._staging_ctx.allocator
|
||||
packed_staging_base_ptr = struct.pack("Q", _alloc.get_base_ptr())
|
||||
staging_total_size_str = str(_alloc.get_total_size()).encode("ascii")
|
||||
else:
|
||||
packed_staging_base_ptr = b""
|
||||
staging_total_size_str = b""
|
||||
|
||||
sock, lock = self._connect_to_bootstrap_server(bootstrap_info)
|
||||
with lock:
|
||||
sock.send_multipart(
|
||||
@@ -1288,6 +1743,8 @@ class MooncakeKVReceiver(CommonKVReceiver):
|
||||
dst_kv_item_len,
|
||||
packed_state_item_lens,
|
||||
packed_state_dim_per_tensor,
|
||||
packed_staging_base_ptr,
|
||||
staging_total_size_str,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1311,6 +1768,15 @@ class MooncakeKVReceiver(CommonKVReceiver):
|
||||
self.kv_mgr.update_status(self.bootstrap_room, KVPoll.Failed)
|
||||
return
|
||||
|
||||
if (
|
||||
self.kv_mgr.enable_staging
|
||||
and self.kv_mgr._staging_ctx.allocator is not None
|
||||
):
|
||||
self.chunk_staging_infos = []
|
||||
self.kv_mgr.register_staging_room_bootstrap(
|
||||
self.bootstrap_room, self.bootstrap_infos, self
|
||||
)
|
||||
|
||||
for bootstrap_info in self.bootstrap_infos:
|
||||
sock, lock = self._connect_to_bootstrap_server(bootstrap_info)
|
||||
is_dummy = bootstrap_info["is_dummy"]
|
||||
|
||||
@@ -42,6 +42,7 @@ from sglang.srt.disaggregation.utils import (
|
||||
poll_and_all_reduce_attn_cp_tp_group,
|
||||
prepare_abort,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.managers.schedule_batch import (
|
||||
FINISH_ABORT,
|
||||
FINISH_LENGTH,
|
||||
@@ -201,6 +202,21 @@ class PrefillBootstrapQueue:
|
||||
self.scheduler.server_args,
|
||||
self.is_mla_backend,
|
||||
)
|
||||
# Pass KV pool tensor refs to the manager for GPU gather (staging mode)
|
||||
if (
|
||||
envs.SGLANG_DISAGG_STAGING_BUFFER.get()
|
||||
and hasattr(kv_manager, "set_kv_buffer_tensors")
|
||||
and not self.is_mla_backend
|
||||
):
|
||||
kv_pool = self.token_to_kv_pool
|
||||
if hasattr(kv_pool, "full_kv_pool"):
|
||||
kv_pool = kv_pool.full_kv_pool
|
||||
if hasattr(kv_pool, "k_buffer") and hasattr(kv_pool, "v_buffer"):
|
||||
kv_manager.set_kv_buffer_tensors(
|
||||
kv_pool.k_buffer,
|
||||
kv_pool.v_buffer,
|
||||
kv_pool.page_size,
|
||||
)
|
||||
return kv_manager
|
||||
|
||||
def add(self, req: Req, num_kv_heads: int) -> None:
|
||||
@@ -336,6 +352,17 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
Mixin for Scheduler to handle disaggregation prefill
|
||||
"""
|
||||
|
||||
def maybe_prefetch_staging_for_batch(self: Scheduler, batch: ScheduleBatch) -> None:
|
||||
"""Pre-send STAGING_REQ so decode allocates staging during GPU forward."""
|
||||
kv_mgr = self.disagg_prefill_bootstrap_queue.kv_manager
|
||||
prefetch = getattr(kv_mgr, "_prefetch_staging_reqs", None)
|
||||
if prefetch is None:
|
||||
return
|
||||
for req in batch.reqs:
|
||||
room = getattr(req, "bootstrap_room", None)
|
||||
if room is not None and room in kv_mgr.transfer_infos:
|
||||
prefetch(room)
|
||||
|
||||
def get_next_disagg_prefill_batch_to_run(
|
||||
self: Scheduler,
|
||||
) -> Optional[ScheduleBatch]:
|
||||
@@ -356,6 +383,7 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
@torch.no_grad()
|
||||
def event_loop_normal_disagg_prefill(self: Scheduler) -> None:
|
||||
"""A normal scheduler loop for prefill worker in disaggregation mode."""
|
||||
self.enable_staging = envs.SGLANG_DISAGG_STAGING_BUFFER.get()
|
||||
|
||||
while True:
|
||||
# Receive requests
|
||||
@@ -371,6 +399,8 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
|
||||
# Launch the current batch
|
||||
if batch:
|
||||
if self.enable_staging:
|
||||
self.maybe_prefetch_staging_for_batch(batch)
|
||||
result = self.run_batch(batch)
|
||||
self.process_batch_result(batch, result)
|
||||
else:
|
||||
@@ -384,6 +414,7 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
@torch.no_grad()
|
||||
def event_loop_overlap_disagg_prefill(self: Scheduler) -> None:
|
||||
self.result_queue = deque()
|
||||
self.enable_staging = envs.SGLANG_DISAGG_STAGING_BUFFER.get()
|
||||
|
||||
while True:
|
||||
# Receive requests
|
||||
@@ -399,6 +430,8 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
|
||||
# Launch the current batch
|
||||
if batch:
|
||||
if self.enable_staging:
|
||||
self.maybe_prefetch_staging_for_batch(batch)
|
||||
batch_result = self.run_batch(batch)
|
||||
self.result_queue.append((batch.copy(), batch_result))
|
||||
else:
|
||||
|
||||
@@ -80,6 +80,30 @@ def poll_and_all_reduce_attn_cp_tp_group(
|
||||
return tensor_to_reduce.tolist()
|
||||
|
||||
|
||||
def poll_and_all_reduce_with_staging(
|
||||
decode_reqs, staging_handler, gloo_group: dist.ProcessGroup
|
||||
):
|
||||
"""Staging-aware polling: advance scatter, demote incomplete transfers, all_reduce."""
|
||||
from sglang.srt.disaggregation.base import KVPoll
|
||||
|
||||
for decode_req in decode_reqs:
|
||||
if decode_req.kv_receiver.require_staging and not staging_handler.is_done(
|
||||
decode_req
|
||||
):
|
||||
staging_handler.advance_scatter(decode_req)
|
||||
|
||||
raw_polls = [int(dr.kv_receiver.poll()) for dr in decode_reqs]
|
||||
for i, decode_req in enumerate(decode_reqs):
|
||||
if raw_polls[i] == int(KVPoll.Success):
|
||||
if decode_req.kv_receiver.require_staging and not staging_handler.is_done(
|
||||
decode_req
|
||||
):
|
||||
raw_polls[i] = int(KVPoll.Transferring)
|
||||
poll_tensor = torch.tensor(raw_polls, dtype=torch.uint8, device="cpu")
|
||||
dist.all_reduce(poll_tensor, op=dist.ReduceOp.MIN, group=gloo_group)
|
||||
return poll_tensor.tolist()
|
||||
|
||||
|
||||
#########################
|
||||
# Metadata Buffers
|
||||
#########################
|
||||
|
||||
@@ -287,6 +287,14 @@ class Envs:
|
||||
# Max fraction of cache (by token count) that can be pinned; 0 = disable pinning.
|
||||
SGLANG_HICACHE_MAX_PINNED_RATIO = EnvFloat(0.0)
|
||||
|
||||
# Staging buffer for heterogeneous TP KV transfer
|
||||
SGLANG_DISAGG_STAGING_BUFFER = EnvBool(False)
|
||||
SGLANG_DISAGG_STAGING_BUFFER_SIZE_MB = EnvInt(64)
|
||||
SGLANG_DISAGG_STAGING_POOL_SIZE_MB = EnvInt(4096)
|
||||
# TODO(yangminl): remove SGLANG_STAGING_USE_TORCH and the torch fallback in
|
||||
# staging_buffer.py once Triton kernels are fully validated in production.
|
||||
SGLANG_STAGING_USE_TORCH = EnvBool(False)
|
||||
|
||||
# Mooncake KV Transfer
|
||||
SGLANG_MOONCAKE_CUSTOM_MEM_POOL = EnvStr(None)
|
||||
ENABLE_ASCEND_TRANSFER_WITH_MOONCAKE = EnvBool(False)
|
||||
|
||||
@@ -3238,6 +3238,17 @@ class ServerArgs:
|
||||
"Cuda graph is disabled for prefill server when piecewise cuda graph is not enabled."
|
||||
)
|
||||
|
||||
if self.disaggregation_mode in ("prefill", "decode"):
|
||||
if (
|
||||
envs.SGLANG_DISAGG_STAGING_BUFFER.get()
|
||||
and self.disaggregation_transfer_backend != "mooncake"
|
||||
):
|
||||
raise ValueError(
|
||||
f"SGLANG_DISAGG_STAGING_BUFFER requires "
|
||||
f"disaggregation_transfer_backend='mooncake', "
|
||||
f"got '{self.disaggregation_transfer_backend}'."
|
||||
)
|
||||
|
||||
def _handle_encoder_disaggregation(self):
|
||||
if self.enable_prefix_mm_cache and not self.encoder_only:
|
||||
raise ValueError(
|
||||
|
||||
Reference in New Issue
Block a user