94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
695 lines
25 KiB
Python
695 lines
25 KiB
Python
from dataclasses import dataclass
|
|
from itertools import accumulate
|
|
from typing import Callable, List
|
|
|
|
import torch
|
|
import torch.nn.functional as F
|
|
|
|
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
|
use_symmetric_memory,
|
|
)
|
|
from sglang.srt.layers.dp_attention import (
|
|
attn_cp_all_gather_into_tensor,
|
|
is_allocation_symmetric,
|
|
)
|
|
from sglang.srt.layers.moe import get_moe_a2a_backend
|
|
from sglang.srt.mem_cache.memory_pool import KVWriteLoc
|
|
from sglang.srt.model_executor.forward_context import get_token_to_kv_pool
|
|
from sglang.srt.runtime_context import get_parallel, get_server_args
|
|
|
|
|
|
@dataclass
|
|
class ContextParallelMetadata:
|
|
# Layout lists have length bs * cp_segment_num (= bs * 2 * cp_size).
|
|
split_list: List[int] = None
|
|
zigzag_index: List[int] = None
|
|
cp_reverse_index: List[int] = None
|
|
reverse_split_len: List[int] = None
|
|
|
|
# Per-rank-aggregate lists have length cp_size.
|
|
# max_rank_len is a list of cp_size copies of max(per_rank_actual_token),
|
|
# kept as a list for torch.split() bucket sizes.
|
|
per_rank_actual_token: List[int] = None
|
|
max_rank_len: List[int] = None
|
|
|
|
# Per-sequence FlashAttention tensors (shape [bs] or [bs+1]).
|
|
kv_len_prev_tensor: torch.Tensor = None # [bs] int32 CUDA
|
|
kv_len_next_tensor: torch.Tensor = None # [bs] int32 CUDA
|
|
actual_seq_q_prev_tensor: torch.Tensor = None # [bs] int32 CUDA
|
|
actual_seq_q_next_tensor: torch.Tensor = None # [bs] int32 CUDA
|
|
cu_seqlens_q_prev_tensor: torch.Tensor = None # [bs+1] int32 CUDA
|
|
cu_seqlens_q_next_tensor: torch.Tensor = None # [bs+1] int32 CUDA
|
|
|
|
# Scalars derived from the per-sequence lists above.
|
|
total_q_prev_tokens: int = 0
|
|
total_q_next_tokens: int = 0
|
|
max_seqlen_q_prev: int = 0
|
|
max_seqlen_q_next: int = 0
|
|
|
|
# Per-seq CPU lists (useful for NSA indexer and diagnostics).
|
|
kv_len_prev_list: List[int] = None
|
|
kv_len_next_list: List[int] = None
|
|
actual_seq_q_prev_list: List[int] = None
|
|
actual_seq_q_next_list: List[int] = None
|
|
|
|
# Aggregate sum of extend_seq_lens across the batch.
|
|
total_seq_lens: int = 0
|
|
bs: int = 1
|
|
|
|
|
|
def is_prefill_context_parallel_enabled():
|
|
return get_server_args().enable_prefill_context_parallel
|
|
|
|
|
|
def is_prefill_cp_in_seq_split():
|
|
return (
|
|
is_prefill_context_parallel_enabled()
|
|
and get_server_args().prefill_cp_mode == "in-seq-split"
|
|
)
|
|
|
|
|
|
def get_cp_padding_align_size() -> int:
|
|
"""Token-count alignment for CP padding of global_num_tokens: 2 * cp_size
|
|
for zigzag (in-seq-split) CP, otherwise cp_size (1 when CP is off, so the
|
|
padding is a no-op; extra padding breaks EAGLE/MTP draft prefill, see
|
|
#23269). Keep prepare_mlp_sync_batch and cal_padded_tokens consistent
|
|
through this helper.
|
|
"""
|
|
from sglang.srt.layers.attention.dsa.utils import is_dsa_prefill_cp_in_seq_split
|
|
|
|
attn_cp_size = get_parallel().attn_cp_size
|
|
if is_prefill_cp_in_seq_split() or is_dsa_prefill_cp_in_seq_split():
|
|
return attn_cp_size * 2
|
|
return attn_cp_size
|
|
|
|
|
|
def is_mla_prefill_cp_enabled() -> bool:
|
|
sa = get_server_args()
|
|
return sa.enable_prefill_context_parallel and sa.use_mla_backend()
|
|
|
|
|
|
def mla_use_prefill_cp(forward_batch, mla_enable_prefill_cp=None):
|
|
if mla_enable_prefill_cp is None:
|
|
mla_enable_prefill_cp = is_mla_prefill_cp_enabled()
|
|
return (
|
|
forward_batch.attn_cp_metadata is not None
|
|
and mla_enable_prefill_cp
|
|
and forward_batch.forward_mode.is_context_parallel_extend()
|
|
)
|
|
|
|
|
|
def can_cp_split(seq_len: int, cp_size: int, forward_batch):
|
|
# Base conditions: CP must be enabled, size > 1, and this must be a
|
|
# CP-extend (prefill) step. The seq_len // (cp_size * 2) check ensures
|
|
# the load-balancing split into 2 * cp_size blocks is non-degenerate.
|
|
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
|
|
|
cur_cp_seq_len = seq_len // (cp_size * 2)
|
|
if not (
|
|
cur_cp_seq_len != 0
|
|
and cp_size > 1
|
|
# prepare_context_parallel_metadata hard-codes bs_per_cp_group = 1;
|
|
# guard explicitly to avoid silent mis-partitioning under continuous batching.
|
|
and forward_batch.forward_mode.is_context_parallel_extend()
|
|
# is_context_parallel_extend() returns True for MIXED (prefill+decode
|
|
# in one step), but the zigzag split only makes sense on pure extend.
|
|
and forward_batch.forward_mode != ForwardMode.MIXED
|
|
and is_prefill_context_parallel_enabled()
|
|
):
|
|
return False
|
|
|
|
# Per-sequence guards for bs > 1. Every sequence must be long enough for
|
|
# the 2*cp_size-way split. A sub-threshold request reaching this point
|
|
# means the scheduler failed to filter it out and a silent non-CP
|
|
# fallback would have masked the bug -- raise instead. Per-sequence
|
|
# radix-cache prefix is supported: prefix is baked into kv_len_prev/next
|
|
# via prefix_offsets[s] inside prepare_context_parallel_metadata.
|
|
extend_lens = getattr(forward_batch, "extend_seq_lens_cpu", None)
|
|
if extend_lens is None:
|
|
return True
|
|
|
|
cp_min = cp_size * 2
|
|
for L in extend_lens:
|
|
if L < cp_min:
|
|
# A sub-threshold request cannot be zigzag-split into 2*cp_size
|
|
# blocks; fall back to a normal (non-CP) prefill for this batch
|
|
# instead of failing. Happens e.g. when a radix-cache prefix hit
|
|
# leaves only a few unique extend tokens.
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def cp_split_and_rebuild_data(forward_batch, input_: torch.Tensor):
|
|
from sglang.srt.layers.attention.dsa.utils import (
|
|
dsa_cp_round_robin_split_data,
|
|
is_dsa_prefill_cp_round_robin_split,
|
|
)
|
|
|
|
if is_dsa_prefill_cp_round_robin_split():
|
|
cp_size = get_parallel().attn_cp_size
|
|
assert (
|
|
input_.shape[0] % cp_size == 0
|
|
), f"Expect input shape 0 can divided by cp size, but got input shape {input_.shape}, cp size {cp_size}"
|
|
return dsa_cp_round_robin_split_data(input_)
|
|
|
|
input_list = list(
|
|
torch.split(input_, forward_batch.attn_cp_metadata.split_list, dim=0)
|
|
)
|
|
result = torch.cat(
|
|
[input_list[i] for i in forward_batch.attn_cp_metadata.zigzag_index], dim=0
|
|
).view(-1, input_.shape[-1])
|
|
return result
|
|
|
|
|
|
def cp_split_and_rebuild_position(forward_batch, positions: torch.Tensor):
|
|
from sglang.srt.layers.attention.dsa.utils import (
|
|
dsa_cp_round_robin_split_data,
|
|
is_dsa_prefill_cp_round_robin_split,
|
|
)
|
|
|
|
if is_dsa_prefill_cp_round_robin_split():
|
|
cp_size = get_parallel().attn_cp_size
|
|
assert positions.shape[0] % cp_size == 0, (
|
|
f"Expect positions shape 0 can divided by cp size, but got positions shape {positions.shape}, "
|
|
f"cp size {cp_size}"
|
|
)
|
|
return dsa_cp_round_robin_split_data(positions)
|
|
|
|
position_id_list = list(
|
|
torch.split(positions, forward_batch.attn_cp_metadata.split_list, dim=-1)
|
|
)
|
|
positions = torch.cat(
|
|
[position_id_list[i] for i in forward_batch.attn_cp_metadata.zigzag_index],
|
|
dim=-1,
|
|
)
|
|
return positions
|
|
|
|
|
|
def cp_round_robin_input_ids(input_ids):
|
|
"""
|
|
input input_ids:
|
|
rank0~7: 0,1,2,3,4,5,...
|
|
|
|
output input_ids:
|
|
a2a none:
|
|
rank0~7: 0,8,16,...,1,9,17,...,2,10,18,...
|
|
|
|
not a2a none:
|
|
rank0: 0,8,16,...
|
|
rank1: 1,9,17,...
|
|
rank2: 2,10,18,...
|
|
...
|
|
"""
|
|
cp_size = get_parallel().attn_cp_size
|
|
cp_rank = get_parallel().attn_cp_rank
|
|
if get_moe_a2a_backend().is_none():
|
|
input_ids = input_ids.reshape(-1, cp_size).T.flatten()
|
|
else:
|
|
input_ids = input_ids[cp_rank::cp_size].contiguous()
|
|
return input_ids
|
|
|
|
|
|
def cp_all_gather_reorganized_into_tensor(input_tensor, cp_size, forward_batch, stream):
|
|
"""
|
|
Allgather communication for context_parallel(kv_cache, index_k, hidden_states).
|
|
This implementation mainly consists of three parts:
|
|
Step 1, padding the input shape to unify the shape for allgather communication (the shape must be the same).
|
|
Step 2, allgather communication(async).
|
|
Step 3, removing the padding and reassembling the data according to the actual tokens.
|
|
"""
|
|
max_len = forward_batch.attn_cp_metadata.max_rank_len[0]
|
|
pad_size = max_len - input_tensor.shape[0]
|
|
if pad_size > 0:
|
|
input_tensor = F.pad(
|
|
input_tensor, (0, 0, 0, pad_size), mode="constant", value=0
|
|
)
|
|
with use_symmetric_memory(
|
|
get_parallel().attn_cp_group, disabled=not is_allocation_symmetric()
|
|
):
|
|
input_tensor_full = torch.empty(
|
|
max_len * cp_size,
|
|
input_tensor.shape[1],
|
|
device=input_tensor.device,
|
|
dtype=input_tensor.dtype,
|
|
)
|
|
|
|
get_parallel().attn_cp_group.cp_all_gather_into_tensor_async(
|
|
input_tensor_full, input_tensor, stream
|
|
)
|
|
|
|
outputs_list_max = list(
|
|
torch.split(
|
|
input_tensor_full, forward_batch.attn_cp_metadata.max_rank_len, dim=0
|
|
)
|
|
)
|
|
outputs = torch.cat(
|
|
[
|
|
outputs_list_max[index][:per_rank_len]
|
|
for index, per_rank_len in enumerate(
|
|
forward_batch.attn_cp_metadata.per_rank_actual_token
|
|
)
|
|
],
|
|
dim=0,
|
|
)
|
|
|
|
return outputs
|
|
|
|
|
|
def cp_all_gather_reorganized_into_tensor_kv_cache(
|
|
input_tensor, cp_size, forward_batch, stream
|
|
):
|
|
"""
|
|
Allgather communication for context_parallel KV cache.
|
|
Handles multi-dimensional tensors (e.g., [seq_len, num_heads, head_dim]).
|
|
"""
|
|
max_len = forward_batch.attn_cp_metadata.max_rank_len[0]
|
|
pad_size = max_len - input_tensor.shape[0]
|
|
if pad_size > 0:
|
|
# Pad the first dimension (seq_len). F.pad expects padding in reverse dimension order.
|
|
# For n dimensional tensor, we need 2*n values: (last_dim_left, last_dim_right, ..., first_dim_left, first_dim_right)
|
|
# To pad only the first dimension: [0, 0] * (ndim - 1) + [0, pad_size]
|
|
padding = [0, 0] * (input_tensor.ndim - 1) + [0, pad_size]
|
|
input_tensor = F.pad(input_tensor, padding, mode="constant", value=0)
|
|
|
|
# Create output tensor with proper shape for all dimensions
|
|
with use_symmetric_memory(
|
|
get_parallel().attn_cp_group, disabled=not is_allocation_symmetric()
|
|
):
|
|
input_tensor_full = torch.empty(
|
|
max_len * cp_size,
|
|
*input_tensor.shape[1:],
|
|
device=input_tensor.device,
|
|
dtype=input_tensor.dtype,
|
|
)
|
|
|
|
get_parallel().attn_cp_group.cp_all_gather_into_tensor_async(
|
|
input_tensor_full, input_tensor, stream
|
|
)
|
|
|
|
outputs_list_max = list(
|
|
torch.split(
|
|
input_tensor_full, forward_batch.attn_cp_metadata.max_rank_len, dim=0
|
|
)
|
|
)
|
|
outputs = torch.cat(
|
|
[
|
|
outputs_list_max[index][:per_rank_len]
|
|
for index, per_rank_len in enumerate(
|
|
forward_batch.attn_cp_metadata.per_rank_actual_token
|
|
)
|
|
],
|
|
dim=0,
|
|
)
|
|
|
|
return outputs
|
|
|
|
|
|
def cp_all_gather_rerange_output(input_tensor, cp_size, forward_batch, stream):
|
|
"""
|
|
# for in-seq-split
|
|
| +-----------before allgather------------+|
|
|
| | dp_atten_tp0: block0, block7 |
|
|
| | dp_atten_tp1: block1, block6 |
|
|
| | dp_atten_tp2: block2, block5 |
|
|
| | dp_atten_tp3: block3, block4 |
|
|
|
|
|
| +----------before rerange---------------+|
|
|
| block0 | block7 | block1 | block6 | block2 | block5 | block3 | block4 |
|
|
|
|
|
| +--------------result-------------------+
|
|
| block0 | block1 | block2 | block3 | block4 | block5 | block6 | block7 |
|
|
| +-------------------------+
|
|
|
|
# for round-robin-split
|
|
| +-----------before allgather------------+|
|
|
| dp_atten_tp0: token0, token4, token8, token12, token16, ... |
|
|
| dp_atten_tp1: token1, token5, token9, token13, token17, ... |
|
|
| dp_atten_tp2: token2, token6, token10, token14, token18, ... |
|
|
| dp_atten_tp3: token3, token7, token11, token15, token19, ... |
|
|
|
|
|
| +--------------result-------------------+
|
|
| token0, token1, token2, token3, token4, token5, token6, token7, ...
|
|
| +-------------------------+
|
|
"""
|
|
from sglang.srt.layers.attention.dsa.utils import (
|
|
is_dsa_prefill_cp_round_robin_split,
|
|
)
|
|
|
|
if is_dsa_prefill_cp_round_robin_split():
|
|
with use_symmetric_memory(
|
|
get_parallel().attn_cp_group, disabled=not is_allocation_symmetric()
|
|
):
|
|
output_tensor = input_tensor.new_empty(
|
|
(input_tensor.shape[0] * cp_size, *input_tensor.shape[1:]),
|
|
)
|
|
attn_cp_all_gather_into_tensor(
|
|
output_tensor,
|
|
input_tensor,
|
|
)
|
|
out_shape = output_tensor.shape
|
|
output_tensor = (
|
|
output_tensor.view(cp_size, -1, *out_shape[1:])
|
|
.transpose(0, 1)
|
|
.reshape(out_shape)
|
|
)
|
|
return output_tensor
|
|
|
|
# TODO: Do we need to remove the padding here?
|
|
bs_seq_len, hidden_size = input_tensor.shape
|
|
output_tensor = cp_all_gather_reorganized_into_tensor(
|
|
input_tensor,
|
|
cp_size,
|
|
forward_batch,
|
|
stream,
|
|
)
|
|
outputs_list = list(
|
|
torch.split(
|
|
output_tensor, forward_batch.attn_cp_metadata.reverse_split_len, dim=0
|
|
)
|
|
)
|
|
output_tensor = torch.cat(
|
|
[outputs_list[i] for i in forward_batch.attn_cp_metadata.cp_reverse_index],
|
|
dim=0,
|
|
)
|
|
output_tensor = output_tensor.view(-1, hidden_size)
|
|
return output_tensor
|
|
|
|
|
|
def cp_all_gather_rerange_kv_cache(input_tensor, cp_size, forward_batch, stream):
|
|
"""
|
|
Allgather and reorganize KV cache from all ranks in context parallel group.
|
|
|
|
# for in-seq-split
|
|
| +-----------before allgather------------+|
|
|
| | dp_atten_tp0: block0, block7 |
|
|
| | dp_atten_tp1: block1, block6 |
|
|
| | dp_atten_tp2: block2, block5 |
|
|
| | dp_atten_tp3: block3, block4 |
|
|
|
|
|
| +----------before rerange---------------+|
|
|
| block0 | block7 | block1 | block6 | block2 | block5 | block3 | block4 |
|
|
|
|
|
| +--------------result-------------------+
|
|
| block0 | block1 | block2 | block3 | block4 | block5 | block6 | block7 |
|
|
| +-------------------------+
|
|
"""
|
|
output_tensor = cp_all_gather_reorganized_into_tensor_kv_cache(
|
|
input_tensor,
|
|
cp_size,
|
|
forward_batch,
|
|
stream,
|
|
)
|
|
outputs_list = list(
|
|
torch.split(
|
|
output_tensor, forward_batch.attn_cp_metadata.reverse_split_len, dim=0
|
|
)
|
|
)
|
|
output_tensor = torch.cat(
|
|
[outputs_list[i] for i in forward_batch.attn_cp_metadata.cp_reverse_index],
|
|
dim=0,
|
|
)
|
|
# No need to reshape - output_tensor already has the correct shape [seq_len, ...]
|
|
return output_tensor
|
|
|
|
|
|
def cp_allgather_and_save_kv_cache(forward_batch, layer, k, v, cp_size, swa_loc=None):
|
|
"""
|
|
Allgather KV cache from all CP ranks and write the full result
|
|
into each rank's local memory pool.
|
|
|
|
swa_loc is the pre-translated full->SWA write target for hybrid SWA pools.
|
|
"""
|
|
cache_loc = (
|
|
forward_batch.out_cache_loc
|
|
if not layer.is_cross_attention
|
|
else forward_batch.encoder_out_cache_loc
|
|
)
|
|
|
|
k = k.contiguous()
|
|
v = v.contiguous()
|
|
|
|
key_cache_full = cp_all_gather_rerange_kv_cache(
|
|
k, cp_size, forward_batch, torch.cuda.current_stream()
|
|
)
|
|
value_cache_full = cp_all_gather_rerange_kv_cache(
|
|
v, cp_size, forward_batch, torch.cuda.current_stream()
|
|
)
|
|
|
|
get_token_to_kv_pool().set_kv_buffer(
|
|
layer,
|
|
KVWriteLoc(cache_loc, swa_loc),
|
|
key_cache_full,
|
|
value_cache_full,
|
|
layer.k_scale,
|
|
layer.v_scale,
|
|
)
|
|
|
|
|
|
def cp_attn_forward_extend(
|
|
forward_batch,
|
|
q: torch.Tensor,
|
|
device: torch.device,
|
|
attn_fn: Callable[[torch.Tensor, torch.Tensor, torch.Tensor, int], torch.Tensor],
|
|
) -> torch.Tensor:
|
|
"""
|
|
Split q into prev/next zigzag halves based on CP metadata, call the
|
|
backend-specific attention function twice with appropriate per-half
|
|
metadata, and concatenate the results.
|
|
|
|
For bs > 1, q is laid out as [all_prev_tokens_across_seqs,
|
|
all_next_tokens_across_seqs]; the split point is total_q_prev_tokens.
|
|
cu_seqlens_q_prev/next tensors have shape [bs+1] and carry the
|
|
per-sequence boundaries through FlashAttention's variable-length API.
|
|
|
|
attn_fn signature:
|
|
attn_fn(q, cu_seqlens_q, cache_seqlens, max_seqlen_q) -> result
|
|
where only these four CP-varying parameters differ between halves.
|
|
All other backend-specific args should be captured in the closure.
|
|
"""
|
|
cp_meta = forward_batch.attn_cp_metadata
|
|
|
|
q_prev = q[: cp_meta.total_q_prev_tokens]
|
|
q_next = q[cp_meta.total_q_prev_tokens :]
|
|
|
|
result_prev = attn_fn(
|
|
q_prev,
|
|
cp_meta.cu_seqlens_q_prev_tensor,
|
|
cp_meta.kv_len_prev_tensor,
|
|
cp_meta.max_seqlen_q_prev,
|
|
)
|
|
result_next = attn_fn(
|
|
q_next,
|
|
cp_meta.cu_seqlens_q_next_tensor,
|
|
cp_meta.kv_len_next_tensor,
|
|
cp_meta.max_seqlen_q_next,
|
|
)
|
|
|
|
return torch.concat([result_prev, result_next], dim=0)
|
|
|
|
|
|
def prepare_context_parallel_metadata(
|
|
kv_len,
|
|
cp_rank,
|
|
cp_size,
|
|
seqs_len,
|
|
extend_seqs_len=None,
|
|
device="cuda",
|
|
):
|
|
from sglang.srt.layers.attention.dsa.utils import (
|
|
is_dsa_prefill_cp_round_robin_split,
|
|
)
|
|
|
|
if is_dsa_prefill_cp_round_robin_split():
|
|
return ContextParallelMetadata()
|
|
|
|
"""prepare_input_dp_with_cp_dsa-zigzag index
|
|
Example (DP_ATTENT_TP == CP_SIZE == 4, single sequence):
|
|
block0 | block1 | block2 | block3 | block4 | block5 | block6 | block7
|
|
rank 0: block0, block7
|
|
rank 1: block1, block6
|
|
rank 2: block2, block5
|
|
rank 3: block3, block4
|
|
For bs > 1, each sequence is split into cp_segment_num = 2 * cp_size
|
|
blocks independently; per-rank layout becomes:
|
|
[s0.block_r, s1.block_r, ..., s_{bs-1}.block_r,
|
|
s0.block_{2*cp_size-1-r}, ..., s_{bs-1}.block_{2*cp_size-1-r}]
|
|
i.e. all prev blocks first, then all next blocks -- so torch.split at
|
|
total_q_prev_tokens cleanly separates them.
|
|
"""
|
|
assert extend_seqs_len is not None
|
|
extend_seqs_len = [int(x) for x in extend_seqs_len]
|
|
|
|
# Update the extend_seqs_len to the padded length.
|
|
pad_len = int(kv_len) - sum(extend_seqs_len)
|
|
if pad_len > 0:
|
|
extend_seqs_len[-1] += pad_len
|
|
if seqs_len is not None and len(seqs_len) == len(extend_seqs_len):
|
|
seqs_len = list(seqs_len)
|
|
seqs_len[-1] += pad_len
|
|
|
|
bs = len(extend_seqs_len)
|
|
cp_segment_num = cp_size * 2
|
|
|
|
# Prefix offset (radix cache hit length) per sequence. For non-NSA
|
|
# (FlashAttention) the prefix is baked into kv_len_prev/next via
|
|
# prefix_offsets[s] below, so cache_seqlens correctly covers the cached
|
|
# prefix. NSA leaves bare cumulatives so its indexer can re-add the
|
|
# offset itself.
|
|
if seqs_len is not None and len(seqs_len) == bs:
|
|
prefix_offsets = [
|
|
max(int(seqs_len[s]) - extend_seqs_len[s], 0) for s in range(bs)
|
|
]
|
|
else:
|
|
prefix_offsets = [0] * bs
|
|
|
|
# Per-sequence block sizes: first (L % cp_segment_num) blocks get +1.
|
|
per_seq_block_sizes: List[List[int]] = []
|
|
split_list: List[int] = []
|
|
for s in range(bs):
|
|
L = extend_seqs_len[s]
|
|
base = L // cp_segment_num
|
|
rem = L % cp_segment_num
|
|
blk = [base + 1 if i < rem else base for i in range(cp_segment_num)]
|
|
per_seq_block_sizes.append(blk)
|
|
split_list.extend(blk)
|
|
|
|
# Per-rank aggregate: this rank owns block r and block (2*cp_size-1-r)
|
|
# of every sequence.
|
|
per_rank_actual_token = [0] * cp_size
|
|
for r in range(cp_size):
|
|
total = 0
|
|
for s in range(bs):
|
|
total += (
|
|
per_seq_block_sizes[s][r]
|
|
+ per_seq_block_sizes[s][cp_segment_num - 1 - r]
|
|
)
|
|
per_rank_actual_token[r] = total
|
|
max_single_rank = max(per_rank_actual_token) if per_rank_actual_token else 0
|
|
# Kept as cp_size copies so downstream torch.split(x, max_rank_len) still
|
|
# works directly. All entries intentionally identical.
|
|
max_rank_len = [max_single_rank] * cp_size
|
|
|
|
# Zigzag index selecting which of split_list's bs * cp_segment_num pieces
|
|
# this rank owns, in the order [all_prevs, all_nexts].
|
|
zigzag_index = list(
|
|
range(cp_rank, cp_rank + bs * cp_segment_num, cp_segment_num)
|
|
) + list(
|
|
range(
|
|
cp_segment_num - cp_rank - 1,
|
|
bs * cp_segment_num,
|
|
cp_segment_num,
|
|
)
|
|
)
|
|
|
|
# Reverse index: given the post-allgather concatenation
|
|
# [rank0_prevs_all_seqs, rank0_nexts_all_seqs,
|
|
# rank1_prevs_all_seqs, rank1_nexts_all_seqs, ...]
|
|
# produce a permutation that restores [s0_b0..s0_bN, s1_b0..s1_bN, ...].
|
|
cp_reverse_index: List[int] = []
|
|
for batch_id in range(bs):
|
|
cp_reverse_index.extend(
|
|
list(range(batch_id, cp_segment_num * bs, 2 * bs))
|
|
+ list(
|
|
range(
|
|
(cp_segment_num - 1) * bs + batch_id,
|
|
0,
|
|
-2 * bs,
|
|
)
|
|
)
|
|
)
|
|
|
|
# Split sizes matching the post-allgather concatenation order above.
|
|
reverse_split_len: List[int] = []
|
|
for r in range(cp_size):
|
|
for s in range(bs):
|
|
reverse_split_len.append(per_seq_block_sizes[s][r])
|
|
for s in range(bs):
|
|
reverse_split_len.append(per_seq_block_sizes[s][cp_segment_num - 1 - r])
|
|
|
|
# Per-sequence cumulatives used for FA cache_seqlens.
|
|
# kv_len_prev[s] = sum of seq s's blocks [0..cp_rank] (inclusive).
|
|
# kv_len_next[s] = sum of seq s's blocks [0..cp_segment_num-cp_rank-1] (inclusive).
|
|
from sglang.srt.layers.attention.dsa.utils import is_dsa_enable_prefill_cp
|
|
|
|
nsa_mode = is_dsa_enable_prefill_cp()
|
|
kv_len_prev_list: List[int] = []
|
|
kv_len_next_list: List[int] = []
|
|
actual_seq_q_prev_list: List[int] = []
|
|
actual_seq_q_next_list: List[int] = []
|
|
for s in range(bs):
|
|
blk = per_seq_block_sizes[s]
|
|
cum_prev = sum(blk[: cp_rank + 1])
|
|
cum_next = sum(blk[: cp_segment_num - cp_rank])
|
|
# NSA indexer re-adds prefix offset itself; leave bare cumulative.
|
|
# For non-NSA (FlashAttention), bake prefix into cache_seqlens.
|
|
if nsa_mode:
|
|
kv_len_prev_list.append(cum_prev)
|
|
kv_len_next_list.append(cum_next)
|
|
else:
|
|
kv_len_prev_list.append(prefix_offsets[s] + cum_prev)
|
|
kv_len_next_list.append(prefix_offsets[s] + cum_next)
|
|
actual_seq_q_prev_list.append(blk[cp_rank])
|
|
actual_seq_q_next_list.append(blk[cp_segment_num - cp_rank - 1])
|
|
|
|
# FlashAttention CUDA tensors (device parameterized for unit tests).
|
|
kv_len_prev_tensor = torch.tensor(
|
|
kv_len_prev_list, device=device, dtype=torch.int32
|
|
)
|
|
kv_len_next_tensor = torch.tensor(
|
|
kv_len_next_list, device=device, dtype=torch.int32
|
|
)
|
|
actual_seq_q_prev_tensor = torch.tensor(
|
|
actual_seq_q_prev_list, device=device, dtype=torch.int32
|
|
)
|
|
actual_seq_q_next_tensor = torch.tensor(
|
|
actual_seq_q_next_list, device=device, dtype=torch.int32
|
|
)
|
|
cu_prev = [0] + list(accumulate(actual_seq_q_prev_list))
|
|
cu_next = [0] + list(accumulate(actual_seq_q_next_list))
|
|
cu_seqlens_q_prev_tensor = torch.tensor(cu_prev, device=device, dtype=torch.int32)
|
|
cu_seqlens_q_next_tensor = torch.tensor(cu_next, device=device, dtype=torch.int32)
|
|
|
|
total_q_prev_tokens = cu_prev[-1]
|
|
total_q_next_tokens = cu_next[-1]
|
|
max_seqlen_q_prev = max(actual_seq_q_prev_list) if actual_seq_q_prev_list else 0
|
|
max_seqlen_q_next = max(actual_seq_q_next_list) if actual_seq_q_next_list else 0
|
|
total_seq_lens = sum(extend_seqs_len)
|
|
|
|
# Cheap invariants: metadata must be a valid permutation spec.
|
|
# - split_list has bs * cp_segment_num pieces (all blocks, all seqs).
|
|
# - zigzag_index has 2 * bs entries (this rank's prev + next per seq).
|
|
# - cp_reverse_index has bs * cp_segment_num entries (reorders the
|
|
# full allgathered stream back to per-seq-original order).
|
|
assert len(split_list) == bs * cp_segment_num
|
|
assert sum(split_list) == total_seq_lens
|
|
assert len(zigzag_index) == 2 * bs
|
|
assert len(cp_reverse_index) == bs * cp_segment_num
|
|
assert sorted(cp_reverse_index) == list(range(bs * cp_segment_num))
|
|
assert sum(per_rank_actual_token) == total_seq_lens
|
|
|
|
return ContextParallelMetadata(
|
|
split_list=split_list,
|
|
zigzag_index=zigzag_index,
|
|
cp_reverse_index=cp_reverse_index,
|
|
reverse_split_len=reverse_split_len,
|
|
per_rank_actual_token=per_rank_actual_token,
|
|
max_rank_len=max_rank_len,
|
|
kv_len_prev_tensor=kv_len_prev_tensor,
|
|
kv_len_next_tensor=kv_len_next_tensor,
|
|
actual_seq_q_prev_tensor=actual_seq_q_prev_tensor,
|
|
actual_seq_q_next_tensor=actual_seq_q_next_tensor,
|
|
cu_seqlens_q_prev_tensor=cu_seqlens_q_prev_tensor,
|
|
cu_seqlens_q_next_tensor=cu_seqlens_q_next_tensor,
|
|
total_q_prev_tokens=total_q_prev_tokens,
|
|
total_q_next_tokens=total_q_next_tokens,
|
|
max_seqlen_q_prev=max_seqlen_q_prev,
|
|
max_seqlen_q_next=max_seqlen_q_next,
|
|
kv_len_prev_list=kv_len_prev_list,
|
|
kv_len_next_list=kv_len_next_list,
|
|
actual_seq_q_prev_list=actual_seq_q_prev_list,
|
|
actual_seq_q_next_list=actual_seq_q_next_list,
|
|
total_seq_lens=total_seq_lens,
|
|
bs=bs,
|
|
)
|