Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

254 lines
9.7 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from tokenspeed_kernel.ops.sampling import argmax as sampling_argmax
from tokenspeed_kernel.ops.sampling.cuda import (
verify_chain_greedy as _verify_chain_greedy_cuda,
)
from tokenspeed_kernel.registry import error_fn
from tokenspeed.runtime.sampling.backends.base import (
SamplingBackend,
SamplingBackendConfig,
)
from tokenspeed.runtime.sampling.registry import register_backend
from tokenspeed.runtime.sampling.utils import gather_token_logprobs_torch
from tokenspeed.runtime.utils.nvtx import nvtx_range
from tokenspeed.runtime.utils.pdl import pdl_enabled
if TYPE_CHECKING:
from tokenspeed.runtime.layers.logits_processor import LogitsProcessorOutput
from tokenspeed.runtime.sampling.sampling_batch_info import SamplingBatchInfo
def _verify_chain_greedy_torch(
predicts: torch.Tensor, # [bs * N] int32, in/out
accept_index: torch.Tensor, # [bs, N] int32, in/out (-1-filled on entry)
accept_token_num: torch.Tensor, # [bs] int32, out
candidates: torch.Tensor, # [bs, N] int32
target_predict: torch.Tensor, # [bs, N] int64 (argmax output)
batch_size: int,
num_draft_tokens: int,
) -> None:
"""Pure-torch equivalent of tokenspeed_kernel.verify_chain_greedy.
Used on non-CUDA devices and when the CUDA kernel is unavailable.
"""
bs = batch_size
n = num_draft_tokens
# For i in 1..n-1: candidates[b, i] accepted iff it equals target_predict[b, i-1].
# Accepted prefix length per row = longest-leading-1s of the match array.
match = candidates[:, 1:] == target_predict[:, :-1].to(
candidates.dtype
) # [bs, n-1]
leading = torch.cumprod(match.to(torch.int32), dim=1) # [bs, n-1]
num_accepted = leading.sum(dim=1).to(torch.int32) # [bs]
# Fill all of `predicts` with target_predict; slots outside the accepted
# prefix are harmless because accept_index keeps them at -1 and callers
# mask on that. Matches the CUDA kernel's observable state.
predicts.copy_(target_predict.reshape(-1).to(torch.int32))
device = candidates.device
pos = torch.arange(n, device=device).unsqueeze(0) # [1, n]
batch_off = torch.arange(bs, device=device).unsqueeze(1) * n # [bs, 1]
flat_idx = (batch_off + pos).to(torch.int32) # [bs, n]
valid = pos <= num_accepted.unsqueeze(1) # [bs, n]
accept_index.copy_(torch.where(valid, flat_idx, torch.full_like(accept_index, -1)))
accept_token_num.copy_(num_accepted)
def _verify_chain_greedy(
predicts: torch.Tensor,
accept_index: torch.Tensor,
accept_token_num: torch.Tensor,
candidates: torch.Tensor,
target_predict: torch.Tensor,
batch_size: int,
num_draft_tokens: int,
enable_pdl: bool = False,
) -> None:
# Prefer the CUDA kernel when available AND the tensors are on CUDA.
if _verify_chain_greedy_cuda is not error_fn and candidates.is_cuda:
_verify_chain_greedy_cuda(
predicts=predicts,
accept_index=accept_index,
accept_token_num=accept_token_num,
candidates=candidates,
target_predict=target_predict,
batch_size=batch_size,
num_draft_tokens=num_draft_tokens,
enable_pdl=enable_pdl,
)
return
_verify_chain_greedy_torch(
predicts=predicts,
accept_index=accept_index,
accept_token_num=accept_token_num,
candidates=candidates,
target_predict=target_predict,
batch_size=batch_size,
num_draft_tokens=num_draft_tokens,
)
class GreedySamplingBackend(SamplingBackend):
"""Greedy-only backend: argmax for single-step, chain-greedy verify for
multi-step verification. No flashinfer / min_p / penalty machinery, no
coin buffers. Verify uses the fused CUDA kernel when available; falls
back to a pure-torch implementation otherwise (CPU, ROCm, etc.).
sampling_info is ignored for single-step (always argmax). verify() also
treats every request as greedy — stochastic verification is not
supported. Intended as the default backend and as a fallback when
flashinfer is unavailable."""
def __init__(self, config: SamplingBackendConfig) -> None:
super().__init__(config)
self._ones_buf = torch.ones(
(config.max_bs,), dtype=torch.int32, device=config.device
)
# Pre-allocated int32 buffer for ``sample``'s argmax output: lets the
# cute_dsl kernel write int32 token ids directly, skipping the
# ``.to(torch.int32)`` cast and its elementwise launch in the
# CUDA-graph-captured hot path.
self._sample_token_buf = torch.empty(
(config.max_bs,), dtype=torch.int32, device=config.device
)
self._predict_buf = torch.zeros(
(config.max_bs * config.max_draft_tokens_per_req,),
dtype=torch.int32,
device=config.device,
)
# Flat layout so [:bs * n].view(bs, n) is contiguous for any bs/n
# (required by maybe_broadcast / NCCL).
self._accept_index_buf = torch.zeros(
(config.max_bs * config.max_draft_tokens_per_req,),
dtype=torch.int32,
device=config.device,
)
self._accept_length_buf = torch.zeros(
(config.max_bs,), dtype=torch.int32, device=config.device
)
@nvtx_range("sampling:sample", color="yellow")
def sample(
self,
logits_output: LogitsProcessorOutput,
sampling_info: SamplingBatchInfo,
) -> tuple[torch.Tensor, torch.Tensor]:
logits = logits_output.next_token_logits
# Grammar bitmask apply — captured inside the CUDA graph. Buffer is
# pre-bound by bind_grammar_mask_buf; non-grammar rows stay all-ones
# so apply is a no-op.
if sampling_info.vocab_mask is not None:
sampling_info.apply_vocab_mask(
logits=logits, vocab_mask=sampling_info.vocab_mask
)
bs = logits.shape[0]
tokens = sampling_argmax(logits, out=self._sample_token_buf[:bs])
# TP-rank sync (rank 0 wins), mirrors FlashInferSamplingBackend.sample.
# All-gathered logits are not bit-identical across ranks, so per-rank
# argmax can diverge; an unsynced token id desyncs batch composition and
# deadlocks a downstream model all-reduce.
self.maybe_broadcast(tokens)
if self.config.enable_output_logprobs:
logits_output.next_token_logprobs = gather_token_logprobs_torch(
logits, tokens
)
return tokens, self._ones_buf[:bs]
@nvtx_range("sampling:verify", color="yellow")
def verify(
self,
logits_output: LogitsProcessorOutput,
sampling_info: SamplingBatchInfo,
candidates: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
bs = candidates.shape[0]
num_tokens_per_req = candidates.shape[1]
predict = self._predict_buf[: bs * num_tokens_per_req]
accept_index = (
self._accept_index_buf[: bs * num_tokens_per_req]
.view(bs, num_tokens_per_req)
.fill_(-1)
)
accept_length = self._accept_length_buf[:bs]
logits = logits_output.next_token_logits
# Per-draft-position grammar bitmask: buffer shape
# [bs * num_tokens_per_req, V/32] matches the flat target logits.
if sampling_info.vocab_mask is not None:
sampling_info.apply_vocab_mask(
logits=logits,
vocab_mask=sampling_info.vocab_mask,
)
target_predict = sampling_argmax(logits).reshape(bs, num_tokens_per_req)
_verify_chain_greedy(
predicts=predict,
accept_index=accept_index,
accept_token_num=accept_length,
candidates=candidates.to(torch.int32),
target_predict=target_predict,
batch_size=bs,
num_draft_tokens=num_tokens_per_req,
enable_pdl=pdl_enabled(),
)
accept_length += 1
# TP-rank sync on the full verify-output triple, mirrors
# FlashInferSamplingBackend.verify. Per-rank argmax / accept-length
# divergence (logits not bit-identical across ranks) desyncs batch
# composition and deadlocks the model all-reduce. Buffers are laid out
# flat so these views are NCCL-contiguous.
self.maybe_broadcast(predict, accept_index, accept_length)
if self.config.enable_output_logprobs:
logits_output.next_token_logprobs = gather_token_logprobs_torch(
logits, predict
)
return predict, accept_length
register_backend("greedy", GreedySamplingBackend)