Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

802 lines
32 KiB
Python

import logging
from typing import Callable, Dict, List, Optional, Tuple
import torch
import torch.distributed as dist
from torch import nn
from sglang.srt.distributed import get_tp_group
from sglang.srt.layers.dp_attention import (
is_dp_attention_enabled,
)
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.layers.utils.hash import murmur_hash32
from sglang.srt.layers.utils.logprob import get_token_ids_logprobs, get_top_logprobs
from sglang.srt.runtime_context import get_parallel, get_server_args
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
from sglang.srt.sampling.sampling_params import TOP_K_ALL
from sglang.srt.utils.async_probe import sanitize_nan_logits
from sglang.srt.utils.common import (
get_bool_env_var,
is_cuda,
is_hip,
is_musa,
is_npu,
)
if is_cuda():
from flashinfer.sampling import (
min_p_sampling_from_probs,
top_k_top_p_sampling_from_probs,
)
from sgl_kernel import (
top_k_renorm_prob,
top_p_renorm_prob,
)
if is_musa():
from sgl_kernel import (
min_p_sampling_from_probs,
top_k_renorm_prob,
top_k_top_p_sampling_from_probs,
top_p_renorm_prob,
)
_use_aiter = get_bool_env_var("SGLANG_USE_AITER") and is_hip()
if _use_aiter:
from aiter import greedy_sample as _aiter_greedy_sample
# The aiter greedy_sample kernel can return an out-of-range token id (== vocab_size,
# e.g. 151666 for MiniCPM-V) for all-NaN / all -inf logit rows on ROCm, which decodes
# to an empty string and breaks downstream consumers. Set this to 1 to fall back to
# torch.argmax (which always returns a valid index). Default off so behavior is
# unchanged elsewhere.
_disable_aiter_greedy_sample = get_bool_env_var("SGLANG_DISABLE_AITER_GREEDY_SAMPLE")
if is_npu():
import torch_npu
logger = logging.getLogger(__name__)
SYNC_TOKEN_IDS_ACROSS_TP = get_bool_env_var("SYNC_TOKEN_IDS_ACROSS_TP")
SGLANG_RETURN_ORIGINAL_LOGPROB = get_bool_env_var("SGLANG_RETURN_ORIGINAL_LOGPROB")
_CUSTOM_SAMPLER_FACTORIES: Dict[str, Callable[[], "Sampler"]] = {}
_BUILT_IN_SAMPLING_BACKENDS = {"flashinfer", "pytorch", "ascend"}
class Sampler(nn.Module):
def __init__(self):
super().__init__()
self.tp_sync_group = get_tp_group().device_group
if is_dp_attention_enabled():
self.tp_sync_group = get_parallel().attn_tp_group.device_group
self.rl_on_policy_target = get_server_args().rl_on_policy_target
# In RL on-policy mode, deterministic inference is automatically enabled.
self.enable_deterministic = get_server_args().enable_deterministic_inference
# In RL on-policy mode, we use log_softmax to compute logprobs to match the trainer.
self.use_log_softmax_logprob = self.rl_on_policy_target is not None
self.use_ascend_backend = get_server_args().sampling_backend == "ascend"
def _preprocess_logits(
self, logits: torch.Tensor, sampling_info: SamplingBatchInfo
) -> torch.Tensor:
"""Apply custom logit processors and sanitize non-finite logits."""
if sampling_info.has_custom_logit_processor:
apply_custom_logit_processor(logits, sampling_info)
sanitize_nan_logits(logits, "sampler: next_token_logits")
return logits
def forward(
self,
logits_output: LogitsProcessorOutput,
sampling_info: SamplingBatchInfo,
return_logprob: bool,
top_logprobs_nums: List[int],
token_ids_logprobs: List[List[int]],
positions: torch.Tensor,
):
"""Run a sampler & compute logprobs and update logits_output accordingly.
Args:
logits_output: The logits from the model forward
sampling_info: Metadata for sampling
return_logprob: If set, store the output logprob information to
logits_output
top_logprobs_nums: Number of top lobprobs per sequence in a batch
token_ids_logprobs: Per-sequence list of specific token IDs to retrieve
logprobs for. Each element is a list of token IDs (or None) for one
sequence in the batch. This is used in speculative decoding.
positions: The positions of the tokens in the sequence. Used for deterministic sampling
to get the unique seed for each position.
"""
logits = logits_output.next_token_logits
# Preprocess logits (custom processors and NaN handling)
logits = self._preprocess_logits(logits, sampling_info)
if sampling_info.is_all_greedy:
if _use_aiter and not _disable_aiter_greedy_sample:
batch_next_token_ids = torch.empty(
logits.shape[0], device=logits.device, dtype=torch.int32
)
_aiter_greedy_sample(batch_next_token_ids, logits)
else:
batch_next_token_ids = torch.argmax(logits, -1)
if return_logprob:
original_logprobs = logprobs = torch.nn.functional.log_softmax(
logits, dim=-1
)
else:
simple_sampling_case = (
not sampling_info.need_top_p_sampling
and not sampling_info.need_top_k_sampling
and not sampling_info.need_min_p_sampling
)
# If requested, cache original logprobs before temperature scaling.
if return_logprob and SGLANG_RETURN_ORIGINAL_LOGPROB:
original_logprobs = torch.log_softmax(logits, dim=-1)
# In RL on-policy mode, we use log_softmax to compute logprobs to match the trainer.
logprobs_via_logsoftmax_kernel = None
if self.rl_on_policy_target is not None:
# TODO: use more inplace ops to save memory
logits_div_temperature = (
logits.bfloat16().div(sampling_info.temperatures).bfloat16()
)
logprobs_via_logsoftmax_kernel = torch.log_softmax(
logits_div_temperature, dim=-1
)
del logits_div_temperature
if self.use_ascend_backend:
# Ascend backend: sample from logits directly.
batch_next_token_ids, logprobs = self._forward_ascend_backend(
logits,
sampling_info,
simple_sampling_case,
return_logprob,
positions,
)
elif (
self.use_log_softmax_logprob
and self.enable_deterministic
and simple_sampling_case
):
# RL on-policy path: sample from logprobs to match the trainer.
batch_next_token_ids = self._sample_from_logprobs(
logprobs_via_logsoftmax_kernel,
sampling_info,
positions,
)
if return_logprob and not SGLANG_RETURN_ORIGINAL_LOGPROB:
logprobs = logprobs_via_logsoftmax_kernel
else:
# Standard path: do softmax and sample from probs.
logits.div_(sampling_info.temperatures)
# In-place op to save memory
logits[:] = torch.softmax(logits, dim=-1)
probs = logits
batch_next_token_ids = self._sample_from_probs(
probs, sampling_info, positions, simple_sampling_case
)
if return_logprob and not SGLANG_RETURN_ORIGINAL_LOGPROB:
logprobs = (
logprobs_via_logsoftmax_kernel
if logprobs_via_logsoftmax_kernel is not None
else torch.log(probs)
)
del probs
# Attach logprobs to logits_output (in-place modification)
if return_logprob:
if SGLANG_RETURN_ORIGINAL_LOGPROB:
logprobs = original_logprobs
self._attach_logprobs_to_output(
logits_output,
logprobs,
top_logprobs_nums,
token_ids_logprobs,
sampling_info,
batch_next_token_ids,
)
self._sync_token_ids_across_tp(batch_next_token_ids, sampling_info)
return batch_next_token_ids
def _sample_from_probs(
self,
probs: torch.Tensor,
sampling_info: SamplingBatchInfo,
positions: torch.Tensor,
simple_sampling_case: bool,
) -> torch.Tensor:
"""Sample from probability distribution (after softmax).
Used for standard sampling with flashinfer/pytorch backends.
Handles both simple (direct multinomial) and complex (top-k/top-p/min-p) cases.
"""
if simple_sampling_case:
batch_next_token_ids = sampling_from_probs_torch(
probs,
sampling_seed=sampling_info.sampling_seed,
positions=positions,
)
else:
backend = get_server_args().sampling_backend
if backend == "flashinfer":
assert (
sampling_info.sampling_seed is None
), "Sampling seed is not supported for flashinfer backend"
if sampling_info.need_min_p_sampling:
probs = top_k_renorm_prob(probs, sampling_info.top_ks)
probs = top_p_renorm_prob(probs, sampling_info.top_ps)
batch_next_token_ids = min_p_sampling_from_probs(
probs, sampling_info.min_ps
)
else:
batch_next_token_ids = top_k_top_p_sampling_from_probs(
probs.contiguous(),
sampling_info.top_ks,
sampling_info.top_ps,
filter_apply_order="joint",
)
elif backend == "pytorch":
# A slower fallback implementation with torch native operations.
batch_next_token_ids = top_k_top_p_min_p_sampling_from_probs_torch(
probs,
sampling_info.top_ks,
sampling_info.top_ps,
sampling_info.min_ps,
sampling_info.need_min_p_sampling,
sampling_info.sampling_seed,
positions,
)
else:
raise ValueError(f"Invalid sampling backend: {backend}")
return batch_next_token_ids
def _sample_from_logprobs(
self,
logprobs: torch.Tensor,
sampling_info: SamplingBatchInfo,
positions: torch.Tensor,
) -> torch.Tensor:
"""Sample from log-probabilities using the Gumbel trick.
Used for deterministic sampling with simple cases (no top-k/top-p/min-p).
Requires sampling_seed to be set in sampling_info.
"""
assert (
sampling_info.sampling_seed is not None
), "sampling_seed is required for sampling from logprobs"
sampled_index = multinomial_with_seed(
logprobs, sampling_info.sampling_seed, positions
)
return sampled_index.view(-1).to(torch.int32)
def _sample_from_logits(
self,
logits: torch.Tensor,
sampling_info: SamplingBatchInfo,
simple_sampling_case: bool,
positions: torch.Tensor,
) -> torch.Tensor:
"""Sample from temperature-scaled logits without softmax.
Used for the Ascend NPU backend which handles softmax internally.
"""
if simple_sampling_case:
probs = torch.softmax(logits, dim=-1)
if sampling_info.sampling_seed is not None:
probabilities = probs.to(torch.float64).log_()
batch_next_token_ids = multinomial_with_seed(
probabilities, sampling_info.sampling_seed, positions
).view(-1)
else:
batch_next_token_ids = torch.multinomial(probs, num_samples=1).view(-1)
return batch_next_token_ids.to(torch.int32)
else:
assert (
self.use_ascend_backend
), "Only ascend backend supports sampling from logits"
batch_next_token_ids = top_k_top_p_min_p_sampling_from_logits_ascend(
logits,
sampling_info.top_ks,
sampling_info.top_ps,
sampling_info.min_ps,
sampling_info.need_min_p_sampling,
sampling_info.sampling_seed,
positions,
)
return batch_next_token_ids.to(torch.int32)
def _forward_ascend_backend(
self,
logits: torch.Tensor,
sampling_info: SamplingBatchInfo,
simple_sampling_case: bool,
return_logprob: bool,
positions: torch.Tensor,
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
"""Handle the full Ascend backend sampling path.
Ascend backend has fused kernels that handle softmax internally,
so we sample directly from temperature-scaled logits.
Returns:
A tuple of (batch_next_token_ids, logprobs). logprobs is None
when return_logprob is False or SGLANG_RETURN_ORIGINAL_LOGPROB is set.
"""
logits.div_(sampling_info.temperatures)
batch_next_token_ids = self._sample_from_logits(
logits, sampling_info, simple_sampling_case, positions
)
logprobs = None
if return_logprob and not SGLANG_RETURN_ORIGINAL_LOGPROB:
logprobs = torch.log_softmax(logits, dim=-1)
return batch_next_token_ids, logprobs
def _attach_logprobs_to_output(
self,
logits_output: LogitsProcessorOutput,
logprobs: torch.Tensor,
top_logprobs_nums: List[int],
token_ids_logprobs: List[List[int]],
sampling_info: SamplingBatchInfo,
batch_next_token_ids: torch.Tensor,
):
# clamp to avoid -inf values
logprobs.clamp_(min=torch.finfo(logprobs.dtype).min)
# Attach logprobs to logits_output (in-place modification)
if any(x > 0 for x in top_logprobs_nums):
(
logits_output.next_token_top_logprobs_val,
logits_output.next_token_top_logprobs_idx,
) = get_top_logprobs(logprobs, top_logprobs_nums, no_copy_to_cpu=True)
if any(x is not None for x in token_ids_logprobs):
(
logits_output.next_token_token_ids_logprobs_val,
logits_output.next_token_token_ids_logprobs_idx,
) = get_token_ids_logprobs(
logprobs, token_ids_logprobs, no_copy_to_cpu=True
)
logits_output.next_token_logprobs = logprobs[
torch.arange(len(batch_next_token_ids), device=sampling_info.device),
batch_next_token_ids,
]
def _sync_token_ids_across_tp(
self, batch_next_token_ids: torch.Tensor, sampling_info: SamplingBatchInfo
):
if SYNC_TOKEN_IDS_ACROSS_TP or sampling_info.grammars:
# For performance reasons, SGLang does not sync the final token IDs across TP ranks by default.
# This saves one all-reduce, but the correctness of this approach depends on the determinism of several operators:
# the last all-reduce, the last lm_head matmul, and all sampling kernels.
# These kernels are deterministic in most cases, but there are some rare instances where they are not deterministic.
# In such cases, enable this env variable to prevent hanging due to TP ranks becoming desynchronized.
# When using xgrammar, this becomes more likely so we also do the sync when grammar is used.
torch.distributed.all_reduce(
batch_next_token_ids,
op=dist.ReduceOp.MIN,
group=self.tp_sync_group,
)
def compute_logprobs_only(
self,
logits_output: LogitsProcessorOutput,
sampling_info: SamplingBatchInfo,
return_logprob: bool,
top_logprobs_nums: List[int],
token_ids_logprobs: List[List[int]],
) -> None:
"""
Compute logprobs for requested token IDs without performing sampling.
Optimized for prefill-only scoring requests that need token probabilities
but don't require next token generation.
"""
if logits_output.next_token_logits is None:
logger.warning("No logits available for logprob computation")
return
# Check if any requests actually need logprobs computation
needs_token_ids_logprobs = any(
token_ids is not None and len(token_ids) > 0
for token_ids in token_ids_logprobs
)
needs_top_logprobs = any(x > 0 for x in top_logprobs_nums)
if not (needs_token_ids_logprobs or needs_top_logprobs):
return
# Preprocess logits (custom processors and NaN handling)
logits = self._preprocess_logits(logits_output.next_token_logits, sampling_info)
# Compute logprobs
logprobs = torch.nn.functional.log_softmax(logits, dim=-1)
# Handle top logprobs if requested
if needs_top_logprobs:
(
logits_output.next_token_top_logprobs_val,
logits_output.next_token_top_logprobs_idx,
) = get_top_logprobs(logprobs, top_logprobs_nums, no_copy_to_cpu=True)
# Handle token_ids logprobs if requested
if needs_token_ids_logprobs:
(
logits_output.next_token_token_ids_logprobs_val,
logits_output.next_token_token_ids_logprobs_idx,
) = get_token_ids_logprobs_batch_optimized(logprobs, token_ids_logprobs)
def register_sampler_backend(backend: str, factory: Callable[[], "Sampler"]) -> None:
"""Register a custom sampler factory for a backend string."""
if not backend:
raise ValueError("backend must be a non-empty string")
from sglang.srt.server_args import SAMPLING_BACKEND_CHOICES
if backend in _CUSTOM_SAMPLER_FACTORIES:
logger.warning("Overriding existing sampler factory for backend '%s'", backend)
SAMPLING_BACKEND_CHOICES.add(backend)
_CUSTOM_SAMPLER_FACTORIES[backend] = factory
def create_sampler(backend: Optional[str] = None) -> "Sampler":
"""Create a sampler honoring custom backend registrations."""
server_args = get_server_args()
backend = backend or (server_args.sampling_backend if server_args else None)
if backend in _CUSTOM_SAMPLER_FACTORIES:
sampler = _CUSTOM_SAMPLER_FACTORIES[backend]()
if not isinstance(sampler, Sampler):
raise TypeError(
f"Custom sampler factory for backend '{backend}' must return a Sampler"
)
return sampler
if backend is None or backend in _BUILT_IN_SAMPLING_BACKENDS:
return Sampler()
raise ValueError(
f"Unknown sampling backend '{backend}'. Register it via register_sampler_backend()."
)
def top_k_top_p_min_p_sampling_from_probs_torch(
probs: torch.Tensor,
top_ks: torch.Tensor,
top_ps: torch.Tensor,
min_ps: torch.Tensor,
need_min_p_sampling: bool,
sampling_seed: Optional[torch.Tensor],
positions: torch.Tensor,
):
"""
A top-k, top-p and min-p sampling implementation with native pytorch operations.
When sampling_seed is not None, deterministic inference will be enabled, it will sample
with the sampling_seed of each request.
"""
probs_sort, probs_idx = probs.sort(dim=-1, descending=True)
probs_sum = torch.cumsum(probs_sort, dim=-1)
probs_sort[
torch.arange(0, probs.shape[-1], device=probs.device).view(1, -1)
>= top_ks.view(-1, 1)
] = 0.0
probs_sort[(probs_sum - probs_sort) > top_ps.view(-1, 1)] = 0.0
if need_min_p_sampling:
# TODO: probs_sort should be re-normalized for the use of multinomial_with_seed
assert (
sampling_seed is None
), "With sampling seed, multinomial_with_seed will provide wrong results"
min_p_thresholds = probs_sort[:, 0] * min_ps
probs_sort[probs_sort < min_p_thresholds.view(-1, 1)] = 0.0
if sampling_seed is None:
sampled_index = torch.multinomial(probs_sort, num_samples=1)
else:
# NOTE: when using top-k/top-p/min-p sampling, we need to modify probs before we
# apply log to get logprobs. Therefore, we cannot use log_softmax directly.
# For now, we use log to the modified probs to get logprobs, but for numerical
# stability, we'd better come up with a solution to use log_softmax.
logprobs = probs_sort.to(torch.float64) # Using float64 for numerical stability
del probs_sort
logprobs.log_()
sampled_index = multinomial_with_seed(logprobs, sampling_seed, positions)
# int32 range is enough to represent the token ids
probs_idx = probs_idx.to(torch.int32)
batch_next_token_ids = torch.gather(probs_idx, dim=1, index=sampled_index).view(-1)
return batch_next_token_ids
def top_k_top_p_min_p_sampling_from_logits_ascend(
logits: torch.Tensor,
top_ks: torch.Tensor,
top_ps: torch.Tensor,
min_ps: torch.Tensor,
need_min_p_sampling: bool,
sampling_seed: Optional[torch.Tensor],
positions: torch.Tensor,
):
"""A top-k, top-p and min-p sampling implementation for ascend npu with torch_npu interface.
Takes temperature-scaled logits as input (softmax is applied internally).
"""
# torch_npu.npu_top_k_top_p requires top_k value range in [1, 1024]
if hasattr(torch_npu, "npu_top_k_top_p") and torch.all(
(top_ks <= 1024) & (top_ks >= 1)
):
logits_top_k_top_p = torch_npu.npu_top_k_top_p(logits, top_ps, top_ks)
probs_top_k_top_p = logits_top_k_top_p.softmax(dim=-1)
if need_min_p_sampling:
min_p_thresholds = probs_top_k_top_p.max(dim=-1) * min_ps
min_p_mask = probs_top_k_top_p < min_p_thresholds.view(-1, 1)
probs_top_k_top_p.masked_fill_(min_p_mask, 0.0)
if sampling_seed is None:
batch_next_token_ids = torch.multinomial(probs_top_k_top_p, num_samples=1)
else:
logprobs_top_k_top_p = probs_top_k_top_p.to(
torch.float64
) # Using float64 for numerical stability
del probs_top_k_top_p
logprobs_top_k_top_p.log_()
batch_next_token_ids = multinomial_with_seed(
logprobs_top_k_top_p, sampling_seed, positions
)
else:
probs = torch.softmax(logits, dim=-1)
probs_sort, probs_idx = probs.sort(dim=-1, descending=True)
# when top_k is -1 (in which sglang turns it to TOP_K_ALL), make it explicitly equal to logit's size
topk_all_mask = top_ks == TOP_K_ALL
top_ks.masked_fill_(topk_all_mask, probs.shape[1])
top_k_mask = torch.arange(0, probs.shape[-1], device=probs.device).view(
1, -1
) >= top_ks.view(-1, 1)
probs_sort.masked_fill_(top_k_mask, 0.0)
probs_sum = torch.cumsum(probs_sort, dim=-1)
top_p_mask = probs_sum - probs_sort > top_ps.view(-1, 1)
probs_sort.masked_fill_(top_p_mask, 0.0)
if need_min_p_sampling:
min_p_thresholds = probs_sort[:, 0] * min_ps
min_p_mask = probs_sort < min_p_thresholds.view(-1, 1)
probs_sort.masked_fill_(min_p_mask, 0.0)
if sampling_seed is None:
sampled_index = torch.multinomial(probs_sort, num_samples=1)
else:
logprobs = probs_sort.to(
torch.float64
) # Using float64 for numerical stability
del probs_sort
logprobs.log_()
sampled_index = multinomial_with_seed(logprobs, sampling_seed, positions)
probs_idx = probs_idx.to(torch.int32)
batch_next_token_ids = torch.gather(probs_idx, dim=1, index=sampled_index)
return batch_next_token_ids.view(-1)
@torch.compile(dynamic=True, disable=is_npu())
def multinomial_with_seed(
logprobs: torch.Tensor, seed: torch.Tensor, positions: torch.Tensor
) -> torch.Tensor:
"""
Samples n elements from an input tensor `inputs` of shape (n, m) using
a unique random seed for each row. This is a deterministic batched alternative to
`torch.multinomial`.
Args:
inputs: A float tensor of shape (n, m) representing n categorical
distributions with m categories each. The values are treated
as weights and do not need to sum to 1.
seed: An integer tensor of shape (n,) containing the random seed
for each corresponding row in `inputs`.
positions: The positions of the tokens in the sequence. Used for deterministic sampling
to get the unique seed for each position.
Returns:
A tensor of shape (n,) where the i-th element is an index sampled
from the distribution in `inputs[i]` using `seed[i]`.
"""
n, m = logprobs.shape
seed = seed.to(torch.uint64)
col_indices = torch.arange(m, device=logprobs.device)
hashed = murmur_hash32(seed, positions, col_indices)
# NOTE (sehoon): it is critical to keep gumbel noise calculation in float64 to avoid numerical instability.
# keeping logprobs in float64 is less critical, but we found it's still safer to keep it in float64.
x = hashed.to(torch.float64) / torch.iinfo(torch.uint32).max
# x is a uniform sample in [0, 1]. get gumbel noise from it.
# which is equivalent to -log(-log(x))
# keep everything in in-place operations to avoid unnecessary memory allocations.
x.log_().clamp_(min=torch.finfo(x.dtype).min).neg_() # -log(x)
x.log_().neg_() # -log(-log(x)) == gumbel noise
# add gumbel noise to logprobs
x.add_(logprobs.to(torch.float64))
return torch.argmax(x, dim=1, keepdim=True)
def sampling_from_probs_torch(
probs: torch.Tensor,
sampling_seed: Optional[torch.Tensor] = None,
positions: Optional[torch.Tensor] = None,
):
"""A sampling implementation with native pytorch operations, without
top-k, top-p, or min-p filtering.
Note: For deterministic sampling from logprobs, use Sampler._sample_from_logprobs instead.
"""
if sampling_seed is None:
sampled_index = torch.multinomial(probs, num_samples=1)
else:
# Deterministic sampling: convert probs to logprobs and use gumbel trick
sampled_index = multinomial_with_seed(
torch.log(probs), sampling_seed, positions
)
batch_next_token_ids = sampled_index.view(-1).to(torch.int32)
return batch_next_token_ids
def top_p_normalize_probs_torch(
probs: torch.Tensor,
top_ps: torch.Tensor,
):
# See also top_k_top_p_min_p_sampling_from_probs_torch
probs_sort, probs_idx = probs.sort(dim=-1, descending=True)
probs_sum = torch.cumsum(probs_sort, dim=-1)
probs_sort[(probs_sum - probs_sort) > top_ps.view(-1, 1)] = 0.0
probs_sort.div_(probs_sort.sum(dim=-1, keepdim=True))
return torch.zeros_like(probs_sort).scatter_(-1, probs_idx, probs_sort)
def get_token_ids_logprobs_batch_optimized(
logprobs: torch.Tensor,
token_ids_logprobs: List[List[int]],
) -> Tuple[List, List]:
"""
Vectorized batch processing for token ID logprobs extraction.
Uses a single GPU kernel call for the entire batch instead of multiple
separate calls, significantly improving performance for large batches.
Args:
logprobs: Log probabilities tensor [batch_size, vocab_size]
token_ids_logprobs: List of token IDs to extract logprobs for
Example:
# Input: batch_size=3, vocab_size=5
logprobs = torch.tensor([
[-1.2, -2.1, -0.8, -3.0, -1.5], # batch 0
[-0.5, -1.8, -2.2, -1.1, -2.7], # batch 1
[-2.0, -0.9, -1.4, -2.8, -1.6], # batch 2
])
token_ids_logprobs = [[1, 3], [2], [0, 2, 4]]
# Output:
# values = [tensor([-2.1, -3.0]), tensor([-2.2]), tensor([-2.0, -1.4, -1.6])]
# indices = [[1, 3], [2], [0, 2, 4]]
"""
batch_size = len(token_ids_logprobs)
device = logprobs.device
# Step 1: Calculate lengths for each request, treating None as empty list
# Example: [[1, 3], [2], [0, 2, 4]] -> token_lengths = tensor([2, 1, 3])
token_lengths = torch.tensor(
[len(token_ids or []) for token_ids in token_ids_logprobs], device=device
)
total_tokens = int(token_lengths.sum().item()) # 2 + 1 + 3 = 6
# Handle edge case where no tokens are requested
if total_tokens == 0:
return [logprobs.new_empty(0) for _ in token_ids_logprobs], [
[] for _ in token_ids_logprobs
]
# Step 2: Build flattened indices using torch operations
# Example: row_indices = [0, 0, 1, 2, 2, 2] (batch indices repeated by their lengths)
row_indices = torch.repeat_interleave(
torch.arange(batch_size, device=device), token_lengths
)
# Example: col_indices = [1, 3, 2, 0, 2, 4] (flattened token IDs from all requests)
col_indices = torch.tensor(
[
token_id
for token_ids in token_ids_logprobs
for token_id in (token_ids or [])
],
device=device,
dtype=torch.long,
)
# Step 3: Single vectorized gather operation
# Example: logprobs[row_indices, col_indices] -> [-2.1, -3.0, -2.2, -2.0, -1.4, -1.6]
gathered_logprobs = logprobs[row_indices, col_indices]
# Step 4: Split results back per request using torch operations
# Example: split tensor [6] into chunks of sizes [2, 1, 3] -> [tensor(2), tensor(1), tensor(3)]
split_logprobs = torch.split_with_sizes(
gathered_logprobs, token_lengths.tolist(), dim=0
)
# Step 5: Format output to match expected return structure
# Example: Convert split tensors back to list format with proper empty handling
# i=0: [1,3] -> append split_logprobs[0] and [1,3]
# i=1: [2] -> append split_logprobs[1] and [2]
# i=2: [0,2,4] -> append split_logprobs[2] and [0,2,4]
output_token_ids_logprobs_val = []
output_token_ids_logprobs_idx = []
for i, token_ids in enumerate(token_ids_logprobs):
if token_ids is not None and len(token_ids) > 0:
output_token_ids_logprobs_val.append(split_logprobs[i])
output_token_ids_logprobs_idx.append(token_ids)
else:
output_token_ids_logprobs_val.append(logprobs.new_empty(0))
output_token_ids_logprobs_idx.append([])
return output_token_ids_logprobs_val, output_token_ids_logprobs_idx
def apply_custom_logit_processor(
logits: torch.Tensor,
sampling_batch_info: SamplingBatchInfo,
num_tokens_in_batch: int = 1,
):
"""Apply custom logit processors to the logits.
This function will modify the logits in-place.
num_tokens_in_batch is needed to support spec decoding, where each batch can contain multiple
tokens. By default, we assume each batch contains only 1 token.
"""
assert logits.shape[0] == len(sampling_batch_info) * num_tokens_in_batch, (
f"The batch size of logits ({logits.shape[0]}) does not match the batch size of "
f"sampling_batch_info ({len(sampling_batch_info)}) x num_tokens_in_batch "
f"({num_tokens_in_batch})"
)
for _, (
processor,
batch_mask,
) in sampling_batch_info.custom_logit_processor.items():
# Get the batch indices that need to be processed
batch_indices = batch_mask.nonzero(as_tuple=True)[0]
assert batch_mask.shape[0] == len(sampling_batch_info), (
f"The number of batch mask ({batch_mask.shape[0]}) does not match the number of "
f"sampling_batch_info ({len(sampling_batch_info)})"
)
batch_mask = torch.repeat_interleave(batch_mask, num_tokens_in_batch)
# Apply the processor to the logits
logits[batch_mask] = processor(
logits[batch_mask],
[sampling_batch_info.custom_params[i] for i in batch_indices],
)
logger.debug(
f"Custom logit processor {processor.__class__.__name__} is applied."
)