chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,3 @@
# Temp workaround, make layer utils more fine-grained later
from sglang.srt.layers.utils.common import *
from sglang.srt.layers.utils.multi_platform import MultiPlatformOp
+127
View File
@@ -0,0 +1,127 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import logging
import re
import torch
from torch.nn.parameter import Parameter
logger = logging.getLogger(__name__)
def get_layer_id(weight_name):
# example weight name: model.layers.10.self_attn.qkv_proj.weight
match = re.search(r"layers\.(\d+)\.", weight_name)
if match:
return int(match.group(1))
return None
def pad_or_narrow_weight(
loaded_weight: torch.Tensor, input_dim: int, start_idx: int, shard_size: int
) -> torch.Tensor:
# Padding with zeros for special case such as qwen2_5_VL's mlp which is not 8-aligned
valid_size = max(loaded_weight.shape[input_dim] - start_idx, 0)
if valid_size > 0:
loaded_slice = loaded_weight.narrow(input_dim, start_idx, valid_size)
pad_shape = list(loaded_weight.shape)
pad_shape[input_dim] = shard_size - valid_size
pad = torch.zeros(
pad_shape, dtype=loaded_weight.dtype, device=loaded_weight.device
)
return torch.cat([loaded_slice, pad], dim=input_dim)
# All padding
pad_shape = list(loaded_weight.shape)
pad_shape[input_dim] = shard_size
return torch.zeros(
pad_shape, dtype=loaded_weight.dtype, device=loaded_weight.device
)
def is_strict_contiguous(x: torch.Tensor) -> bool:
expected_stride = 1
for size, stride in zip(reversed(x.shape), reversed(x.stride())):
if stride != expected_stride:
return False
expected_stride *= size
return True
def strict_contiguous(x: torch.Tensor) -> torch.Tensor:
if is_strict_contiguous(x):
return x
return x.clone(memory_format=torch.contiguous_format)
def copy_or_rebind_param(
module: torch.nn.Module, name: str, new_value: torch.Tensor
) -> None:
"""Keep parameter identities stable for CUDA graph reuse and hot reload."""
new_value = new_value.detach()
param = getattr(module, name, None)
if isinstance(param, Parameter):
if param.data.shape == new_value.shape and param.data.dtype == new_value.dtype:
param.data.copy_(new_value)
else:
param.data = new_value
param.requires_grad_(False)
else:
setattr(module, name, Parameter(new_value, requires_grad=False))
def alias_or_bind_derived_param(
module: torch.nn.Module,
source_name: str,
derived_name: str,
derived_value: torch.Tensor,
) -> None:
"""Bind a post-processed (derived) tensor to a derived attribute name.
When `derived_value` is broadcastable to the source Parameter's shape (and
dtype matches), write it broadcast-filled into the source's storage in
place and register `derived_name` as an alias of the source Parameter. The
two attribute names then share one underlying buffer, so:
- apply() can read via `derived_name`
- update_weights_from_disk can keep refilling `source_name` (the loader
re-runs process_weights_after_loading which re-derives in place)
- peak GPU memory is the source size, not source + derived.
When the shapes are not broadcast-compatible, fall back to allocating a
separate Parameter under `derived_name` via copy_or_rebind_param.
"""
derived_value = derived_value.detach()
source = getattr(module, source_name, None)
if isinstance(source, Parameter) and source.data.dtype == derived_value.dtype:
try:
broadcast = torch.broadcast_to(derived_value, source.data.shape)
except RuntimeError:
broadcast = None
if broadcast is not None:
source.data.copy_(broadcast)
source.requires_grad_(False)
setattr(module, derived_name, source)
return
copy_or_rebind_param(module, derived_name, derived_value)
class PPMissingLayer(torch.nn.Identity):
# Adapted from
# https://github.com/vllm-project/vllm/blob/18ed3132d2bfe1df9a74729457b69243955221e8/vllm/model_executor/models/utils.py#L468C1-L486C1
"""
A placeholder layer for missing layers in a pipeline parallel model.
"""
def __init__(self, *args, **kwargs):
super().__init__()
self.return_tuple = kwargs.get("return_tuple", False)
def forward(self, *args, **kwargs):
"""
Return the first arg from args or the first value from kwargs.
Wraps the input in a tuple if `self.return_tuple` is True.
"""
input = args[0] if args else next(iter(kwargs.values()))
return (input,) if self.return_tuple else input
+694
View File
@@ -0,0 +1,694 @@
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,
)
+121
View File
@@ -0,0 +1,121 @@
import torch
import triton
import triton.language as tl
@triton.jit
def rotl32(x, r: tl.constexpr) -> tl.uint32:
"""
rotate left 32-bit integer x by r bits
e.g. x = 01110001, r = 2 -> 11000101
"""
x = x.to(tl.uint64)
return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF
@triton.jit
def fmix32(h: tl.uint32) -> tl.uint32:
"""
final mix of 32-bit hash value for MurmurHash
"""
h ^= h >> 16
h = (h * 0x85EBCA6B) & 0xFFFFFFFF
h ^= h >> 13
h = (h * 0xC2B2AE35) & 0xFFFFFFFF
h ^= h >> 16
return h
@triton.jit
def murmur3_mix(h: tl.uint32, k: tl.uint32) -> tl.uint32:
"""
Mixes a 32-bit key into the hash state.
"""
c1: tl.uint32 = 0xCC9E2D51
c2: tl.uint32 = 0x1B873593
r1: tl.constexpr = 15
r2: tl.constexpr = 13
mm: tl.uint32 = 5
nn: tl.uint32 = 0xE6546B64
k = (k * c1) & 0xFFFFFFFF
k = rotl32(k, r1)
k = (k * c2) & 0xFFFFFFFF
h ^= k
h = rotl32(h, r2)
h = (h * mm + nn) & 0xFFFFFFFF
return h
@triton.jit
def murmur_hash32_kernel(
seed_ptr,
positions_ptr,
col_indices_ptr,
output_ptr,
num_rows,
num_cols,
BLOCK_SIZE: tl.constexpr,
):
"""
MurmurHash 32-bit implementation for Triton.
Reference:
- https://medium.com/@thealonemusk/murmurhash-the-scrappy-algorithm-that-secretly-powers-half-the-internet-2d3f79b4509b
- https://en.wikipedia.org/wiki/MurmurHash
We treat 64-bit seed, 32-bit position, and 32-bit col_index as 4 4-byte blocks, and bit-blend them together.
"""
pid_row = tl.program_id(0)
pid_col = tl.program_id(1)
row_idx = pid_row
col_offsets = pid_col * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = col_offsets < num_cols
# Load inputs
seed = tl.load(seed_ptr + row_idx).to(tl.uint64)
pos = tl.load(positions_ptr + row_idx).to(tl.uint32)
col = tl.load(col_indices_ptr + col_offsets, mask=mask, other=0).to(tl.uint32)
h: tl.uint32 = 0 # hash accumulator
# Process seed_low
k: tl.uint32 = (seed & 0xFFFFFFFF).to(tl.uint32)
h = murmur3_mix(h, k)
# Process seed_high
k = ((seed >> 32) & 0xFFFFFFFF).to(tl.uint32)
h = murmur3_mix(h, k)
# Process position block starting from seed32
h = murmur3_mix(h, pos)
# Process col block
h = murmur3_mix(h, col)
# Finalize (len=16 for seed + pos + col)
h ^= 16
h = fmix32(h)
# Store result as uint32
tl.store(output_ptr + row_idx * num_cols + col_offsets, h, mask=mask)
def murmur_hash32(seed, positions, col_indices):
assert (
seed.shape == positions.shape
), "Seed and positions must have the same shape (n,)"
assert (
len(seed.shape) == 1 and len(col_indices.shape) == 1
), f"Inputs must be 1D tensors {seed.shape=} {col_indices.shape=}"
n = seed.shape[0]
m = col_indices.shape[0]
device = seed.device
hashed = torch.empty((n, m), dtype=torch.uint32, device=device)
BLOCK_SIZE = 1024
grid = (n, triton.cdiv(m, BLOCK_SIZE))
murmur_hash32_kernel[grid](
seed, positions, col_indices, hashed, n, m, BLOCK_SIZE=BLOCK_SIZE
)
return hashed
+357
View File
@@ -0,0 +1,357 @@
from __future__ import annotations
import dataclasses
from enum import Enum, auto
from typing import TYPE_CHECKING, List, Optional
import torch
from sglang.srt.environ import envs
if TYPE_CHECKING:
from sglang.srt.layers.logits_processor import LogitsMetadata
class LogprobStage(Enum):
PREFILL = auto()
DECODE = auto()
@dataclasses.dataclass
class InputLogprobsResult:
input_token_logprobs: torch.Tensor
input_top_logprobs_val: Optional[List] = None
input_top_logprobs_idx: Optional[List] = None
input_token_ids_logprobs_val: Optional[List] = None
input_token_ids_logprobs_idx: Optional[List] = None
def get_top_logprobs_raw(
logprobs: torch.Tensor,
top_logprobs_nums: List[int],
stage: LogprobStage,
extend_logprob_pruned_lens_cpu: Optional[List[int]] = None,
no_copy_to_cpu: bool = False,
):
max_k = max(top_logprobs_nums)
values, indices = logprobs.topk(max_k, dim=-1)
if not no_copy_to_cpu:
values = values.tolist()
indices = indices.tolist()
top_logprobs_val = []
top_logprobs_idx = []
if stage == LogprobStage.DECODE:
for i, k in enumerate(top_logprobs_nums):
top_logprobs_val.append(values[i][:k])
top_logprobs_idx.append(indices[i][:k])
else:
pt = 0
for k, pruned_len in zip(top_logprobs_nums, extend_logprob_pruned_lens_cpu):
if pruned_len <= 0:
top_logprobs_val.append([])
top_logprobs_idx.append([])
continue
top_logprobs_val.append([values[pt + j][:k] for j in range(pruned_len)])
top_logprobs_idx.append([indices[pt + j][:k] for j in range(pruned_len)])
pt += pruned_len
return top_logprobs_val, top_logprobs_idx
def get_top_logprobs_prefill(
all_logprobs: torch.Tensor, logits_metadata: LogitsMetadata
):
return get_top_logprobs_raw(
all_logprobs,
logits_metadata.top_logprobs_nums,
stage=LogprobStage.PREFILL,
extend_logprob_pruned_lens_cpu=logits_metadata.extend_logprob_pruned_lens_cpu,
)
def get_top_logprobs(
logprobs: torch.Tensor,
top_logprobs_nums: List[int],
no_copy_to_cpu: bool = False,
):
return get_top_logprobs_raw(
logprobs,
top_logprobs_nums,
stage=LogprobStage.DECODE,
no_copy_to_cpu=no_copy_to_cpu,
)
def get_token_ids_logprobs_raw(
logprobs: torch.Tensor,
token_ids_logprobs_list: List[Optional[List[int]]],
stage: LogprobStage,
extend_logprob_pruned_lens_cpu: Optional[List[int]] = None,
no_copy_to_cpu: bool = False,
):
vals, idxs = [], []
if stage == LogprobStage.DECODE:
for i, token_ids in enumerate(token_ids_logprobs_list):
if token_ids is None:
vals.append([])
idxs.append([])
else:
token_ids_tensor = torch.tensor(token_ids, dtype=torch.long).to(
logprobs.device, non_blocking=True
)
row = logprobs[i, token_ids_tensor]
vals.append(row if no_copy_to_cpu else row.tolist())
idxs.append(token_ids)
else: # prefill
pt = 0
for i, (token_ids, pruned_len) in enumerate(
zip(token_ids_logprobs_list, extend_logprob_pruned_lens_cpu)
):
if pruned_len <= 0:
vals.append([])
idxs.append([])
continue
token_ids_tensor = torch.tensor(token_ids, dtype=torch.long).to(
logprobs.device, non_blocking=True
)
pos_logprobs = logprobs[pt : pt + pruned_len, token_ids_tensor]
vals.append(pos_logprobs if no_copy_to_cpu else pos_logprobs.tolist())
idxs.append([token_ids for _ in range(pruned_len)])
pt += pruned_len
return vals, idxs
def get_token_ids_logprobs_prefill(
all_logprobs, logits_metadata: LogitsMetadata, no_copy_to_cpu=False
):
return get_token_ids_logprobs_raw(
all_logprobs,
logits_metadata.token_ids_logprobs,
stage=LogprobStage.PREFILL,
extend_logprob_pruned_lens_cpu=logits_metadata.extend_logprob_pruned_lens_cpu,
no_copy_to_cpu=no_copy_to_cpu,
)
def get_token_ids_logprobs(logprobs, token_ids_logprobs, no_copy_to_cpu=False):
return get_token_ids_logprobs_raw(
logprobs,
token_ids_logprobs,
stage=LogprobStage.DECODE,
no_copy_to_cpu=no_copy_to_cpu,
)
def get_top_logprobs_chunk(
logprobs: torch.Tensor,
logits_metadata: LogitsMetadata,
top_k_nums: List[int],
pruned_lens: List[int],
input_top_logprobs_val: List,
input_top_logprobs_idx: List,
split_pruned_len: int,
) -> int:
"""Get top-k logprobs for each sequence in the chunk.
Args:
logprobs: Log probabilities tensor of shape [seq_len, vocab_size]
logits_metadata: Metadata containing top-k and pruned length info
top_k_nums: List of top-k numbers for each sequence
pruned_lens: List of pruned lengths for each sequence
input_top_logprobs_val: List to store top-k logprob values
input_top_logprobs_idx: List to store top-k token indices
split_pruned_len: Length of pruned tokens from previous chunk
Returns:
int: Number of remaining tokens to process in next chunk
"""
# No sequences in the chunk
if logprobs.shape[0] == 0:
return 0
max_k = max(logits_metadata.top_logprobs_nums)
ret = logprobs.topk(max_k, dim=1)
values = ret.values.tolist()
indices = ret.indices.tolist()
pt = 0
next_split_pruned_len = 0
for n, (k, pruned_len) in enumerate(zip(top_k_nums, pruned_lens)):
if n == 0:
# For the first sequence, adjust the pruned length
pruned_len -= split_pruned_len
else:
# After the first sequence, no split in the middle
split_pruned_len = 0
if pruned_len <= 0:
# if pruned length is less than or equal to 0,
# there is no top-k logprobs to process
input_top_logprobs_val.append([])
input_top_logprobs_idx.append([])
continue
# Get the top-k logprobs
val = []
idx = []
for j in range(pruned_len):
# Handle remaining tokens in next chunk if any
if pt + j >= len(values):
next_split_pruned_len = split_pruned_len + j
break
# Append the top-k logprobs
val.append(values[pt + j][:k])
idx.append(indices[pt + j][:k])
# Append or extend based on whether the sequence was split across chunks
if len(val) > 0:
if split_pruned_len > 0:
input_top_logprobs_val[-1].extend(val)
input_top_logprobs_idx[-1].extend(idx)
else:
input_top_logprobs_val.append(val)
input_top_logprobs_idx.append(idx)
pt += pruned_len
return next_split_pruned_len
def get_token_ids_logprobs_chunk(
logprobs: torch.Tensor,
token_ids_logprobs: List[int],
pruned_lens: List[int],
input_token_ids_logprobs_val: List,
input_token_ids_logprobs_idx: List,
split_pruned_len: int = 0,
):
"""Get token_ids logprobs for each sequence in the chunk.
Args:
logprobs: Log probabilities tensor of shape [seq_len, vocab_size]
logits_metadata: Metadata containing token IDs and pruned length info
token_ids_logprobs: List of token IDs for each sequence
pruned_lens: List of pruned lengths for each sequence
input_token_ids_logprobs_val: List to store token logprob values
input_token_ids_logprobs_idx: List to store token indices
split_pruned_len: Length of pruned tokens from previous chunk
Returns:
int: Number of remaining tokens to process in next chunk
"""
# No sequences in the chunk
if logprobs.shape[0] == 0:
return 0
pt = 0
next_split_pruned_len = 0
for n, (token_ids, pruned_len) in enumerate(
zip(
token_ids_logprobs,
pruned_lens,
)
):
# Adjust pruned length for first sequence
if n == 0:
pruned_len -= split_pruned_len
else:
split_pruned_len = 0
if pruned_len <= 0:
# if pruned length is less than or equal to 0,
# there is no token ids logprobs to process
input_token_ids_logprobs_val.append([])
input_token_ids_logprobs_idx.append([])
continue
# Get the token ids logprobs
val = []
idx = []
for j in range(pruned_len):
# Handle remaining tokens in next chunk if any
if pt + j >= logprobs.shape[0]:
next_split_pruned_len = split_pruned_len + j
break
if token_ids is not None:
val.append(logprobs[pt + j, token_ids].tolist())
idx.append(token_ids)
# Append or extend based on whether the sequence was split across chunks
if len(val) > 0:
if split_pruned_len > 0:
input_token_ids_logprobs_val[-1].extend(val)
input_token_ids_logprobs_idx[-1].extend(idx)
else:
input_token_ids_logprobs_val.append(val)
input_token_ids_logprobs_idx.append(idx)
pt += pruned_len
return next_split_pruned_len
def compute_spec_v2_logprobs(
batch,
logits_output,
predict: torch.Tensor,
accept_index: torch.Tensor,
speculative_num_steps: int,
):
"""Compute logprobs for accepted tokens after spec v2 verify sampling.
Gathers logits at accepted positions, applies log_softmax (temperature-scaled
if not greedy), and populates logits_output.next_token_logprobs (plus optional
top-k / token-ids logprobs) so they flow through copy_to_cpu().
"""
bs = len(batch.seq_lens)
max_accept = speculative_num_steps + 1
device = predict.device
flat_accept_idx = accept_index.long().reshape(-1)
gathered_logits = logits_output.next_token_logits[flat_accept_idx]
if batch.sampling_info.is_all_greedy or envs.SGLANG_RETURN_ORIGINAL_LOGPROB.get():
gathered_logprobs = torch.nn.functional.log_softmax(gathered_logits, dim=-1)
else:
temperatures = torch.repeat_interleave(
batch.sampling_info.temperatures,
max_accept,
dim=0,
)
gathered_logprobs = torch.nn.functional.log_softmax(
gathered_logits / temperatures, dim=-1
)
gathered_logprobs.clamp_(min=torch.finfo(gathered_logprobs.dtype).min)
accepted_token_ids = predict[flat_accept_idx]
token_logprobs = gathered_logprobs[
torch.arange(bs * max_accept, device=device),
accepted_token_ids.long(),
]
logits_output.next_token_logprobs = token_logprobs.reshape(bs, max_accept)
if batch.top_logprobs_nums and any(x > 0 for x in batch.top_logprobs_nums):
top_logprobs_nums_expanded = [
num for num in batch.top_logprobs_nums for _ in range(max_accept)
]
(
logits_output.next_token_top_logprobs_val,
logits_output.next_token_top_logprobs_idx,
) = get_top_logprobs(
gathered_logprobs, top_logprobs_nums_expanded, no_copy_to_cpu=True
)
if batch.token_ids_logprobs and any(
x is not None for x in batch.token_ids_logprobs
):
token_ids_logprobs_expanded = [
ids for ids in batch.token_ids_logprobs for _ in range(max_accept)
]
(
logits_output.next_token_token_ids_logprobs_val,
logits_output.next_token_token_ids_logprobs_idx,
) = get_token_ids_logprobs(
gathered_logprobs, token_ids_logprobs_expanded, no_copy_to_cpu=True
)
@@ -0,0 +1,134 @@
from typing import Callable, ClassVar
from torch import nn
from sglang.kernel_api_logging import debug_kernel_api
from sglang.srt.platforms import current_platform
from sglang.srt.utils import (
cpu_has_amx_support,
is_cpu,
is_cuda,
is_hip,
is_musa,
is_npu,
is_xpu,
)
_is_cuda = is_cuda()
_is_hip = is_hip()
_is_cpu = is_cpu()
_is_cpu_amx_available = cpu_has_amx_support()
_is_npu = is_npu()
_is_xpu = is_xpu()
_is_musa = is_musa()
class MultiPlatformOp(nn.Module):
# OOT forward registry: maps dispatch_key -> {op_cls -> forward_fn}
_oot_forward_registry: ClassVar[dict[str, dict[type, Callable]]] = {}
@classmethod
def register_oot_forward(cls, op_cls: type, fn: Callable, platform_key: str):
"""Register an OOT forward implementation for a specific op class and platform."""
cls._oot_forward_registry.setdefault(platform_key, {})[op_cls] = fn
def __init__(self):
super().__init__()
self._forward_method: Callable = self.dispatch_forward()
# States for torch.compile
self._original_forward_method = None
self.is_torch_compile = False
def enter_torch_compile(self, num_tokens: int):
# Skip if Op is already entered compile mode.
# NOTE(alcanderian): Some Ops(for example RotaryEmbedding) will be reused
# among layers and `enter_torch_compile` will be called many times.
# We should prevent `self._original_forward_method` from being overridden when
# it is not the first time `enter_torch_compile` called.
if self.is_torch_compile:
return
self._original_forward_method = self._forward_method
# NOTE: Temporarily workaround MoE
# The performance of torch.compile on this layer is not always good when bs > 1,
# so we decide to only use torch.compile when bs=1
if "FusedMoE" in self.__class__.__name__:
if num_tokens == 1:
from sglang.srt.layers.moe.fused_moe_native import (
fused_moe_forward_native,
)
self._forward_method = fused_moe_forward_native
elif "TopK" in self.__class__.__name__:
if num_tokens == 1:
self._forward_method = self.forward_native
else:
self._forward_method = self.forward_native
self.is_torch_compile = True
def leave_torch_compile(self):
# Skip if Op is already exited compile mode.
if not self.is_torch_compile:
return
self._forward_method = self._original_forward_method
self._original_forward_method = None
self.is_torch_compile = False
# Please do not override this method, because `self._forward_method` can change when in torch compile mode
@debug_kernel_api
def forward(self, *args, **kwargs):
return self._forward_method(*args, **kwargs)
def forward_native(self, *args, **kwargs):
raise NotImplementedError
def forward_cuda(self, *args, **kwargs):
raise NotImplementedError
def forward_npu(self, *args, **kwargs):
return self.forward_native(*args, **kwargs)
def forward_hip(self, *args, **kwargs):
return self.forward_cuda(*args, **kwargs)
def forward_xpu(self, *args, **kwargs):
return self.forward_native(*args, **kwargs)
def forward_musa(self, *args, **kwargs):
return self.forward_cuda(*args, **kwargs)
def forward_hpu(self, *args, **kwargs):
return self.forward_native(*args, **kwargs)
def forward_cpu(self, *args, **kwargs):
return self.forward_native(*args, **kwargs)
def dispatch_forward(self):
# OOT platform dispatch: check registry then method lookup
if current_platform.is_out_of_tree():
key = current_platform.get_dispatch_key_name()
oot = self._oot_forward_registry.get(key, {})
if type(self) in oot:
return oot[type(self)].__get__(self)
method = getattr(self, f"forward_{key}", None)
if method is not None:
return method
return self.forward_native
if _is_cuda:
return self.forward_cuda
elif _is_hip:
return self.forward_hip
elif _is_cpu and _is_cpu_amx_available:
return self.forward_cpu
elif _is_npu:
return self.forward_npu
elif _is_xpu:
return self.forward_xpu
elif _is_musa:
return self.forward_musa
else:
return self.forward_native