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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:31 +08:00
commit 59a0a3844c
1103 changed files with 340460 additions and 0 deletions
@@ -0,0 +1,124 @@
# 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.
"""Sampling kernel entry points."""
from __future__ import annotations
import torch
from tokenspeed_kernel.profiling import ShapeCapture, kernel_scope
from tokenspeed_kernel.selection import NoKernelFoundError, select_kernel
from tokenspeed_kernel.signature import dense_tensor_format, format_signature
__all__ = ["argmax"]
_SUPPORTED_DTYPES = (torch.float16, torch.bfloat16, torch.float32)
_SUPPORTED_OUT_DTYPES = (torch.int32, torch.int64)
def _validate_argmax_out(logits: torch.Tensor, out: torch.Tensor) -> None:
if out.shape != (logits.shape[0],):
raise ValueError(
f"out must have shape (M,)={(logits.shape[0],)}, got {tuple(out.shape)}"
)
if out.dtype not in _SUPPORTED_OUT_DTYPES:
raise ValueError(f"out must be int32 or int64; got {out.dtype}")
if out.device != logits.device:
raise ValueError("out must be on the same device as logits")
def _argmax_torch_fallback(
logits: torch.Tensor,
*,
out: torch.Tensor | None = None,
) -> torch.Tensor:
if out is not None:
_validate_argmax_out(logits, out)
result = torch.argmax(logits, dim=-1)
if out is not None:
out.copy_(result)
return out
return result
def argmax(
logits: torch.Tensor,
*,
out: torch.Tensor | None = None,
solution: str | None = None,
override: str | None = None,
) -> torch.Tensor:
"""Return row-wise argmax indices over the last logits dimension.
Args:
logits: Input logits with shape ``(M, N)``. The argmax is taken
over the last dimension.
out: Optional output buffer with shape ``(M,)`` and dtype int32 or
int64 on the same device as ``logits``.
solution: Optional kernel solution to force through normal selection.
override: Optional exact kernel-name or solution override.
NaN handling:
Kernel-backed sampling paths treat NaNs as invalid candidates.
Rows with at least one non-NaN value return the index of the
maximum non-NaN value, with ties broken toward the lowest index.
Rows with no valid non-NaN values return the ``-1`` sentinel.
Unsupported inputs fall back to ``torch.argmax`` semantics.
Returns:
A tensor containing argmax indices for each row of ``logits``.
"""
if (
logits.dim() != 2
or logits.shape[0] == 0
or not logits.is_cuda
or logits.dtype not in _SUPPORTED_DTYPES
):
return _argmax_torch_fallback(logits, out=out)
signature = format_signature(logits=dense_tensor_format(logits.dtype))
try:
kernel = select_kernel(
"sampling",
"argmax",
signature,
solution=solution,
override=override,
)
except NoKernelFoundError:
return _argmax_torch_fallback(logits, out=out)
shape_params = {
"M": logits.shape[0],
"N": logits.shape[1],
"has_out": out is not None,
}
ShapeCapture.get().record(
"sampling", "argmax", kernel.name, logits.dtype, shape_params
)
with kernel_scope(
"sampling", "argmax", logits.dtype, kernel_name=kernel.name, **shape_params
):
return kernel(logits, out=out)
# Backend registration (side-effect imports).
import tokenspeed_kernel.ops.sampling.cute_dsl # noqa: E402,F401
import tokenspeed_kernel.ops.sampling.gluon # noqa: E402,F401
@@ -0,0 +1,59 @@
# 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 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.
"""CUDA sampling kernels."""
from tokenspeed_kernel.platform import current_platform
from tokenspeed_kernel.registry import error_fn
chain_speculative_sampling_target_only = error_fn
# Prepare is a side-stream registration helper. On non-NVIDIA the fused
# kernel doesn't exist, so callers should never reach the renorm path —
# but prepare gets called unconditionally from FlashInfer backends'
# __init__, so we keep it as a silent no-op rather than error_fn.
fused_topk_topp_prepare = lambda *_args, **_kwargs: None # noqa: E731
fused_topk_topp_renorm = error_fn
fused_topk_topp_workspace_size = error_fn
verify_chain_greedy = error_fn
if current_platform().is_nvidia:
try:
from tokenspeed_kernel.thirdparty.cuda.sampling_chain import (
chain_speculative_sampling_target_only,
verify_chain_greedy,
)
except ImportError:
pass
try:
from tokenspeed_kernel.thirdparty.cuda.fused_topk_topp import (
fused_topk_topp_renorm,
fused_topk_topp_workspace_size,
)
from tokenspeed_kernel.thirdparty.cuda.fused_topk_topp import (
prepare_for_device as fused_topk_topp_prepare,
)
except ImportError:
pass
__all__ = [
"chain_speculative_sampling_target_only",
"fused_topk_topp_prepare",
"fused_topk_topp_renorm",
"fused_topk_topp_workspace_size",
"verify_chain_greedy",
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
# 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 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.
"""FlashInfer sampling kernels."""
from tokenspeed_kernel.platform import current_platform
from tokenspeed_kernel.registry import error_fn
min_p_sampling_from_probs = error_fn
softmax = error_fn
top_k_renorm_prob = error_fn
top_k_top_p_sampling_from_probs = error_fn
top_k_top_p_sampling_from_logits = error_fn
top_p_renorm_prob = error_fn
top_p_renorm_probs = error_fn
if current_platform().is_nvidia:
try:
from flashinfer.sampling import (
min_p_sampling_from_probs,
top_k_renorm_prob,
top_k_top_p_sampling_from_logits,
top_k_top_p_sampling_from_probs,
top_p_renorm_prob,
top_p_renorm_probs,
)
except ImportError:
pass
try:
from tokenspeed_kernel.thirdparty.cuda.flashinfer_softmax import softmax
except ImportError:
pass
__all__ = [
"min_p_sampling_from_probs",
"softmax",
"top_k_renorm_prob",
"top_k_top_p_sampling_from_logits",
"top_k_top_p_sampling_from_probs",
"top_p_renorm_prob",
"top_p_renorm_probs",
]
@@ -0,0 +1,78 @@
# 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.
"""Registration shims for AMD Gluon sampling kernels."""
from __future__ import annotations
import torch
from tokenspeed_kernel.platform import ArchVersion, CapabilityRequirement
from tokenspeed_kernel.registry import Priority, register_kernel
from tokenspeed_kernel.signature import format_signatures
try:
from tokenspeed_kernel_amd.ops.sampling.gluon.argmax_gfx950 import (
gluon_argmax_gfx950 as _argmax_impl,
)
except ImportError as exc:
_IMPORT_ERROR = exc
_argmax_impl = None
else:
_IMPORT_ERROR = None
if _argmax_impl is not None:
@register_kernel(
"sampling",
"argmax",
name="gluon_argmax_gfx950",
solution="gluon",
capability=CapabilityRequirement(
min_arch_version=ArchVersion(9, 5),
max_arch_version=ArchVersion(9, 5),
vendors=frozenset({"amd"}),
),
signatures=format_signatures(
"logits", "dense", {torch.float16, torch.bfloat16, torch.float32}
),
priority=Priority.SPECIALIZED,
tags={"latency", "throughput"},
)
def gluon_argmax_gfx950(
logits: torch.Tensor,
*,
out: torch.Tensor | None = None,
) -> torch.Tensor:
return _argmax_impl(logits, out=out)
else:
def gluon_argmax_gfx950(
logits: torch.Tensor,
*,
out: torch.Tensor | None = None,
) -> torch.Tensor:
raise ImportError(
"gluon_argmax_gfx950 requires tokenspeed-kernel-amd"
) from _IMPORT_ERROR
__all__ = ["gluon_argmax_gfx950"]
@@ -0,0 +1,60 @@
# 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.
"""Triton sampling kernel entry points."""
from .common import gather_and_expand_scalars
from .generic import gumbel_sample_from_pools_generic
from .gumbel import (
gumbel_sample_from_pools,
gumbel_sample_from_pools_compact,
)
from .logprobs import selected_token_logprobs
from .min_p import (
gumbel_sample_min_p_from_pools,
gumbel_sample_min_p_from_pools_parallel,
)
from .penalties import accumulate_counts_inplace, apply_penalties_logit_bias_inplace
from .probability import min_p_renorm_prob
from .topk_topp import (
_QRITA_PERCENTILE_TO_STD_TABLE,
gumbel_sample_top_k_top_p_from_pools,
gumbel_sample_top_k_top_p_qrita_from_pools,
)
from .topp import gumbel_sample_top_p_parallel_from_pools
from .verify import verify_chain_target_sampled
__all__ = [
"_QRITA_PERCENTILE_TO_STD_TABLE",
"gather_and_expand_scalars",
"gumbel_sample_from_pools",
"gumbel_sample_from_pools_compact",
"gumbel_sample_min_p_from_pools",
"gumbel_sample_min_p_from_pools_parallel",
"gumbel_sample_from_pools_generic",
"gumbel_sample_top_p_parallel_from_pools",
"gumbel_sample_top_k_top_p_from_pools",
"gumbel_sample_top_k_top_p_qrita_from_pools",
"min_p_renorm_prob",
"apply_penalties_logit_bias_inplace",
"accumulate_counts_inplace",
"selected_token_logprobs",
"verify_chain_target_sampled",
]
@@ -0,0 +1,179 @@
# 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.
"""Triton sampling helper kernels."""
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
@triton.jit
def _gather_and_expand_scalars_kernel(
index_ptr,
temperature_ptr,
top_k_ptr,
top_p_ptr,
min_p_ptr,
seed_ptr,
offsets_ptr,
out_temperature_ptr,
out_top_k_ptr,
out_top_p_ptr,
out_min_p_ptr,
out_seed_ptr,
out_offsets_ptr,
n: tl.constexpr,
N_BLOCK: tl.constexpr,
ENABLE_PDL: tl.constexpr,
):
# PDL: wait for producer (e.g., penalty kernel writing into pools) to drain.
if ENABLE_PDL:
tl.extra.cuda.gdc_wait()
bi = tl.program_id(0)
idx = tl.load(index_ptr + bi)
t = tl.load(temperature_ptr + idx)
k = tl.load(top_k_ptr + idx)
p = tl.load(top_p_ptr + idx)
if min_p_ptr is not None:
mp = tl.load(min_p_ptr + idx)
if seed_ptr is not None:
s = tl.load(seed_ptr + idx)
if offsets_ptr is not None:
# Cast int32 valid_cache_lengths to int64 for flashinfer's offset arg.
o = tl.load(offsets_ptr + idx).to(tl.int64)
n_off = tl.arange(0, N_BLOCK)
mask = n_off < n
base = bi * n
tl.store(out_temperature_ptr + base + n_off, t, mask=mask)
tl.store(out_top_k_ptr + base + n_off, k, mask=mask)
tl.store(out_top_p_ptr + base + n_off, p, mask=mask)
if out_min_p_ptr is not None:
tl.store(out_min_p_ptr + base + n_off, mp, mask=mask)
if out_seed_ptr is not None:
tl.store(out_seed_ptr + base + n_off, s, mask=mask)
if out_offsets_ptr is not None:
tl.store(out_offsets_ptr + base + n_off, o, mask=mask)
# PDL: signal that dependents (e.g., flashinfer softmax) can begin preamble.
if ENABLE_PDL:
tl.extra.cuda.gdc_launch_dependents()
def gather_and_expand_scalars(
index: torch.Tensor,
*,
temperature: torch.Tensor,
top_k: torch.Tensor,
top_p: torch.Tensor,
min_p: torch.Tensor | None = None,
seed: torch.Tensor | None = None,
offsets: torch.Tensor | None = None,
n: int = 1,
enable_pdl: bool = False,
) -> tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor | None,
torch.Tensor | None,
torch.Tensor | None,
]:
"""Fused gather-and-broadcast for per-request sampling scalars.
Replaces the pattern ``index_select(pool, index)`` followed by
``repeat_interleave(..., n)`` across up to six streams with one Triton
launch. ``offsets`` (int32) is cast to int64 inside the kernel.
Optional streams (min_p, seed, offsets) pass through as ``None`` — Triton
specializes the kernel on pointer-None-ness at JIT time and the gated
load/store paths are dead-code-eliminated.
Args:
...
enable_pdl: opt into Programmatic Dependent Launch (Hopper+). Lets the
downstream flashinfer softmax/renorm kernels start their preamble
while our writes drain.
Returns ``(temperatures, top_ks, top_ps, min_ps_or_None, seeds_or_None,
offsets_or_None)``, each shape ``[bs * n]`` (or ``None`` when the
corresponding pool was omitted).
"""
bs = index.size(0)
total = bs * n
device = index.device
out_temperature = torch.empty(total, dtype=temperature.dtype, device=device)
out_top_k = torch.empty(total, dtype=top_k.dtype, device=device)
out_top_p = torch.empty(total, dtype=top_p.dtype, device=device)
out_min_p = (
torch.empty(total, dtype=min_p.dtype, device=device)
if min_p is not None
else None
)
out_seed = (
torch.empty(total, dtype=seed.dtype, device=device)
if seed is not None
else None
)
out_offsets = (
torch.empty(total, dtype=torch.int64, device=device)
if offsets is not None
else None
)
if bs == 0:
return (
out_temperature,
out_top_k,
out_top_p,
out_min_p,
out_seed,
out_offsets,
)
extra_kwargs = {"launch_pdl": True} if enable_pdl else {}
_gather_and_expand_scalars_kernel[(bs,)](
index,
temperature,
top_k,
top_p,
min_p,
seed,
offsets,
out_temperature,
out_top_k,
out_top_p,
out_min_p,
out_seed,
out_offsets,
n=n,
N_BLOCK=triton.next_power_of_2(max(n, 1)),
ENABLE_PDL=enable_pdl,
num_warps=1,
**extra_kwargs,
)
return out_temperature, out_top_k, out_top_p, out_min_p, out_seed, out_offsets
@@ -0,0 +1,321 @@
# SPDX-License-Identifier: MIT AND Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026 LightSeek Foundation
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
#
# 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.
# TokenSpeed-specific mixed-parameter route.
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
_GENERIC_GUMBEL_BLOCK_SIZE = 2048
_GENERIC_GUMBEL_TOP_K_PAD = 128
_GENERIC_GUMBEL_NUM_ATTEMPTS = 8
_TOP_K_DISABLED = 1 << 30
@triton.jit
def _gumbel_sample_generic_pool_kernel(
logits_ptr,
req_pool_indices_ptr,
temperature_pool_ptr,
top_k_pool_ptr,
top_p_pool_ptr,
min_p_pool_ptr,
seed_pool_ptr,
offsets_pool_ptr,
out_ptr,
logits_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
TOPK_PAD: tl.constexpr,
NUM_ATTEMPTS: tl.constexpr,
TOP_K_DISABLED: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
req_row = row // NUM_TOKENS_PER_REQ
spec_pos = row - req_row * NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
token_offsets = tl.arange(0, BLOCK_SIZE)
rank_offsets = tl.arange(0, TOPK_PAD)
temperature = tl.maximum(
tl.load(temperature_pool_ptr + pool_idx).to(tl.float32), 1.0e-20
)
top_k_raw = tl.load(top_k_pool_ptr + pool_idx)
top_p = tl.load(top_p_pool_ptr + pool_idx).to(tl.float32)
seed = tl.load(seed_pool_ptr + pool_idx).to(tl.int64)
offset = tl.load(offsets_pool_ptr + pool_idx).to(tl.int64) + spec_pos
top_k_disabled = top_k_raw == TOP_K_DISABLED
top_k = tl.minimum(tl.maximum(top_k_raw, 1), TOPK_PAD)
top_k = tl.minimum(top_k, vocab_size)
min_p = tl.full((), 0.0, tl.float32)
if min_p_pool_ptr is not None:
min_p = tl.load(min_p_pool_ptr + pool_idx).to(tl.float32)
min_p_log_threshold = tl.log(tl.maximum(min_p, 1.0e-20))
row_max = tl.full((), float("-inf"), tl.float32)
row_argmax = tl.full((), 2147483647, tl.int32)
top_vals = tl.full((TOPK_PAD,), float("-inf"), tl.float32)
top_ids = tl.full((TOPK_PAD,), 2147483647, tl.int32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + token_offsets
mask = cols < vocab_size
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
block_max = tl.max(vals, axis=0)
block_argmax = tl.min(tl.where(vals == block_max, cols, 2147483647), axis=0)
better_max = (block_max > row_max) | (
(block_max == row_max) & (block_argmax < row_argmax)
)
row_max = tl.where(better_max, block_max, row_max)
row_argmax = tl.where(better_max, block_argmax, row_argmax)
block_top_vals = tl.topk(vals, TOPK_PAD)
remaining = vals
for block_rank in tl.static_range(0, TOPK_PAD):
cand_val = tl.max(
tl.where(rank_offsets == block_rank, block_top_vals, float("-inf")),
axis=0,
)
cand_id = tl.min(
tl.where(mask & (remaining == cand_val), cols, 2147483647),
axis=0,
)
worst_val = tl.min(top_vals, axis=0)
worst_id = tl.max(
tl.where(top_vals == worst_val, top_ids, -1),
axis=0,
)
worst_pos = tl.min(
tl.where(
(top_vals == worst_val) & (top_ids == worst_id),
rank_offsets,
TOPK_PAD,
),
axis=0,
)
better = (cand_val > worst_val) | (
(cand_val == worst_val) & (cand_id < worst_id)
)
replace = (rank_offsets == worst_pos) & better
top_vals = tl.where(replace, cand_val, top_vals)
top_ids = tl.where(replace, cand_id, top_ids)
remaining = tl.where(cols == cand_id, float("-inf"), remaining)
sorted_vals = tl.full((TOPK_PAD,), float("-inf"), tl.float32)
sorted_ids = tl.full((TOPK_PAD,), 2147483647, tl.int32)
work_vals = top_vals
work_ids = top_ids
for rank in tl.static_range(0, TOPK_PAD):
best_val = tl.max(work_vals, axis=0)
best_id = tl.min(tl.where(work_vals == best_val, work_ids, 2147483647), axis=0)
active_rank = rank < top_k
sorted_vals = tl.where(
(rank_offsets == rank) & active_rank,
best_val,
sorted_vals,
)
sorted_ids = tl.where(
(rank_offsets == rank) & active_rank,
best_id,
sorted_ids,
)
work_vals = tl.where(work_ids == best_id, float("-inf"), work_vals)
top_max = tl.max(sorted_vals, axis=0)
top_weights = tl.exp(sorted_vals - top_max)
top_weights = tl.where(rank_offsets < top_k, top_weights, 0.0)
top_denom = tl.maximum(tl.sum(top_weights, axis=0), 1.0e-20)
top_probs = top_weights / top_denom
top_cumulative_before = tl.cumsum(top_probs) - top_probs
top_keep = (
(rank_offsets < top_k)
& (top_cumulative_before < top_p)
& (sorted_vals >= top_max + min_p_log_threshold)
)
finite_seed = tl.randint(seed, offset)
finite_rand_offsets = tl.where(top_keep, sorted_ids, 0)
finite_uniform = tl.maximum(tl.rand(finite_seed, finite_rand_offsets), 1.0e-7)
finite_gumbel = -tl.log(-tl.log(finite_uniform))
finite_scores = tl.where(top_keep, sorted_vals + finite_gumbel, float("-inf"))
finite_score = tl.max(finite_scores, axis=0)
finite_token = tl.min(
tl.where(finite_scores == finite_score, sorted_ids, 2147483647),
axis=0,
)
disabled_token = tl.full((), 2147483647, tl.int32)
disabled_found = tl.full((), 0, tl.int32)
for attempt in tl.static_range(0, NUM_ATTEMPTS):
attempt_seed = tl.randint(seed, offset + attempt)
best_score = tl.full((), float("-inf"), tl.float32)
best_id = tl.full((), 2147483647, tl.int32)
best_logit = tl.full((), float("-inf"), tl.float32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + token_offsets
mask = cols < vocab_size
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
uniform = tl.maximum(tl.rand(attempt_seed, cols), 1.0e-7)
gumbel = -tl.log(-tl.log(uniform))
scores = tl.where(mask, vals + gumbel, float("-inf"))
block_score = tl.max(scores, axis=0)
block_id = tl.min(tl.where(scores == block_score, cols, 2147483647), axis=0)
block_logit = tl.max(
tl.where(cols == block_id, vals, float("-inf")), axis=0
)
better = (block_score > best_score) | (
(block_score == best_score) & (block_id < best_id)
)
best_score = tl.where(better, block_score, best_score)
best_id = tl.where(better, block_id, best_id)
best_logit = tl.where(better, block_logit, best_logit)
total = tl.full((), 0.0, tl.float32)
before = tl.full((), 0.0, tl.float32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + token_offsets
mask = cols < vocab_size
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
weights = tl.exp(vals - row_max)
weights = tl.where(mask, weights, 0.0)
total += tl.sum(weights, axis=0)
before_mask = (vals > best_logit) | (
(vals == best_logit) & (cols < best_id)
)
before += tl.sum(tl.where(mask & before_mask, weights, 0.0), axis=0)
min_p_accept = best_logit >= row_max + min_p_log_threshold
accepted = (before < (top_p * total)) & min_p_accept
take = (disabled_found == 0) & accepted
disabled_token = tl.where(take, best_id, disabled_token)
disabled_found = tl.where(accepted, 1, disabled_found)
disabled_token = tl.where(disabled_found != 0, disabled_token, row_argmax)
token = tl.where(top_k_disabled, disabled_token, finite_token)
tl.store(out_ptr + row, token)
def gumbel_sample_from_pools_generic(
logits: torch.Tensor,
req_pool_indices: torch.Tensor,
temperature_pool: torch.Tensor,
top_k_pool: torch.Tensor,
top_p_pool: torch.Tensor,
seed_pool: torch.Tensor,
offsets_pool: torch.Tensor,
out: torch.Tensor,
*,
min_p_pool: torch.Tensor | None = None,
num_tokens_per_req: int = 1,
) -> torch.Tensor:
"""Graph-safe Gumbel sampler for mixed top-k/top-p rows."""
if logits.ndim != 2:
raise ValueError(f"gumbel_sample_from_pools_generic expects 2D logits")
if logits.device.type != "cuda":
raise ValueError("gumbel_sample_from_pools_generic requires CUDA logits")
if logits.stride(-1) != 1:
raise ValueError(
"gumbel_sample_from_pools_generic requires stride-1 vocab dimension, "
f"got stride={logits.stride()}"
)
rows, vocab_size = logits.shape
if vocab_size <= 0:
raise ValueError("gumbel_sample_from_pools_generic requires non-empty vocab")
if num_tokens_per_req <= 0:
raise ValueError("num_tokens_per_req must be positive")
if rows % num_tokens_per_req != 0:
raise ValueError(
"logits rows must be divisible by num_tokens_per_req, "
f"got rows={rows}, num_tokens_per_req={num_tokens_per_req}"
)
request_rows = rows // num_tokens_per_req
if req_pool_indices.shape[0] != request_rows:
raise ValueError(
"req_pool_indices length must match request rows, "
f"got {req_pool_indices.shape[0]} and {request_rows}"
)
if req_pool_indices.dtype != torch.int32:
raise ValueError(
f"req_pool_indices must be int32, got {req_pool_indices.dtype}"
)
if top_k_pool.dtype != torch.int32:
raise ValueError(f"top_k_pool must be int32, got {top_k_pool.dtype}")
if seed_pool.dtype != torch.int64:
raise ValueError(f"seed_pool must be int64, got {seed_pool.dtype}")
if out.dtype != torch.int32:
raise ValueError(f"out must be int32, got {out.dtype}")
if out.shape[0] < rows:
raise ValueError(f"out is too small: {out.shape[0]} < {rows}")
if min_p_pool is not None:
if min_p_pool.device.type != "cuda":
raise ValueError("min_p_pool must be CUDA")
if min_p_pool.ndim != 1:
raise ValueError(f"min_p_pool must be 1D, got {min_p_pool.ndim}D")
if rows == 0:
return out[:0]
_gumbel_sample_generic_pool_kernel[(rows,)](
logits,
req_pool_indices,
temperature_pool,
top_k_pool,
top_p_pool,
min_p_pool,
seed_pool,
offsets_pool,
out,
logits_row_stride=logits.stride(0),
vocab_size=vocab_size,
BLOCK_SIZE=_GENERIC_GUMBEL_BLOCK_SIZE,
TOPK_PAD=_GENERIC_GUMBEL_TOP_K_PAD,
NUM_ATTEMPTS=_GENERIC_GUMBEL_NUM_ATTEMPTS,
TOP_K_DISABLED=_TOP_K_DISABLED,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=8,
num_stages=3,
)
return out[:rows]
@@ -0,0 +1,359 @@
# SPDX-License-Identifier: MIT AND Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026 LightSeek Foundation
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
#
# 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.
# TokenSpeed-specific pool state, scratch ownership, and tie-breaking.
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
_GUMBEL_BLOCK_SIZE = 1024
_GUMBEL_COMPACT_BLOCK_SIZE = 2048
@triton.jit
def _gumbel_sample_pool_stage1_kernel(
logits_ptr,
req_pool_indices_ptr,
temperature_pool_ptr,
seed_pool_ptr,
offsets_pool_ptr,
local_ids_ptr,
local_scores_ptr,
logits_row_stride: tl.constexpr,
local_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
req_row = row // NUM_TOKENS_PER_REQ
spec_pos = row - req_row * NUM_TOKENS_PER_REQ
block_idx = tl.program_id(1)
token_offsets = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = token_offsets < vocab_size
pool_idx = tl.load(req_pool_indices_ptr + req_row)
logits = tl.load(
logits_ptr + row * logits_row_stride + token_offsets,
mask=mask,
other=float("-inf"),
).to(tl.float32)
temperature = tl.maximum(
tl.load(temperature_pool_ptr + pool_idx).to(tl.float32), 1.0e-20
)
seed = tl.load(seed_pool_ptr + pool_idx).to(tl.int64)
offset = tl.load(offsets_pool_ptr + pool_idx).to(tl.int64) + spec_pos
gumbel_seed = tl.randint(seed, offset)
uniform = tl.maximum(tl.rand(gumbel_seed, token_offsets), 1.0e-7)
gumbel = -tl.log(-tl.log(uniform))
scores = tl.where(mask, logits / temperature + gumbel, float("-inf"))
max_score = tl.max(scores, axis=0)
token_id = tl.min(
tl.where(scores == max_score, token_offsets, vocab_size + BLOCK_SIZE),
axis=0,
)
tl.store(local_ids_ptr + row * local_row_stride + block_idx, token_id)
tl.store(local_scores_ptr + row * local_row_stride + block_idx, max_score)
@triton.jit
def _gumbel_sample_stage2_kernel(
local_ids_ptr,
local_scores_ptr,
out_ptr,
local_row_stride: tl.constexpr,
num_blocks: tl.constexpr,
NUM_BLOCKS_PAD: tl.constexpr,
):
row = tl.program_id(0)
block_offsets = tl.arange(0, NUM_BLOCKS_PAD)
mask = block_offsets < num_blocks
scores = tl.load(
local_scores_ptr + row * local_row_stride + block_offsets,
mask=mask,
other=float("-inf"),
).to(tl.float32)
ids = tl.load(
local_ids_ptr + row * local_row_stride + block_offsets,
mask=mask,
other=2147483647,
)
max_score = tl.max(scores, axis=0)
token_id = tl.min(tl.where(scores == max_score, ids, 2147483647), axis=0)
tl.store(out_ptr + row, token_id)
@triton.jit
def _gumbel_sample_compact_pool_kernel(
logits_ptr,
req_pool_indices_ptr,
temperature_pool_ptr,
seed_pool_ptr,
offsets_pool_ptr,
out_ptr,
logits_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
req_row = row // NUM_TOKENS_PER_REQ
spec_pos = row - req_row * NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
token_offsets = tl.arange(0, BLOCK_SIZE)
temperature = tl.maximum(
tl.load(temperature_pool_ptr + pool_idx).to(tl.float32), 1.0e-20
)
seed = tl.load(seed_pool_ptr + pool_idx).to(tl.int64)
offset = tl.load(offsets_pool_ptr + pool_idx).to(tl.int64) + spec_pos
gumbel_seed = tl.randint(seed, offset)
best_score = tl.full((), float("-inf"), tl.float32)
best_id = tl.full((), 2147483647, tl.int32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + token_offsets
mask = cols < vocab_size
logits = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
uniform = tl.maximum(tl.rand(gumbel_seed, cols), 1.0e-7)
gumbel = -tl.log(-tl.log(uniform))
scores = tl.where(mask, logits / temperature + gumbel, float("-inf"))
block_score = tl.max(scores, axis=0)
block_id = tl.min(tl.where(scores == block_score, cols, 2147483647), axis=0)
better = (block_score > best_score) | (
(block_score == best_score) & (block_id < best_id)
)
best_score = tl.where(better, block_score, best_score)
best_id = tl.where(better, block_id, best_id)
tl.store(out_ptr + row, best_id)
def _check_gumbel_pool_inputs(
logits: torch.Tensor,
req_pool_indices: torch.Tensor,
temperature_pool: torch.Tensor,
seed_pool: torch.Tensor,
offsets_pool: torch.Tensor,
local_ids: torch.Tensor,
local_scores: torch.Tensor,
out: torch.Tensor,
*,
fn_name: str,
block_size: int,
num_tokens_per_req: int,
) -> tuple[int, int, int]:
if logits.ndim != 2:
raise ValueError(f"{fn_name} expects 2D logits, got {logits.ndim}D")
if logits.device.type != "cuda":
raise ValueError(f"{fn_name} requires CUDA logits")
if logits.stride(-1) != 1:
raise ValueError(
f"{fn_name} requires stride-1 vocab dimension, "
f"got stride={logits.stride()}"
)
rows, vocab_size = logits.shape
if vocab_size <= 0:
raise ValueError(f"{fn_name} requires non-empty vocab dimension")
for name, tensor, ndim in (
("req_pool_indices", req_pool_indices, 1),
("temperature_pool", temperature_pool, 1),
("seed_pool", seed_pool, 1),
("offsets_pool", offsets_pool, 1),
("out", out, 1),
):
if tensor.device.type != "cuda":
raise ValueError(f"{name} must be CUDA")
if tensor.ndim != ndim:
raise ValueError(f"{name} must be {ndim}D, got {tensor.ndim}D")
if num_tokens_per_req <= 0:
raise ValueError("num_tokens_per_req must be positive")
if rows % num_tokens_per_req != 0:
raise ValueError(
"logits rows must be divisible by num_tokens_per_req, "
f"got rows={rows}, num_tokens_per_req={num_tokens_per_req}"
)
request_rows = rows // num_tokens_per_req
if req_pool_indices.shape[0] != request_rows:
raise ValueError(
"req_pool_indices length must match request rows, "
f"got {req_pool_indices.shape[0]} and {request_rows}"
)
if out.shape[0] < rows:
raise ValueError(f"out is too small: {out.shape[0]} < {rows}")
if req_pool_indices.dtype != torch.int32:
raise ValueError(
f"req_pool_indices must be int32, got {req_pool_indices.dtype}"
)
if seed_pool.dtype != torch.int64:
raise ValueError(f"seed_pool must be int64, got {seed_pool.dtype}")
num_blocks = triton.cdiv(vocab_size, block_size)
if local_ids.device.type != "cuda" or local_scores.device.type != "cuda":
raise ValueError("gumbel pool scratch tensors must be CUDA")
if local_ids.ndim != 2 or local_scores.ndim != 2:
raise ValueError("gumbel pool scratch tensors must be 2D")
if local_ids.shape[0] < rows or local_scores.shape[0] < rows:
raise ValueError("gumbel pool scratch tensors have too few rows")
if local_ids.shape[1] < num_blocks or local_scores.shape[1] < num_blocks:
raise ValueError(
"gumbel pool scratch tensors have too few blocks: "
f"need {num_blocks}, got {local_ids.shape[1]} / {local_scores.shape[1]}"
)
if local_ids.dtype != torch.int32:
raise ValueError(f"local_ids must be int32, got {local_ids.dtype}")
if local_scores.dtype != torch.float32:
raise ValueError(f"local_scores must be float32, got {local_scores.dtype}")
if local_ids.stride(-1) != 1 or local_scores.stride(-1) != 1:
raise ValueError("gumbel pool scratch tensors require stride-1 block dimension")
return rows, vocab_size, num_blocks
def gumbel_sample_from_pools(
logits: torch.Tensor,
req_pool_indices: torch.Tensor,
temperature_pool: torch.Tensor,
seed_pool: torch.Tensor,
offsets_pool: torch.Tensor,
local_ids: torch.Tensor,
local_scores: torch.Tensor,
out: torch.Tensor,
*,
num_tokens_per_req: int = 1,
) -> torch.Tensor:
"""Sample token ids from logits with Gumbel-Max and pool-indexed scalars."""
rows, vocab_size, num_blocks = _check_gumbel_pool_inputs(
logits,
req_pool_indices,
temperature_pool,
seed_pool,
offsets_pool,
local_ids,
local_scores,
out,
fn_name="gumbel_sample_from_pools",
block_size=_GUMBEL_BLOCK_SIZE,
num_tokens_per_req=num_tokens_per_req,
)
if rows == 0:
return out[:0]
_gumbel_sample_pool_stage1_kernel[(rows, num_blocks)](
logits,
req_pool_indices,
temperature_pool,
seed_pool,
offsets_pool,
local_ids,
local_scores,
logits_row_stride=logits.stride(0),
local_row_stride=local_ids.stride(0),
vocab_size=vocab_size,
BLOCK_SIZE=_GUMBEL_BLOCK_SIZE,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=4,
)
_gumbel_sample_stage2_kernel[(rows,)](
local_ids,
local_scores,
out,
local_row_stride=local_ids.stride(0),
num_blocks=num_blocks,
NUM_BLOCKS_PAD=triton.next_power_of_2(num_blocks),
num_warps=1,
)
return out[:rows]
def gumbel_sample_from_pools_compact(
logits: torch.Tensor,
req_pool_indices: torch.Tensor,
temperature_pool: torch.Tensor,
seed_pool: torch.Tensor,
offsets_pool: torch.Tensor,
out: torch.Tensor,
*,
block_size: int = _GUMBEL_COMPACT_BLOCK_SIZE,
num_tokens_per_req: int = 1,
) -> torch.Tensor:
"""Single-kernel Gumbel-Max path for vocabularies that fit one scan loop."""
if logits.ndim != 2:
raise ValueError(
f"gumbel_sample_from_pools_compact expects 2D logits, got {logits.ndim}D"
)
if logits.device.type != "cuda":
raise ValueError("gumbel_sample_from_pools_compact requires CUDA logits")
if logits.stride(-1) != 1:
raise ValueError(
"gumbel_sample_from_pools_compact requires stride-1 vocab dimension, "
f"got stride={logits.stride()}"
)
rows, vocab_size = logits.shape
if vocab_size <= 0:
raise ValueError("gumbel_sample_from_pools_compact requires non-empty vocab")
if num_tokens_per_req <= 0:
raise ValueError("num_tokens_per_req must be positive")
if rows % num_tokens_per_req != 0:
raise ValueError(
"logits rows must be divisible by num_tokens_per_req, "
f"got rows={rows}, num_tokens_per_req={num_tokens_per_req}"
)
request_rows = rows // num_tokens_per_req
if req_pool_indices.shape[0] != request_rows:
raise ValueError(
"req_pool_indices length must match request rows, "
f"got {req_pool_indices.shape[0]} and {request_rows}"
)
if out.shape[0] < rows:
raise ValueError(f"out is too small: {out.shape[0]} < {rows}")
if rows == 0:
return out[:0]
_gumbel_sample_compact_pool_kernel[(rows,)](
logits,
req_pool_indices,
temperature_pool,
seed_pool,
offsets_pool,
out,
logits_row_stride=logits.stride(0),
vocab_size=vocab_size,
BLOCK_SIZE=block_size,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=8,
num_stages=3,
)
return out[:rows]
@@ -0,0 +1,109 @@
# SPDX-License-Identifier: MIT AND Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026 LightSeek Foundation
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
#
# 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.
# Computes selected-token logprobs without full-vocabulary materialization.
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
@triton.jit
def _selected_token_logprobs_kernel(
logits_ptr,
tokens_ptr,
out_ptr,
vocab_size: tl.constexpr,
logits_row_stride: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
):
row = tl.program_id(0)
offsets = tl.arange(0, BLOCK_SIZE)
row_ptr = logits_ptr + row * logits_row_stride
row_max = tl.full((), float("-inf"), tl.float32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + offsets
mask = cols < vocab_size
vals = tl.load(row_ptr + cols, mask=mask, other=float("-inf")).to(tl.float32)
row_max = tl.maximum(
row_max, tl.max(tl.where(mask, vals, float("-inf")), axis=0)
)
denom = tl.full((), 0.0, tl.float32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + offsets
mask = cols < vocab_size
vals = tl.load(row_ptr + cols, mask=mask, other=float("-inf")).to(tl.float32)
weights = tl.exp(vals - row_max)
denom += tl.sum(tl.where(mask, weights, 0.0), axis=0)
token = tl.load(tokens_ptr + row).to(tl.int64)
selected = tl.load(row_ptr + token).to(tl.float32)
tl.store(out_ptr + row, selected - row_max - tl.log(tl.maximum(denom, 1.0e-20)))
def selected_token_logprobs(
logits: torch.Tensor,
tokens: torch.Tensor,
out: torch.Tensor | None = None,
) -> torch.Tensor:
"""Compute ``log_softmax(logits)[row, tokens[row]]`` without materializing it."""
if logits.ndim != 2:
raise ValueError(f"selected_token_logprobs expects 2D logits")
if logits.device.type != "cuda":
raise ValueError("selected_token_logprobs requires CUDA logits")
if logits.stride(-1) != 1:
raise ValueError(
"selected_token_logprobs requires stride-1 vocab dimension, "
f"got stride={logits.stride()}"
)
rows, vocab_size = logits.shape
if tokens.numel() != rows:
raise ValueError(
f"tokens length must match rows, got {tokens.numel()} and {rows}"
)
if tokens.dtype not in (torch.int32, torch.int64):
raise ValueError(f"tokens must be int32 or int64, got {tokens.dtype}")
if out is None:
out = torch.empty((rows,), dtype=torch.float32, device=logits.device)
if out.dtype != torch.float32:
raise ValueError(f"out must be float32, got {out.dtype}")
if out.shape[0] < rows:
raise ValueError(f"out is too small: {out.shape[0]} < {rows}")
if rows == 0:
return out[:0]
_selected_token_logprobs_kernel[(rows,)](
logits,
tokens.reshape(-1),
out,
vocab_size=vocab_size,
logits_row_stride=logits.stride(0),
BLOCK_SIZE=1024,
num_warps=4,
num_stages=3,
)
return out[:rows]
@@ -0,0 +1,365 @@
# SPDX-License-Identifier: MIT AND Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026 LightSeek Foundation
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
#
# 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.
# TokenSpeed-native min-p Gumbel kernels.
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
from .gumbel import _check_gumbel_pool_inputs, _gumbel_sample_stage2_kernel
_MIN_P_GUMBEL_BLOCK_SIZE = 1024
_MIN_P_PARALLEL_BLOCK_SIZE = 1024
@triton.jit
def _gumbel_sample_min_p_pool_kernel(
logits_ptr,
req_pool_indices_ptr,
temperature_pool_ptr,
min_p_pool_ptr,
seed_pool_ptr,
offsets_pool_ptr,
out_ptr,
logits_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
req_row = row // NUM_TOKENS_PER_REQ
spec_pos = row - req_row * NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
offsets = tl.arange(0, BLOCK_SIZE)
temperature = tl.maximum(
tl.load(temperature_pool_ptr + pool_idx).to(tl.float32), 1.0e-20
)
min_p = tl.load(min_p_pool_ptr + pool_idx).to(tl.float32)
min_p_log_threshold = tl.log(tl.maximum(min_p, 1.0e-20))
row_max = tl.full((), float("-inf"), tl.float32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + offsets
mask = cols < vocab_size
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
row_max = tl.maximum(
row_max, tl.max(tl.where(mask, vals, float("-inf")), axis=0)
)
threshold = row_max + min_p_log_threshold
seed = tl.load(seed_pool_ptr + pool_idx).to(tl.int64)
step_offset = tl.load(offsets_pool_ptr + pool_idx).to(tl.int64) + spec_pos
rng_seed = tl.randint(seed, step_offset)
best_score = tl.full((), float("-inf"), tl.float32)
best_id = tl.full((), 2147483647, tl.int32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + offsets
mask = cols < vocab_size
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
keep = mask & (vals >= threshold)
uniform = tl.maximum(tl.rand(rng_seed, cols), 1.0e-7)
gumbel = -tl.log(-tl.log(uniform))
scores = tl.where(keep, vals + gumbel, float("-inf"))
block_score = tl.max(scores, axis=0)
block_id = tl.min(tl.where(scores == block_score, cols, 2147483647), axis=0)
better = (block_score > best_score) | (
(block_score == best_score) & (block_id < best_id)
)
best_score = tl.where(better, block_score, best_score)
best_id = tl.where(better, block_id, best_id)
tl.store(out_ptr + row, best_id)
def gumbel_sample_min_p_from_pools(
logits: torch.Tensor,
req_pool_indices: torch.Tensor,
temperature_pool: torch.Tensor,
min_p_pool: torch.Tensor,
seed_pool: torch.Tensor,
offsets_pool: torch.Tensor,
out: torch.Tensor,
*,
num_tokens_per_req: int = 1,
) -> torch.Tensor:
"""Gumbel-Max sampler for no top-k/top-p rows with a min-p cutoff."""
if logits.ndim != 2:
raise ValueError(f"gumbel_sample_min_p_from_pools expects 2D logits")
if logits.device.type != "cuda":
raise ValueError("gumbel_sample_min_p_from_pools requires CUDA logits")
if logits.stride(-1) != 1:
raise ValueError(
"gumbel_sample_min_p_from_pools requires stride-1 vocab dimension, "
f"got stride={logits.stride()}"
)
if num_tokens_per_req <= 0:
raise ValueError("num_tokens_per_req must be positive")
rows, vocab_size = logits.shape
if rows % num_tokens_per_req != 0:
raise ValueError(
"logits rows must be divisible by num_tokens_per_req, "
f"got rows={rows}, num_tokens_per_req={num_tokens_per_req}"
)
request_rows = rows // num_tokens_per_req
if req_pool_indices.shape[0] != request_rows:
raise ValueError(
"req_pool_indices length must match request rows, "
f"got {req_pool_indices.shape[0]} and {request_rows}"
)
if req_pool_indices.dtype != torch.int32:
raise ValueError(
f"req_pool_indices must be int32, got {req_pool_indices.dtype}"
)
if min_p_pool.ndim != 1:
raise ValueError(f"min_p_pool must be 1D, got {min_p_pool.ndim}D")
if seed_pool.dtype != torch.int64:
raise ValueError(f"seed_pool must be int64, got {seed_pool.dtype}")
if out.dtype != torch.int32:
raise ValueError(f"out must be int32, got {out.dtype}")
if out.shape[0] < rows:
raise ValueError(f"out is too small: {out.shape[0]} < {rows}")
if rows == 0:
return out[:0]
_gumbel_sample_min_p_pool_kernel[(rows,)](
logits,
req_pool_indices,
temperature_pool,
min_p_pool,
seed_pool,
offsets_pool,
out,
logits_row_stride=logits.stride(0),
vocab_size=vocab_size,
BLOCK_SIZE=_MIN_P_GUMBEL_BLOCK_SIZE,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=4,
num_stages=3,
)
return out[:rows]
@triton.jit
def _min_p_local_max_kernel(
logits_ptr,
req_pool_indices_ptr,
temperature_pool_ptr,
local_max_ptr,
logits_row_stride: tl.constexpr,
local_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
block_idx = tl.program_id(1)
req_row = row // NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
cols = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = cols < vocab_size
temperature = tl.maximum(
tl.load(temperature_pool_ptr + pool_idx).to(tl.float32), 1.0e-20
)
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
tl.store(local_max_ptr + row * local_row_stride + block_idx, tl.max(vals, axis=0))
@triton.jit
def _min_p_row_max_kernel(
local_max_ptr,
row_max_ptr,
local_row_stride: tl.constexpr,
num_blocks: tl.constexpr,
NUM_BLOCKS_PAD: tl.constexpr,
):
row = tl.program_id(0)
offsets = tl.arange(0, NUM_BLOCKS_PAD)
mask = offsets < num_blocks
vals = tl.load(
local_max_ptr + row * local_row_stride + offsets,
mask=mask,
other=float("-inf"),
).to(tl.float32)
tl.store(row_max_ptr + row, tl.max(vals, axis=0))
@triton.jit
def _min_p_local_gumbel_kernel(
logits_ptr,
req_pool_indices_ptr,
temperature_pool_ptr,
min_p_pool_ptr,
seed_pool_ptr,
offsets_pool_ptr,
row_max_ptr,
local_ids_ptr,
local_scores_ptr,
logits_row_stride: tl.constexpr,
local_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
block_idx = tl.program_id(1)
req_row = row // NUM_TOKENS_PER_REQ
spec_pos = row - req_row * NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
cols = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = cols < vocab_size
temperature = tl.maximum(
tl.load(temperature_pool_ptr + pool_idx).to(tl.float32), 1.0e-20
)
min_p = tl.load(min_p_pool_ptr + pool_idx).to(tl.float32)
threshold = tl.load(row_max_ptr + row).to(tl.float32) + tl.log(
tl.maximum(min_p, 1.0e-20)
)
seed = tl.load(seed_pool_ptr + pool_idx).to(tl.int64)
step_offset = tl.load(offsets_pool_ptr + pool_idx).to(tl.int64) + spec_pos
rng_seed = tl.randint(seed, step_offset)
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
keep = mask & (vals >= threshold)
uniform = tl.maximum(tl.rand(rng_seed, cols), 1.0e-7)
gumbel = -tl.log(-tl.log(uniform))
scores = tl.where(keep, vals + gumbel, float("-inf"))
block_score = tl.max(scores, axis=0)
block_id = tl.min(tl.where(scores == block_score, cols, 2147483647), axis=0)
tl.store(local_ids_ptr + row * local_row_stride + block_idx, block_id)
tl.store(local_scores_ptr + row * local_row_stride + block_idx, block_score)
def gumbel_sample_min_p_from_pools_parallel(
logits: torch.Tensor,
req_pool_indices: torch.Tensor,
temperature_pool: torch.Tensor,
min_p_pool: torch.Tensor,
seed_pool: torch.Tensor,
offsets_pool: torch.Tensor,
local_ids: torch.Tensor,
local_scores: torch.Tensor,
row_max: torch.Tensor,
out: torch.Tensor,
*,
num_tokens_per_req: int = 1,
) -> torch.Tensor:
"""Parallel large-vocab min-p Gumbel sampler."""
rows, vocab_size, num_blocks = _check_gumbel_pool_inputs(
logits,
req_pool_indices,
temperature_pool,
seed_pool,
offsets_pool,
local_ids,
local_scores,
out,
fn_name="gumbel_sample_min_p_from_pools_parallel",
block_size=_MIN_P_PARALLEL_BLOCK_SIZE,
num_tokens_per_req=num_tokens_per_req,
)
if min_p_pool.device.type != "cuda":
raise ValueError("min_p_pool must be CUDA")
if min_p_pool.ndim != 1:
raise ValueError(f"min_p_pool must be 1D, got {min_p_pool.ndim}D")
if row_max.device.type != "cuda":
raise ValueError("row_max must be CUDA")
if row_max.dtype != torch.float32:
raise ValueError(f"row_max must be float32, got {row_max.dtype}")
if row_max.shape[0] < rows:
raise ValueError(f"row_max is too small: {row_max.shape[0]} < {rows}")
if rows == 0:
return out[:0]
_min_p_local_max_kernel[(rows, num_blocks)](
logits,
req_pool_indices,
temperature_pool,
local_scores,
logits_row_stride=logits.stride(0),
local_row_stride=local_scores.stride(0),
vocab_size=vocab_size,
BLOCK_SIZE=_MIN_P_PARALLEL_BLOCK_SIZE,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=4,
)
_min_p_row_max_kernel[(rows,)](
local_scores,
row_max,
local_row_stride=local_scores.stride(0),
num_blocks=num_blocks,
NUM_BLOCKS_PAD=triton.next_power_of_2(num_blocks),
num_warps=1,
)
_min_p_local_gumbel_kernel[(rows, num_blocks)](
logits,
req_pool_indices,
temperature_pool,
min_p_pool,
seed_pool,
offsets_pool,
row_max,
local_ids,
local_scores,
logits_row_stride=logits.stride(0),
local_row_stride=local_ids.stride(0),
vocab_size=vocab_size,
BLOCK_SIZE=_MIN_P_PARALLEL_BLOCK_SIZE,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=4,
)
_gumbel_sample_stage2_kernel[(rows,)](
local_ids,
local_scores,
out,
local_row_stride=local_ids.stride(0),
num_blocks=num_blocks,
NUM_BLOCKS_PAD=triton.next_power_of_2(num_blocks),
num_warps=1,
)
return out[:rows]
@@ -0,0 +1,211 @@
# SPDX-License-Identifier: MIT AND Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026 LightSeek Foundation
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
#
# 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.
# TokenSpeed-owned penalty, logit-bias, and count kernels.
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
@triton.jit
def _apply_penalties_logit_bias_inplace_kernel(
logits_ptr,
req_pool_indices_ptr,
counts_ptr,
logit_bias_ptr,
freq_pen_pool_ptr,
pres_pen_pool_ptr,
rep_pen_pool_ptr,
vocab_size: tl.constexpr,
logits_row_stride: tl.constexpr,
counts_row_stride: tl.constexpr,
bias_row_stride: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
block = tl.program_id(1)
req_row = row // NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
cols = block * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = cols < vocab_size
logits_offsets = row * logits_row_stride + cols
state_offsets = pool_idx * counts_row_stride + cols
bias_offsets = pool_idx * bias_row_stride + cols
vals = tl.load(logits_ptr + logits_offsets, mask=mask, other=0.0).to(tl.float32)
counts = tl.load(counts_ptr + state_offsets, mask=mask, other=0).to(tl.float32)
active = counts > 0.0
rep = tl.load(rep_pen_pool_ptr + pool_idx).to(tl.float32)
freq = tl.load(freq_pen_pool_ptr + pool_idx).to(tl.float32)
presence = tl.load(pres_pen_pool_ptr + pool_idx).to(tl.float32)
rep_vals = tl.where(vals > 0.0, vals / rep, vals * rep)
vals = tl.where(active, rep_vals, vals)
vals = vals - freq * counts - presence * active.to(tl.float32)
vals += tl.load(logit_bias_ptr + bias_offsets, mask=mask, other=0.0).to(tl.float32)
tl.store(logits_ptr + logits_offsets, vals, mask=mask)
def apply_penalties_logit_bias_inplace(
logits: torch.Tensor,
req_pool_indices: torch.Tensor,
counts: torch.Tensor,
logit_bias: torch.Tensor,
freq_pen_pool: torch.Tensor,
pres_pen_pool: torch.Tensor,
rep_pen_pool: torch.Tensor,
*,
num_tokens_per_req: int = 1,
) -> torch.Tensor:
"""Apply repetition/frequency/presence penalties and logit_bias in-place."""
if logits.ndim != 2:
raise ValueError(f"logits must be 2D, got {logits.ndim}D")
if counts.ndim != 2 or logit_bias.ndim != 2:
raise ValueError("counts and logit_bias must be 2D")
if logits.device.type != "cuda":
raise ValueError("apply_penalties_logit_bias_inplace requires CUDA logits")
if logits.stride(-1) != 1:
raise ValueError(
"apply_penalties_logit_bias_inplace requires stride-1 vocab dimension, "
f"got stride={logits.stride()}"
)
if req_pool_indices.dtype != torch.int32:
raise ValueError(
f"req_pool_indices must be int32, got {req_pool_indices.dtype}"
)
if counts.dtype != torch.int32:
raise ValueError(f"counts must be int32, got {counts.dtype}")
if num_tokens_per_req <= 0:
raise ValueError("num_tokens_per_req must be positive")
rows, vocab_size = logits.shape
if rows % num_tokens_per_req != 0:
raise ValueError(
"logits rows must be divisible by num_tokens_per_req, "
f"got rows={rows}, num_tokens_per_req={num_tokens_per_req}"
)
request_rows = rows // num_tokens_per_req
if req_pool_indices.shape[0] != request_rows:
raise ValueError(
"req_pool_indices length must match request rows, "
f"got {req_pool_indices.shape[0]} and {request_rows}"
)
if counts.shape[1] < vocab_size or logit_bias.shape[1] < vocab_size:
raise ValueError(
"counts/logit_bias vocab dimension must cover logits vocab, "
f"got counts={counts.shape}, logit_bias={logit_bias.shape}, logits={logits.shape}"
)
if rows == 0:
return logits
num_blocks = triton.cdiv(vocab_size, 1024)
_apply_penalties_logit_bias_inplace_kernel[(rows, num_blocks)](
logits,
req_pool_indices,
counts,
logit_bias,
freq_pen_pool,
pres_pen_pool,
rep_pen_pool,
vocab_size=vocab_size,
logits_row_stride=logits.stride(0),
counts_row_stride=counts.stride(0),
bias_row_stride=logit_bias.stride(0),
BLOCK_SIZE=1024,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=4,
num_stages=3,
)
return logits
@triton.jit
def _accumulate_counts_inplace_kernel(
counts_ptr,
pool_idx_ptr,
tokens_ptr,
weights_ptr,
total: tl.constexpr,
counts_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
):
offs = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = offs < total
weights = tl.load(weights_ptr + offs, mask=mask, other=0).to(tl.int32)
pool_idx = tl.load(pool_idx_ptr + offs, mask=mask, other=0).to(tl.int64)
tokens = tl.load(tokens_ptr + offs, mask=mask, other=0).to(tl.int64)
valid = mask & (weights != 0) & (tokens >= 0) & (tokens < vocab_size)
tl.atomic_add(
counts_ptr + pool_idx * counts_row_stride + tokens,
weights,
sem="relaxed",
mask=valid,
)
def accumulate_counts_inplace(
counts: torch.Tensor,
pool_idx: torch.Tensor,
tokens: torch.Tensor,
weights: torch.Tensor,
) -> None:
"""Graph-safe ``counts[pool_idx, tokens] += weights``."""
if counts.ndim != 2:
raise ValueError(f"counts must be 2D, got {counts.ndim}D")
if counts.device.type != "cuda":
raise ValueError("accumulate_counts_inplace requires CUDA counts")
if counts.dtype != torch.int32:
raise ValueError(f"counts must be int32, got {counts.dtype}")
if pool_idx.dtype != torch.int32:
raise ValueError(f"pool_idx must be int32, got {pool_idx.dtype}")
if weights.dtype != torch.int32:
raise ValueError(f"weights must be int32, got {weights.dtype}")
if tokens.dtype not in (torch.int32, torch.int64):
raise ValueError(f"tokens must be int32 or int64, got {tokens.dtype}")
total = int(tokens.numel())
if pool_idx.numel() != total or weights.numel() != total:
raise ValueError(
"pool_idx, tokens, and weights must have the same number of elements"
)
if total == 0:
return
_accumulate_counts_inplace_kernel[(triton.cdiv(total, 256),)](
counts,
pool_idx.reshape(-1),
tokens.reshape(-1),
weights.reshape(-1),
total=total,
counts_row_stride=counts.stride(0),
vocab_size=counts.shape[1],
BLOCK_SIZE=256,
num_warps=4,
)
@@ -0,0 +1,128 @@
# 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.
# Probability-route compatibility helper for the existing FlashInfer full backend.
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
@triton.jit
def _min_p_renorm_prob_kernel(
probs_ptr,
min_p_ptr,
out_ptr,
vocab_size: tl.constexpr,
probs_row_stride: tl.constexpr,
out_row_stride: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
ENABLE_PDL: tl.constexpr,
):
if ENABLE_PDL:
tl.extra.cuda.gdc_wait()
row = tl.program_id(0)
offs = tl.arange(0, BLOCK_SIZE)
probs_row = probs_ptr + row * probs_row_stride
out_row = out_ptr + row * out_row_stride
max_prob = tl.full((), 0.0, tl.float32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + offs
mask = cols < vocab_size
vals = tl.load(probs_row + cols, mask=mask, other=0.0).to(tl.float32)
max_prob = tl.maximum(max_prob, tl.max(tl.where(mask, vals, 0.0), axis=0))
threshold = max_prob * tl.load(min_p_ptr + row).to(tl.float32)
denom = tl.full((), 0.0, tl.float32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + offs
mask = cols < vocab_size
vals = tl.load(probs_row + cols, mask=mask, other=0.0).to(tl.float32)
keep = mask & (vals >= threshold)
denom += tl.sum(tl.where(keep, vals, 0.0), axis=0)
inv_denom = 1.0 / tl.maximum(denom, 1.0e-20)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + offs
mask = cols < vocab_size
vals = tl.load(probs_row + cols, mask=mask, other=0.0).to(tl.float32)
keep = mask & (vals >= threshold)
out = tl.where(keep, vals * inv_denom, 0.0)
tl.store(out_row + cols, out, mask=mask)
if ENABLE_PDL:
tl.extra.cuda.gdc_launch_dependents()
def min_p_renorm_prob(
probs: torch.Tensor,
min_p: torch.Tensor,
*,
enable_pdl: bool = False,
) -> torch.Tensor:
"""Renormalize probabilities after applying a per-row min-p cutoff.
For each row, this computes ``threshold = min_p[row] * max(probs[row])``,
zeros probabilities below the threshold, and renormalizes the surviving
probabilities so the row sums to one.
"""
if probs.ndim != 2:
raise ValueError(f"min_p_renorm_prob expects 2D probs, got {probs.ndim}D")
if min_p.ndim != 1:
raise ValueError(f"min_p_renorm_prob expects 1D min_p, got {min_p.ndim}D")
if min_p.shape[0] != probs.shape[0]:
raise ValueError(
"min_p length must match probs rows, "
f"got {min_p.shape[0]} and {probs.shape[0]}"
)
if probs.device.type != "cuda" or min_p.device.type != "cuda":
raise ValueError("min_p_renorm_prob requires CUDA tensors")
if probs.stride(-1) != 1:
raise ValueError(
f"min_p_renorm_prob requires stride-1 vocab dimension, got stride={probs.stride()}"
)
if not min_p.is_contiguous():
min_p = min_p.contiguous()
out = torch.empty_like(probs)
rows, vocab_size = probs.shape
if rows == 0:
return out
block_size = min(4096, triton.next_power_of_2(vocab_size))
num_warps = 4 if block_size <= 1024 else 8
extra_kwargs = {"launch_pdl": True} if enable_pdl else {}
_min_p_renorm_prob_kernel[(rows,)](
probs,
min_p,
out,
vocab_size=vocab_size,
probs_row_stride=probs.stride(0),
out_row_stride=out.stride(0),
BLOCK_SIZE=block_size,
ENABLE_PDL=enable_pdl,
num_warps=num_warps,
num_stages=3,
**extra_kwargs,
)
return out
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,577 @@
# SPDX-License-Identifier: MIT AND Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026 LightSeek Foundation
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
#
# 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.
# TokenSpeed-specific top-p-only rejection/repair layout.
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
_TOP_P_PARALLEL_BLOCK_SIZE = 1024
_TOP_P_PARALLEL_NUM_ATTEMPTS = 3
_TOP_P_REPAIR_NUM_ATTEMPTS = 8
@triton.jit
def _top_p_parallel_stage1_kernel(
logits_ptr,
req_pool_indices_ptr,
temperature_pool_ptr,
seed_pool_ptr,
offsets_pool_ptr,
local_max_ptr,
local_sum_ptr,
local_argmax_ptr,
local_scores_ptr,
local_logits_ptr,
local_ids_ptr,
logits_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
num_blocks: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
NUM_ATTEMPTS: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
block = tl.program_id(1)
req_row = row // NUM_TOKENS_PER_REQ
spec_pos = row - req_row * NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
token_offsets = tl.arange(0, BLOCK_SIZE)
cols = block * BLOCK_SIZE + token_offsets
mask = cols < vocab_size
temperature = tl.maximum(
tl.load(temperature_pool_ptr + pool_idx).to(tl.float32), 1.0e-20
)
seed = tl.load(seed_pool_ptr + pool_idx).to(tl.int64)
offset = tl.load(offsets_pool_ptr + pool_idx).to(tl.int64) + spec_pos
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
block_max = tl.max(vals, axis=0)
safe_block_max = tl.where(block_max > -float("inf"), block_max, 0.0)
block_sum = tl.sum(
tl.where(mask & (vals > -float("inf")), tl.exp(vals - safe_block_max), 0.0),
axis=0,
)
block_argmax = tl.min(tl.where(vals == block_max, cols, 2147483647), axis=0)
block_base = row * num_blocks + block
tl.store(local_max_ptr + block_base, block_max)
tl.store(local_sum_ptr + block_base, block_sum)
tl.store(local_argmax_ptr + block_base, block_argmax)
for attempt in tl.static_range(0, NUM_ATTEMPTS):
attempt_seed = tl.randint(seed, offset + attempt)
uniform = tl.maximum(tl.rand(attempt_seed, cols), 1.0e-7)
gumbel = -tl.log(-tl.log(uniform))
scores = tl.where(mask, vals + gumbel, float("-inf"))
best_score = tl.max(scores, axis=0)
best_id = tl.min(tl.where(scores == best_score, cols, 2147483647), axis=0)
best_logit = tl.max(tl.where(cols == best_id, vals, float("-inf")), axis=0)
out_offset = block_base * NUM_ATTEMPTS + attempt
tl.store(local_scores_ptr + out_offset, best_score)
tl.store(local_logits_ptr + out_offset, best_logit)
tl.store(local_ids_ptr + out_offset, best_id)
@triton.jit
def _top_p_parallel_stage2_kernel(
local_max_ptr,
local_sum_ptr,
local_argmax_ptr,
local_scores_ptr,
local_logits_ptr,
local_ids_ptr,
row_max_ptr,
row_total_ptr,
row_argmax_ptr,
row_candidate_logits_ptr,
row_candidate_ids_ptr,
num_blocks: tl.constexpr,
NUM_BLOCKS_PAD: tl.constexpr,
NUM_ATTEMPTS: tl.constexpr,
):
row = tl.program_id(0)
block_offsets = tl.arange(0, NUM_BLOCKS_PAD)
block_mask = block_offsets < num_blocks
block_base = row * num_blocks + block_offsets
local_max = tl.load(
local_max_ptr + block_base, mask=block_mask, other=-float("inf")
)
local_sum = tl.load(local_sum_ptr + block_base, mask=block_mask, other=0.0)
row_max = tl.max(local_max, axis=0)
safe_row_max = tl.where(row_max > -float("inf"), row_max, 0.0)
total = tl.sum(
tl.where(block_mask, local_sum * tl.exp(local_max - safe_row_max), 0.0),
axis=0,
)
local_argmax = tl.load(
local_argmax_ptr + block_base, mask=block_mask, other=2147483647
)
row_argmax = tl.min(
tl.where((local_max == row_max) & block_mask, local_argmax, 2147483647),
axis=0,
)
tl.store(row_max_ptr + row, row_max)
tl.store(row_total_ptr + row, total)
tl.store(row_argmax_ptr + row, row_argmax)
for attempt in tl.static_range(0, NUM_ATTEMPTS):
candidate_base = (row * num_blocks + block_offsets) * NUM_ATTEMPTS + attempt
scores = tl.load(
local_scores_ptr + candidate_base, mask=block_mask, other=-float("inf")
)
ids = tl.load(local_ids_ptr + candidate_base, mask=block_mask, other=2147483647)
logits = tl.load(
local_logits_ptr + candidate_base, mask=block_mask, other=-float("inf")
)
best_score = tl.max(scores, axis=0)
best_id = tl.min(
tl.where((scores == best_score) & block_mask, ids, 2147483647),
axis=0,
)
best_logit = tl.max(tl.where(ids == best_id, logits, -float("inf")), axis=0)
row_candidate_offset = row * NUM_ATTEMPTS + attempt
tl.store(row_candidate_ids_ptr + row_candidate_offset, best_id)
tl.store(row_candidate_logits_ptr + row_candidate_offset, best_logit)
@triton.jit
def _top_p_parallel_stage3_kernel(
logits_ptr,
req_pool_indices_ptr,
temperature_pool_ptr,
row_max_ptr,
row_candidate_logits_ptr,
row_candidate_ids_ptr,
partial_before_ptr,
logits_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
num_blocks: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
NUM_ATTEMPTS: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
block = tl.program_id(1)
req_row = row // NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
token_offsets = tl.arange(0, BLOCK_SIZE)
cols = block * BLOCK_SIZE + token_offsets
mask = cols < vocab_size
row_max = tl.load(row_max_ptr + row)
temperature = tl.maximum(
tl.load(temperature_pool_ptr + pool_idx).to(tl.float32), 1.0e-20
)
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
weights = tl.exp(vals - row_max)
for attempt in tl.static_range(0, NUM_ATTEMPTS):
row_candidate_offset = row * NUM_ATTEMPTS + attempt
candidate_logit = tl.load(row_candidate_logits_ptr + row_candidate_offset)
candidate_id = tl.load(row_candidate_ids_ptr + row_candidate_offset)
before_mask = (vals > candidate_logit) | (
(vals == candidate_logit) & (cols < candidate_id)
)
before = tl.sum(tl.where(mask & before_mask, weights, 0.0), axis=0)
out_offset = (row * num_blocks + block) * NUM_ATTEMPTS + attempt
tl.store(partial_before_ptr + out_offset, before)
@triton.jit
def _top_p_parallel_stage4_kernel(
top_p_pool_ptr,
req_pool_indices_ptr,
row_total_ptr,
row_argmax_ptr,
row_candidate_ids_ptr,
partial_before_ptr,
accepted_ptr,
out_ptr,
num_blocks: tl.constexpr,
NUM_BLOCKS_PAD: tl.constexpr,
NUM_ATTEMPTS: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
req_row = row // NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
top_p = tl.load(top_p_pool_ptr + pool_idx).to(tl.float32)
target_mass = top_p * tl.load(row_total_ptr + row)
block_offsets = tl.arange(0, NUM_BLOCKS_PAD)
block_mask = block_offsets < num_blocks
token = tl.load(row_argmax_ptr + row)
found = tl.full((), 0, tl.int32)
for attempt in tl.static_range(0, NUM_ATTEMPTS):
before_base = (row * num_blocks + block_offsets) * NUM_ATTEMPTS + attempt
before = tl.sum(
tl.load(partial_before_ptr + before_base, mask=block_mask, other=0.0),
axis=0,
)
accepted = before < target_mass
candidate_id = tl.load(row_candidate_ids_ptr + row * NUM_ATTEMPTS + attempt)
take = (found == 0) & accepted
token = tl.where(take, candidate_id, token)
found = tl.where(take, 1, found)
tl.store(accepted_ptr + row, found)
tl.store(out_ptr + row, token)
@triton.jit
def _top_p_parallel_repair_kernel(
logits_ptr,
req_pool_indices_ptr,
temperature_pool_ptr,
top_p_pool_ptr,
seed_pool_ptr,
offsets_pool_ptr,
row_max_ptr,
row_total_ptr,
row_argmax_ptr,
accepted_ptr,
out_ptr,
logits_row_stride: tl.constexpr,
vocab_size: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
START_ATTEMPT: tl.constexpr,
NUM_ATTEMPTS_TOTAL: tl.constexpr,
NUM_TOKENS_PER_REQ: tl.constexpr,
):
row = tl.program_id(0)
accepted_found = tl.load(accepted_ptr + row)
accepted_token = tl.load(out_ptr + row)
req_row = row // NUM_TOKENS_PER_REQ
spec_pos = row - req_row * NUM_TOKENS_PER_REQ
pool_idx = tl.load(req_pool_indices_ptr + req_row)
token_offsets = tl.arange(0, BLOCK_SIZE)
temperature = tl.maximum(
tl.load(temperature_pool_ptr + pool_idx).to(tl.float32), 1.0e-20
)
top_p = tl.load(top_p_pool_ptr + pool_idx).to(tl.float32)
seed = tl.load(seed_pool_ptr + pool_idx).to(tl.int64)
offset = tl.load(offsets_pool_ptr + pool_idx).to(tl.int64) + spec_pos
row_max = tl.load(row_max_ptr + row)
total = tl.load(row_total_ptr + row)
target_mass = top_p * total
row_argmax = tl.load(row_argmax_ptr + row)
attempt = tl.full((), START_ATTEMPT, tl.int32)
while (attempt < NUM_ATTEMPTS_TOTAL) & (accepted_found == 0):
attempt_seed = tl.randint(seed, offset + attempt)
best_score = tl.full((), float("-inf"), tl.float32)
best_id = tl.full((), 2147483647, tl.int32)
best_logit = tl.full((), float("-inf"), tl.float32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + token_offsets
mask = cols < vocab_size
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
uniform = tl.maximum(tl.rand(attempt_seed, cols), 1.0e-7)
gumbel = -tl.log(-tl.log(uniform))
scores = tl.where(mask, vals + gumbel, float("-inf"))
block_score = tl.max(scores, axis=0)
block_id = tl.min(tl.where(scores == block_score, cols, 2147483647), axis=0)
block_logit = tl.max(
tl.where(cols == block_id, vals, float("-inf")), axis=0
)
better = (block_score > best_score) | (
(block_score == best_score) & (block_id < best_id)
)
best_score = tl.where(better, block_score, best_score)
best_id = tl.where(better, block_id, best_id)
best_logit = tl.where(better, block_logit, best_logit)
before = tl.full((), 0.0, tl.float32)
for start in tl.range(0, vocab_size, BLOCK_SIZE, num_stages=3):
cols = start + token_offsets
mask = cols < vocab_size
vals = tl.load(
logits_ptr + row * logits_row_stride + cols,
mask=mask,
other=float("-inf"),
).to(tl.float32)
vals = vals / temperature
weights = tl.exp(vals - row_max)
before_mask = (vals > best_logit) | (
(vals == best_logit) & (cols < best_id)
)
before += tl.sum(tl.where(mask & before_mask, weights, 0.0), axis=0)
accepted = before < target_mass
accepted_token = tl.where(accepted, best_id, accepted_token)
accepted_found = tl.where(accepted, 1, accepted_found)
attempt += 1
token = tl.where(accepted_found != 0, accepted_token, row_argmax)
tl.store(out_ptr + row, token)
tl.store(accepted_ptr + row, accepted_found)
def gumbel_sample_top_p_parallel_from_pools(
logits: torch.Tensor,
req_pool_indices: torch.Tensor,
temperature_pool: torch.Tensor,
top_p_pool: torch.Tensor,
seed_pool: torch.Tensor,
offsets_pool: torch.Tensor,
local_max: torch.Tensor,
local_sum: torch.Tensor,
local_argmax: torch.Tensor,
local_scores: torch.Tensor,
local_logits: torch.Tensor,
local_ids: torch.Tensor,
row_max: torch.Tensor,
row_total: torch.Tensor,
row_argmax: torch.Tensor,
row_candidate_logits: torch.Tensor,
row_candidate_ids: torch.Tensor,
accepted: torch.Tensor,
out: torch.Tensor,
*,
block_size: int = _TOP_P_PARALLEL_BLOCK_SIZE,
num_attempts: int = _TOP_P_PARALLEL_NUM_ATTEMPTS,
num_tokens_per_req: int = 1,
) -> torch.Tensor:
"""Block-parallel top-p-only Gumbel sampler."""
if logits.ndim != 2:
raise ValueError("gumbel_sample_top_p_parallel_from_pools expects 2D logits")
if logits.device.type != "cuda":
raise ValueError("gumbel_sample_top_p_parallel_from_pools requires CUDA logits")
if logits.stride(-1) != 1:
raise ValueError(
"gumbel_sample_top_p_parallel_from_pools requires stride-1 vocab dimension, "
f"got stride={logits.stride()}"
)
rows, vocab_size = logits.shape
if vocab_size <= 0:
raise ValueError(
"gumbel_sample_top_p_parallel_from_pools requires non-empty vocab"
)
if num_tokens_per_req <= 0:
raise ValueError("num_tokens_per_req must be positive")
if rows % num_tokens_per_req != 0:
raise ValueError(
"logits rows must be divisible by num_tokens_per_req, "
f"got rows={rows}, num_tokens_per_req={num_tokens_per_req}"
)
request_rows = rows // num_tokens_per_req
if req_pool_indices.shape[0] != request_rows:
raise ValueError(
"req_pool_indices length must match request rows for parallel top-p sample, "
f"got {req_pool_indices.shape[0]} and {request_rows}"
)
if num_attempts <= 0:
raise ValueError("num_attempts must be positive")
num_blocks = triton.cdiv(vocab_size, block_size)
num_blocks_pad = triton.next_power_of_2(num_blocks)
for name, tensor, dtype in (
("req_pool_indices", req_pool_indices, torch.int32),
("seed_pool", seed_pool, torch.int64),
("local_argmax", local_argmax, torch.int32),
("local_ids", local_ids, torch.int32),
("row_argmax", row_argmax, torch.int32),
("row_candidate_ids", row_candidate_ids, torch.int32),
("accepted", accepted, torch.int32),
("out", out, torch.int32),
):
if tensor.device.type != "cuda":
raise ValueError(f"{name} must be CUDA")
if tensor.dtype != dtype:
raise ValueError(f"{name} must be {dtype}, got {tensor.dtype}")
for name, tensor in (
("temperature_pool", temperature_pool),
("top_p_pool", top_p_pool),
("offsets_pool", offsets_pool),
("local_max", local_max),
("local_sum", local_sum),
("local_scores", local_scores),
("local_logits", local_logits),
("row_max", row_max),
("row_total", row_total),
("row_candidate_logits", row_candidate_logits),
):
if tensor.device.type != "cuda":
raise ValueError(f"{name} must be CUDA")
if out.shape[0] < rows:
raise ValueError(f"out is too small: {out.shape[0]} < {rows}")
if rows == 0:
return out[:0]
local_shape = (rows, num_blocks)
candidate_shape = (rows, num_blocks, num_attempts)
row_candidate_shape = (rows, num_attempts)
if local_max.shape[0] < rows or local_max.shape[1] < num_blocks:
raise ValueError(f"local_max must cover {local_shape}, got {local_max.shape}")
if local_sum.shape[0] < rows or local_sum.shape[1] < num_blocks:
raise ValueError(f"local_sum must cover {local_shape}, got {local_sum.shape}")
if local_argmax.shape[0] < rows or local_argmax.shape[1] < num_blocks:
raise ValueError(
f"local_argmax must cover {local_shape}, got {local_argmax.shape}"
)
for name, tensor in (
("local_scores", local_scores),
("local_logits", local_logits),
("local_ids", local_ids),
):
if (
tensor.shape[0] < rows
or tensor.shape[1] < num_blocks
or tensor.shape[2] < num_attempts
):
raise ValueError(f"{name} must cover {candidate_shape}, got {tensor.shape}")
for name, tensor in (
("row_max", row_max),
("row_total", row_total),
("row_argmax", row_argmax),
("accepted", accepted),
):
if tensor.shape[0] < rows:
raise ValueError(f"{name} is too small: {tensor.shape[0]} < {rows}")
for name, tensor in (
("row_candidate_logits", row_candidate_logits),
("row_candidate_ids", row_candidate_ids),
):
if tensor.shape[0] < rows or tensor.shape[1] < num_attempts:
raise ValueError(
f"{name} must cover {row_candidate_shape}, got {tensor.shape}"
)
_top_p_parallel_stage1_kernel[(rows, num_blocks)](
logits,
req_pool_indices,
temperature_pool,
seed_pool,
offsets_pool,
local_max,
local_sum,
local_argmax,
local_scores,
local_logits,
local_ids,
logits_row_stride=logits.stride(0),
vocab_size=vocab_size,
num_blocks=num_blocks,
BLOCK_SIZE=block_size,
NUM_ATTEMPTS=num_attempts,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=4,
num_stages=3,
)
_top_p_parallel_stage2_kernel[(rows,)](
local_max,
local_sum,
local_argmax,
local_scores,
local_logits,
local_ids,
row_max,
row_total,
row_argmax,
row_candidate_logits,
row_candidate_ids,
num_blocks=num_blocks,
NUM_BLOCKS_PAD=num_blocks_pad,
NUM_ATTEMPTS=num_attempts,
num_warps=8,
num_stages=3,
)
_top_p_parallel_stage3_kernel[(rows, num_blocks)](
logits,
req_pool_indices,
temperature_pool,
row_max,
row_candidate_logits,
row_candidate_ids,
local_scores,
logits_row_stride=logits.stride(0),
vocab_size=vocab_size,
num_blocks=num_blocks,
BLOCK_SIZE=block_size,
NUM_ATTEMPTS=num_attempts,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=4,
num_stages=3,
)
_top_p_parallel_stage4_kernel[(rows,)](
top_p_pool,
req_pool_indices,
row_total,
row_argmax,
row_candidate_ids,
local_scores,
accepted,
out,
num_blocks=num_blocks,
NUM_BLOCKS_PAD=num_blocks_pad,
NUM_ATTEMPTS=num_attempts,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=8,
num_stages=3,
)
if num_attempts < _TOP_P_REPAIR_NUM_ATTEMPTS:
_top_p_parallel_repair_kernel[(rows,)](
logits,
req_pool_indices,
temperature_pool,
top_p_pool,
seed_pool,
offsets_pool,
row_max,
row_total,
row_argmax,
accepted,
out,
logits_row_stride=logits.stride(0),
vocab_size=vocab_size,
BLOCK_SIZE=block_size,
START_ATTEMPT=num_attempts,
NUM_ATTEMPTS_TOTAL=_TOP_P_REPAIR_NUM_ATTEMPTS,
NUM_TOKENS_PER_REQ=num_tokens_per_req,
num_warps=4,
num_stages=3,
)
return out[:rows]
@@ -0,0 +1,131 @@
# SPDX-License-Identifier: MIT AND Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026 LightSeek Foundation
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
#
# 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.
# TokenSpeed samples target rows first, then verifies by token-id comparison.
from __future__ import annotations
import torch
from tokenspeed_kernel._triton import tl, triton
@triton.jit
def _verify_chain_target_sampled_kernel(
predicts_ptr,
accept_index_ptr,
accept_token_num_ptr,
candidates_ptr,
target_sampled_ptr,
NUM_DRAFT_TOKENS: tl.constexpr,
ENABLE_PDL: tl.constexpr,
):
if ENABLE_PDL:
tl.extra.cuda.gdc_wait()
row = tl.program_id(0)
base = row * NUM_DRAFT_TOKENS
tl.store(accept_index_ptr + base, base)
active = tl.full((), 1, tl.int32)
num_accepted = tl.full((), 0, tl.int32)
for i in tl.range(1, NUM_DRAFT_TOKENS):
target_id = tl.load(target_sampled_ptr + base + i - 1)
draft_id = tl.load(candidates_ptr + base + i)
match = (active != 0) & (draft_id == target_id)
tl.store(predicts_ptr + base + i - 1, target_id, mask=match)
tl.store(accept_index_ptr + base + i, base + i, mask=match)
num_accepted = tl.where(match, i, num_accepted)
active = tl.where(match, 1, 0)
final_id = tl.load(target_sampled_ptr + base + num_accepted)
tl.store(accept_token_num_ptr + row, num_accepted)
tl.store(predicts_ptr + base + num_accepted, final_id)
if ENABLE_PDL:
tl.extra.cuda.gdc_launch_dependents()
def verify_chain_target_sampled(
predicts: torch.Tensor,
accept_index: torch.Tensor,
accept_token_num: torch.Tensor,
candidates: torch.Tensor,
target_sampled: torch.Tensor,
*,
enable_pdl: bool = False,
) -> None:
"""Verify a speculative chain against already-sampled target tokens."""
if candidates.ndim != 2:
raise ValueError(f"candidates must be 2D, got {candidates.ndim}D")
if accept_index.shape != candidates.shape:
raise ValueError(
f"accept_index shape {accept_index.shape} must match candidates {candidates.shape}"
)
bs, num_draft_tokens = candidates.shape
total = bs * num_draft_tokens
if predicts.shape[0] < total:
raise ValueError(f"predicts is too small: {predicts.shape[0]} < {total}")
if accept_token_num.shape[0] < bs:
raise ValueError(
f"accept_token_num is too small: {accept_token_num.shape[0]} < {bs}"
)
if target_sampled.numel() < total:
raise ValueError(
f"target_sampled is too small: {target_sampled.numel()} < {total}"
)
if candidates.dtype != torch.int32:
raise ValueError(f"candidates must be int32, got {candidates.dtype}")
if predicts.dtype != torch.int32:
raise ValueError(f"predicts must be int32, got {predicts.dtype}")
if accept_index.dtype != torch.int32:
raise ValueError(f"accept_index must be int32, got {accept_index.dtype}")
if accept_token_num.dtype != torch.int32:
raise ValueError(
f"accept_token_num must be int32, got {accept_token_num.dtype}"
)
if target_sampled.dtype not in (torch.int32, torch.int64):
raise ValueError(
f"target_sampled must be int32 or int64, got {target_sampled.dtype}"
)
if candidates.device.type != "cuda":
raise ValueError("verify_chain_target_sampled requires CUDA tensors")
if bs == 0:
return
target_sampled = target_sampled.reshape(-1)
extra_kwargs = {"launch_pdl": True} if enable_pdl else {}
_verify_chain_target_sampled_kernel[(bs,)](
predicts,
accept_index,
accept_token_num,
candidates,
target_sampled,
NUM_DRAFT_TOKENS=num_draft_tokens,
ENABLE_PDL=enable_pdl,
num_warps=1,
**extra_kwargs,
)