Add token-id verification to the KV-canary (#26818)

This commit is contained in:
fzyzcjy
2026-05-31 09:58:51 +08:00
committed by GitHub
parent 0ca610a6df
commit 6be4b32d8d
18 changed files with 983 additions and 13 deletions
@@ -0,0 +1,95 @@
from __future__ import annotations
from dataclasses import dataclass
import torch
import triton
import triton.testing
from sglang.jit_kernel.benchmark.utils import (
DEFAULT_DEVICE,
get_benchmark_range,
run_benchmark_no_cudagraph,
)
from sglang.jit_kernel.kv_canary.scatter_req_token_ids import (
launch_scatter_req_token_ids_kernel,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=180, suite="nightly-kernel-1-gpu", nightly=True)
_BS_AXIS_FULL: list[int] = [1, 8, 64, 256]
_SEQ_LEN_AXIS_FULL: list[int] = [128, 512, 2048, 8192]
_BS_AXIS_CI: list[int] = [1, 64]
_SEQ_LEN_AXIS_CI: list[int] = [512, 2048]
@dataclass(frozen=True, slots=True, kw_only=True)
class _BenchCase:
bs: int
seq_len: int
def _build_cases() -> list[_BenchCase]:
bs_axis = get_benchmark_range(full_range=_BS_AXIS_FULL, ci_range=_BS_AXIS_CI)
seq_axis = get_benchmark_range(
full_range=_SEQ_LEN_AXIS_FULL, ci_range=_SEQ_LEN_AXIS_CI
)
return [
_BenchCase(bs=bs, seq_len=seq_len) for bs in bs_axis for seq_len in seq_axis
]
_X_NAMES = ["bs", "seq_len"]
_X_VALS = [(c.bs, c.seq_len) for c in _build_cases()]
def _build_inputs(*, bs: int, seq_len: int, device: torch.device) -> dict:
max_reqs = max(bs + 1, 4)
max_context_len = max(seq_len + 1, 1)
total_tokens = bs * seq_len
flat = torch.randint(
low=0,
high=1 << 30,
size=(total_tokens,),
dtype=torch.int64,
device=device,
)
lens = torch.full((bs,), seq_len, dtype=torch.int64, device=device)
offsets = torch.zeros(bs + 1, dtype=torch.int64, device=device)
offsets[1:] = torch.cumsum(lens, dim=0)
req_pool_indices = torch.arange(1, bs + 1, dtype=torch.int64, device=device)
pool = torch.zeros((max_reqs, max_context_len), dtype=torch.int32, device=device)
return dict(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=pool,
)
@triton.testing.perf_report(
triton.testing.Benchmark(
x_names=_X_NAMES,
x_vals=_X_VALS,
line_arg="provider",
line_vals=["triton"],
line_names=["Triton"],
styles=[("blue", "-")],
ylabel="time (us)",
plot_name="kv-canary-scatter-req-token-ids",
args={},
)
)
def benchmark(bs: int, seq_len: int, provider: str) -> tuple[float, float, float]:
inputs = _build_inputs(bs=bs, seq_len=seq_len, device=torch.device(DEFAULT_DEVICE))
return run_benchmark_no_cudagraph(
lambda: launch_scatter_req_token_ids_kernel(**inputs)
)
if __name__ == "__main__":
benchmark.run(print_data=True)
@@ -0,0 +1,193 @@
from __future__ import annotations
import torch
import triton
import triton.language as tl
_SCATTER_TOKEN_BLOCK: int = 256
# Upper bound on bs+1 the kernel can scan per program. Owner-req lookup uses an
# outer-product tile of shape ``[TOKEN_BLOCK, BATCH_BLOCK]``; keep this small so
# the tile stays in registers (256 x 512 = 128 KiB i1, well below the SM trap-
# inducing budget that bites at the 1M cell mark).
_SCATTER_BATCH_BLOCK: int = 512
def launch_scatter_req_token_ids_kernel(
*,
flat_in: torch.Tensor,
offsets: torch.Tensor,
req_pool_indices: torch.Tensor,
pool_out: torch.Tensor,
) -> None:
"""Scatter a flat per-req int64 object sequence into a 2-D int32 pool.
For each global object index ``t`` in ``[0, total_tokens)``:
- find ``r`` = largest req index s.t. ``offsets[r] <= t``
- ``pos = t - offsets[r]``
- ``rp = req_pool_indices[r]``
- if ``pos < pool_max_context_len``:
``pool_out[rp, pos] = flat_in[t].to(int32)``
Args:
flat_in: ``[total_tokens]`` int64 device tensor of objects, flattened
per-req in req order.
offsets: ``[bs + 1]`` int64 device tensor (host-computed cumsum of per-req
lengths). ``offsets[bs] == total_tokens``.
req_pool_indices: ``[bs]`` int64 device tensor of pool row indices.
pool_out: ``[max_reqs, max_context_len]`` int32 device tensor of objects.
Mutated in-place; rows not addressed by ``req_pool_indices`` are untouched.
Implementation notes:
- Linear scan over ``offsets`` (``BATCH_BLOCK >= bs + 1``); fits easily in
registers for the workloads kv-canary handles (``bs <= a few thousand``).
"""
if flat_in.dim() != 1:
raise ValueError(
f"kv-canary: scatter_req_token_ids flat_in must be 1-D, got shape "
f"{tuple(flat_in.shape)}"
)
if offsets.dim() != 1:
raise ValueError(
f"kv-canary: scatter_req_token_ids offsets must be 1-D, got shape "
f"{tuple(offsets.shape)}"
)
if req_pool_indices.dim() != 1:
raise ValueError(
f"kv-canary: scatter_req_token_ids req_pool_indices must be 1-D, got shape "
f"{tuple(req_pool_indices.shape)}"
)
if pool_out.dim() != 2:
raise ValueError(
f"kv-canary: scatter_req_token_ids pool_out must be 2-D, got shape "
f"{tuple(pool_out.shape)}"
)
if flat_in.dtype != torch.int64:
raise TypeError(
f"kv-canary: scatter_req_token_ids flat_in must be int64, got "
f"{flat_in.dtype}"
)
if offsets.dtype != torch.int64:
raise TypeError(
f"kv-canary: scatter_req_token_ids offsets must be int64, got "
f"{offsets.dtype}"
)
if req_pool_indices.dtype != torch.int64:
raise TypeError(
f"kv-canary: scatter_req_token_ids req_pool_indices must be int64, got "
f"{req_pool_indices.dtype}"
)
if pool_out.dtype != torch.int32:
raise TypeError(
f"kv-canary: scatter_req_token_ids pool_out must be int32, got "
f"{pool_out.dtype}"
)
bs = int(req_pool_indices.shape[0])
if int(offsets.shape[0]) != bs + 1:
raise ValueError(
f"kv-canary: scatter_req_token_ids offsets length {offsets.shape[0]} != "
f"bs+1 ({bs + 1})"
)
if bs + 1 > _SCATTER_BATCH_BLOCK:
raise ValueError(
f"kv-canary: scatter_req_token_ids bs+1={bs + 1} exceeds BATCH_BLOCK="
f"{_SCATTER_BATCH_BLOCK}; bump _SCATTER_BATCH_BLOCK if real workloads need this"
)
num_tokens = int(flat_in.shape[0])
if num_tokens == 0:
return
pool_stride0 = int(pool_out.stride(0))
pool_max_context_len = int(pool_out.shape[1])
grid = (triton.cdiv(num_tokens, _SCATTER_TOKEN_BLOCK),)
_scatter_req_token_ids_kernel[grid](
flat_in,
offsets,
req_pool_indices,
pool_out,
num_tokens=num_tokens,
num_batch=bs,
pool_stride0=pool_stride0,
pool_max_context_len=pool_max_context_len,
TOKEN_BLOCK=_SCATTER_TOKEN_BLOCK,
BATCH_BLOCK=_SCATTER_BATCH_BLOCK,
)
@triton.jit
def _scatter_req_token_ids_kernel(
flat_in_ptr, # [num_tokens] int64
offsets_ptr, # [num_batch + 1] int64
req_pool_indices_ptr, # [num_batch] int64
pool_out_ptr, # [num_rows, pool_max_context_len] int32, row stride = pool_stride0
num_tokens, # scalar int32
num_batch, # scalar int32
pool_stride0, # scalar int32 (row stride of pool_out in elements)
pool_max_context_len, # scalar int32 (dim-1 length of pool_out)
TOKEN_BLOCK: tl.constexpr,
BATCH_BLOCK: tl.constexpr,
):
pid = tl.program_id(0)
tids = pid * TOKEN_BLOCK + tl.arange(0, TOKEN_BLOCK) # [TOKEN_BLOCK] int32
tid_mask = tids < num_tokens # [TOKEN_BLOCK] bool
bs_offs = tl.arange(0, BATCH_BLOCK) # [BATCH_BLOCK] int32
bs_mask = bs_offs < (num_batch + 1) # [BATCH_BLOCK] bool
offs_vals = tl.load( # [BATCH_BLOCK] int64
offsets_ptr + bs_offs,
mask=bs_mask,
other=(1 << 62),
)
# find owning req for each tid via reduce-sum: req_idx = (count of offsets <= tid) - 1
le = offs_vals[None, :] <= tids[:, None] # [TOKEN_BLOCK, BATCH_BLOCK] bool
req_idx = tl.sum(le.to(tl.int32), axis=1) - 1 # [TOKEN_BLOCK] int32
safe_req_idx = tl.where(tid_mask, req_idx, 0) # [TOKEN_BLOCK] int32
starts = tl.load(
offsets_ptr + safe_req_idx, mask=tid_mask, other=0
) # [TOKEN_BLOCK] int64
pos = tids - starts # [TOKEN_BLOCK] int64
rp = tl.load(
req_pool_indices_ptr + safe_req_idx, mask=tid_mask, other=0
) # [TOKEN_BLOCK] int64
# Bound writes by the pool's max_context_len so a token sequence longer than the
# ReqToTokenPool row never spills into an adjacent row.
in_row = pos < pool_max_context_len # [TOKEN_BLOCK] bool
write_mask = tid_mask & in_row # [TOKEN_BLOCK] bool
val = tl.load(flat_in_ptr + tids, mask=tid_mask, other=0).to(
tl.int32
) # [TOKEN_BLOCK] int32
tl.store(pool_out_ptr + rp * pool_stride0 + pos, val, mask=write_mask)
def scatter_req_token_ids_torch_reference(
*,
flat_in: torch.Tensor,
offsets: torch.Tensor,
req_pool_indices: torch.Tensor,
pool_out: torch.Tensor,
) -> None:
"""Plain-PyTorch reference for :func:`launch_scatter_req_token_ids_kernel`."""
bs = int(req_pool_indices.shape[0])
offsets_host = offsets.detach().cpu().tolist()
req_pool_indices_host = req_pool_indices.detach().cpu().tolist()
flat_host = flat_in.detach().cpu()
pool_max_context_len = int(pool_out.shape[1])
for r in range(bs):
start = int(offsets_host[r])
end = int(offsets_host[r + 1])
if end <= start:
continue
rp = int(req_pool_indices_host[r])
seg = flat_host[start:end].to(torch.int32)
write_len = min(int(seg.shape[0]), pool_max_context_len)
if write_len <= 0:
continue
pool_out[rp, :write_len] = seg[:write_len].to(pool_out.device)
@@ -0,0 +1,278 @@
from __future__ import annotations
import random
import unittest
import torch
from sglang.jit_kernel.kv_canary.scatter_req_token_ids import (
_SCATTER_BATCH_BLOCK,
launch_scatter_req_token_ids_kernel,
scatter_req_token_ids_torch_reference,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=10, suite="base-b-kernel-unit-1-gpu-large")
_DEVICE = torch.device("cuda")
def _build_pool(*, max_reqs: int, max_context_len: int) -> torch.Tensor:
return torch.zeros((max_reqs, max_context_len), dtype=torch.int32, device=_DEVICE)
def _build_offsets(lens: list[int]) -> torch.Tensor:
cumsum = [0]
for n in lens:
cumsum.append(cumsum[-1] + n)
return torch.tensor(cumsum, dtype=torch.int64, device=_DEVICE)
def _build_flat(seqs: list[list[int]]) -> torch.Tensor:
flat: list[int] = []
for s in seqs:
flat.extend(s)
return torch.tensor(flat, dtype=torch.int64, device=_DEVICE)
class TestScatterReqTokenIds(CustomTestCase):
def test_scatter_byte_equal_basic(self) -> None:
"""Triton output matches the PyTorch reference for a small mixed batch."""
seqs = [[10, 20, 30], [40, 50], [60, 70, 80, 90]]
lens = [len(s) for s in seqs]
rp = [3, 1, 5]
flat = _build_flat(seqs)
offsets = _build_offsets(lens)
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
triton_pool = _build_pool(max_reqs=8, max_context_len=16)
ref_pool = _build_pool(max_reqs=8, max_context_len=16)
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=triton_pool,
)
scatter_req_token_ids_torch_reference(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=ref_pool,
)
torch.cuda.synchronize()
self.assertTrue(torch.equal(triton_pool, ref_pool))
# Spot-check: req in slot 3 holds [10,20,30,0,0,...] etc.
self.assertEqual(triton_pool[3, :3].tolist(), [10, 20, 30])
self.assertEqual(triton_pool[1, :2].tolist(), [40, 50])
self.assertEqual(triton_pool[5, :4].tolist(), [60, 70, 80, 90])
def test_scatter_empty_batch_no_op(self) -> None:
"""Empty input (num_tokens == 0) returns without touching the pool."""
flat = torch.empty(0, dtype=torch.int64, device=_DEVICE)
offsets = torch.zeros(1, dtype=torch.int64, device=_DEVICE)
req_pool_indices = torch.empty(0, dtype=torch.int64, device=_DEVICE)
pool = _build_pool(max_reqs=8, max_context_len=16)
pool_before = pool.clone()
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=pool,
)
torch.cuda.synchronize()
self.assertTrue(torch.equal(pool, pool_before))
def test_scatter_single_req(self) -> None:
"""One req with a full row of tokens writes byte-equal to the reference."""
seqs = [list(range(10))]
lens = [10]
rp = [2]
flat = _build_flat(seqs)
offsets = _build_offsets(lens)
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
triton_pool = _build_pool(max_reqs=4, max_context_len=16)
ref_pool = _build_pool(max_reqs=4, max_context_len=16)
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=triton_pool,
)
scatter_req_token_ids_torch_reference(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=ref_pool,
)
torch.cuda.synchronize()
self.assertTrue(torch.equal(triton_pool, ref_pool))
def test_scatter_truncates_at_max_context_len(self) -> None:
"""Tokens past the pool's max_context_len are silently dropped (no row spill)."""
# Two reqs; first req longer than max_context_len. Second req must remain
# uncorrupted.
seqs = [list(range(20)), [777, 888, 999]]
lens = [len(s) for s in seqs]
rp = [1, 2]
max_context_len = 8
flat = _build_flat(seqs)
offsets = _build_offsets(lens)
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
pool = _build_pool(max_reqs=4, max_context_len=max_context_len)
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=pool,
)
torch.cuda.synchronize()
self.assertEqual(
pool[1, :max_context_len].tolist(), list(range(max_context_len))
)
self.assertEqual(pool[2, :3].tolist(), [777, 888, 999])
def test_scatter_mixed_empty_and_nonempty_reqs(self) -> None:
"""Middle req has length 0 between two non-empty reqs: pool rows are byte-equal and untouched rows stay zero."""
seqs = [[1, 2], [], [3, 4, 5]]
lens = [len(s) for s in seqs]
rp = [2, 4, 6]
flat = _build_flat(seqs)
offsets = _build_offsets(lens)
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
triton_pool = _build_pool(max_reqs=8, max_context_len=8)
ref_pool = _build_pool(max_reqs=8, max_context_len=8)
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=triton_pool,
)
scatter_req_token_ids_torch_reference(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=ref_pool,
)
torch.cuda.synchronize()
self.assertTrue(torch.equal(triton_pool, ref_pool))
# Middle req contributes nothing; its pool row stays zero.
zero_row = torch.zeros(8, dtype=torch.int32, device=_DEVICE)
self.assertTrue(torch.equal(triton_pool[4], zero_row))
# First and third reqs are written to their respective rows.
self.assertEqual(triton_pool[2, :2].tolist(), [1, 2])
self.assertEqual(triton_pool[6, :3].tolist(), [3, 4, 5])
def test_scatter_random_byte_equal(self) -> None:
"""Randomized fuzz across bs, seq lengths, and req pool indices."""
rng = random.Random(0)
max_reqs = 64
max_context_len = 32
for _ in range(8):
bs = rng.randint(1, 16)
lens = [rng.randint(0, max_context_len) for _ in range(bs)]
# All distinct req pool indices in [1, max_reqs)
rp = rng.sample(range(1, max_reqs), k=bs)
seqs = [[rng.randint(0, 1 << 30) for _ in range(n)] for n in lens]
flat = _build_flat(seqs)
offsets = _build_offsets(lens)
req_pool_indices = torch.tensor(rp, dtype=torch.int64, device=_DEVICE)
triton_pool = _build_pool(
max_reqs=max_reqs, max_context_len=max_context_len
)
ref_pool = _build_pool(max_reqs=max_reqs, max_context_len=max_context_len)
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=triton_pool,
)
scatter_req_token_ids_torch_reference(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=ref_pool,
)
torch.cuda.synchronize()
self.assertTrue(torch.equal(triton_pool, ref_pool))
class TestScatterInputValidation(CustomTestCase):
"""Cover the strict input checks in launch_scatter_req_token_ids_kernel."""
def test_raises_on_2d_flat_in(self) -> None:
"""A 2-D flat_in tensor triggers a ValueError before any kernel launch."""
flat = torch.zeros((2, 2), dtype=torch.int64, device=_DEVICE)
offsets = torch.tensor([0, 1, 2], dtype=torch.int64, device=_DEVICE)
req_pool_indices = torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE)
pool = _build_pool(max_reqs=4, max_context_len=4)
with self.assertRaises(ValueError):
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=pool,
)
def test_raises_on_wrong_dtype_pool(self) -> None:
"""A pool_out with non-int32 dtype triggers a TypeError."""
flat = torch.tensor([10, 20], dtype=torch.int64, device=_DEVICE)
offsets = torch.tensor([0, 2], dtype=torch.int64, device=_DEVICE)
req_pool_indices = torch.tensor([1], dtype=torch.int64, device=_DEVICE)
pool = torch.zeros((4, 4), dtype=torch.int64, device=_DEVICE)
with self.assertRaises(TypeError):
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=pool,
)
def test_raises_on_offsets_len_mismatch(self) -> None:
"""offsets.shape[0] must equal bs + 1; mismatch triggers a ValueError."""
flat = torch.tensor([10, 20], dtype=torch.int64, device=_DEVICE)
# bs = 2 but offsets has length 2 instead of 3.
offsets = torch.tensor([0, 2], dtype=torch.int64, device=_DEVICE)
req_pool_indices = torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE)
pool = _build_pool(max_reqs=4, max_context_len=4)
with self.assertRaises(ValueError):
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=pool,
)
def test_raises_on_bs_plus_one_exceeds_batch_block(self) -> None:
"""bs+1 must fit in _SCATTER_BATCH_BLOCK; exceeding it triggers a ValueError."""
bs = _SCATTER_BATCH_BLOCK
flat = torch.empty(0, dtype=torch.int64, device=_DEVICE)
offsets = torch.zeros(bs + 1, dtype=torch.int64, device=_DEVICE)
req_pool_indices = torch.zeros(bs, dtype=torch.int64, device=_DEVICE)
pool = _build_pool(max_reqs=4, max_context_len=4)
with self.assertRaises(ValueError):
launch_scatter_req_token_ids_kernel(
flat_in=flat,
offsets=offsets,
req_pool_indices=req_pool_indices,
pool_out=pool,
)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -758,6 +758,7 @@ class Envs:
SGLANG_KV_CANARY_PERTURB_TARGET_GROUP = EnvStr(None)
SGLANG_KV_CANARY_PERTURB_NEXT_TOKEN_SWAP_PROB = EnvFloat(0.0)
SGLANG_KV_CANARY_ENABLE_TOKEN_ORACLE = EnvBool(False)
SGLANG_KV_CANARY_ENABLE_VERIFY_TOKEN_ASSERT = EnvBool(False)
SGLANG_KV_CANARY_ENABLE_MHA_V = EnvBool(False)
+6
View File
@@ -43,6 +43,10 @@ class CanaryConfig:
expected_input_positions[i]; mismatch records a violation. Only useful when something else
(e.g. token_oracle.oracle_manager.fill_expected_inputs) is feeding the expected_* placeholders
per forward — canary itself knows no oracle.
enable_verify_token_assert: bool. True = real-model token-id validator: build
expected_tokens from each req's ``origin_input_ids + output_ids`` (snapshotted at
ForwardBatch.init_new) and compare against the canary's stored tokens at verify time.
Independent of ``enable_write_input_assert``.
"""
mode: CanaryMode
@@ -50,6 +54,7 @@ class CanaryConfig:
sweep_interval: int
real_kv_hash_mode: RealKvHashMode
enable_write_input_assert: bool
enable_verify_token_assert: bool
@classmethod
def from_env(cls, server_args: "ServerArgs") -> "CanaryConfig":
@@ -67,4 +72,5 @@ class CanaryConfig:
sweep_interval=server_args.kv_canary_sweep_interval,
real_kv_hash_mode=RealKvHashMode[real_kv_raw],
enable_write_input_assert=envs.SGLANG_KV_CANARY_ENABLE_WRITE_INPUT_ASSERT.get(),
enable_verify_token_assert=envs.SGLANG_KV_CANARY_ENABLE_VERIFY_TOKEN_ASSERT.get(),
)
+7 -2
View File
@@ -29,8 +29,7 @@ class PlanInput:
``sot_pos`` when gathering the expected token; everything past the snapshot
(e.g. EAGLE draft / verify positions, or stale residue from a longer recycled
slot owner) returns the ``-1`` sentinel and the verify kernel skips the check.
The naive build never populates the verify-token-id cross-check, so this stays all
zeros and the plan kernel's gather degrades to the ``-1`` skip sentinel.
Set only when ``CanaryConfig.enable_verify_token_assert`` is on.
Allocated fresh per forward by :class:`SingleForwardManager`. The boundary
ForwardBatch token/position/slot tensors must already be int64
@@ -84,6 +83,12 @@ class PlanInput:
bs=bs,
)
req_all_ids_lens = forward_batch.req_all_ids_lens
if req_all_ids_lens is not None:
self.req_to_verify_expected_tokens_valid_lens[:bs].copy_(
req_all_ids_lens.to(torch.int64), non_blocking=True
)
def _extract_prefix_lens_and_extend_seq_lens(
*,
@@ -0,0 +1,84 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import torch
from sglang.jit_kernel.kv_canary.scatter_req_token_ids import (
launch_scatter_req_token_ids_kernel,
)
from sglang.srt.utils.common import flatten_arrays_to_int64_tensor
if TYPE_CHECKING:
from sglang.srt.managers.schedule_batch import Req
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
def compute_req_all_ids_info(
reqs: "list[Req]",
) -> tuple[torch.Tensor, torch.Tensor]:
"""Snapshot per-req (origin_input_ids + output_ids) as pinned CPU int64 tensors.
Returns:
``(req_all_ids_flat, req_all_ids_lens)`` — both pinned CPU int64. ``flat`` is the
flattened ``cat(r.origin_input_ids, r.output_ids) for r in reqs``; ``lens`` is
per-req ``len(origin_input_ids) + len(output_ids)``.
"""
parts = [arr for req in reqs for arr in (req.origin_input_ids, req.output_ids)]
req_all_ids_flat = flatten_arrays_to_int64_tensor(
parts, device=torch.device("cpu"), pin=True
)
req_all_ids_lens = torch.tensor(
[len(req.origin_input_ids) + len(req.output_ids) for req in reqs],
dtype=torch.int64,
pin_memory=True,
)
return req_all_ids_flat, req_all_ids_lens
def populate_req_to_expected_token_ids(
*,
forward_batch: "ForwardBatch",
req_to_verify_expected_tokens: Optional[torch.Tensor],
) -> None:
"""Scatter the forward batch's per-req token-id snapshot into the device-side pool."""
req_all_ids_flat_cpu = forward_batch.req_all_ids_flat
req_all_ids_lens_cpu = forward_batch.req_all_ids_lens
if req_all_ids_flat_cpu is None or req_all_ids_lens_cpu is None:
return
if req_to_verify_expected_tokens is None:
return
bs = int(forward_batch.req_pool_indices.shape[0])
if bs == 0:
return
if int(req_all_ids_lens_cpu.shape[0]) != bs:
raise RuntimeError(
f"kv-canary: req_all_ids_lens length {int(req_all_ids_lens_cpu.shape[0])} != "
f"batch_size {bs}; ForwardBatch snapshot diverged"
)
offsets_cpu = torch.zeros(bs + 1, dtype=torch.int64, pin_memory=True)
offsets_cpu[1:] = torch.cumsum(req_all_ids_lens_cpu, dim=0)
total_tokens = int(offsets_cpu[bs].item())
if total_tokens != int(req_all_ids_flat_cpu.shape[0]):
raise RuntimeError(
f"kv-canary: cumsum(req_all_ids_lens)={total_tokens} != "
f"req_all_ids_flat.numel()={int(req_all_ids_flat_cpu.shape[0])}; snapshot inconsistent"
)
if total_tokens == 0:
return
device = req_to_verify_expected_tokens.device
req_all_ids_flat_dev = req_all_ids_flat_cpu.to(device, non_blocking=True)
offsets_dev = offsets_cpu.to(device, non_blocking=True)
req_pool_indices_dev = forward_batch.req_pool_indices.to(
device=device, dtype=torch.int64
)
launch_scatter_req_token_ids_kernel(
flat_in=req_all_ids_flat_dev,
offsets=offsets_dev,
req_pool_indices=req_pool_indices_dev,
pool_out=req_to_verify_expected_tokens,
)
@@ -13,6 +13,9 @@ from sglang.srt.kv_canary.config import CanaryConfig
from sglang.srt.kv_canary.endpoint import CanaryEndpoint
from sglang.srt.kv_canary.expected_inputs import ExpectedInputs
from sglang.srt.kv_canary.plan_input import PlanInput
from sglang.srt.kv_canary.req_to_expected_token_ids_manager import (
populate_req_to_expected_token_ids,
)
from sglang.srt.kv_canary.runner.enable_warner import CanaryEnableWarner
from sglang.srt.kv_canary.runner.kernel_launcher import (
invoke_plan,
@@ -130,6 +133,12 @@ class SingleForwardManager:
f"CanaryLaunchCapacities.from_args"
)
if self._config.enable_verify_token_assert:
populate_req_to_expected_token_ids(
forward_batch=maybe_inaccurate_forward_batch,
req_to_verify_expected_tokens=self._device_state.req_to_verify_expected_tokens,
)
def pre_ops_maybe_inside_graph(
self, forward_batch: "ForwardBatch"
) -> "_PreOpsMaybeInsideGraphOutput":
@@ -203,7 +212,7 @@ class SingleForwardManager:
violation_log=violation_log,
real_kv_hash_mode=self._config.real_kv_hash_mode,
enable_write_input_assert=enable_write_input_assert,
enable_verify_token_assert=False,
enable_verify_token_assert=self._config.enable_verify_token_assert,
)
return _PreOpsMaybeInsideGraphOutput(
@@ -241,7 +250,7 @@ class SingleForwardManager:
violation_log=violation_log,
real_kv_hash_mode=self._config.real_kv_hash_mode,
enable_write_input_assert=enable_write_input_assert,
enable_verify_token_assert=False,
enable_verify_token_assert=self._config.enable_verify_token_assert,
)
verify_plan_enable_combined = _torch_reduce_minimum(
+18 -9
View File
@@ -80,11 +80,11 @@ class CanaryDeviceState:
req_to_verify_expected_tokens: Optional int32 device tensor shape
``[req_to_token_alloc_size, max_context_len]``. Mirrors ReqToTokenPool layout;
``pool[req_idx, p]`` = source-of-truth token at logical position ``p`` for the
req in slot ``req_idx``. The plan-side entries kernel gathers from this pool (via
``kv_token_id_vs_position_offset`` per buffer group) into
``VerifyPlan.verify_expected_tokens``; the verify kernel then compares against each
canary slot's stored token. The naive build never populates the verify-token-id
cross-check, so this is always ``None`` and the gather degrades to the ``-1`` sentinel.
req in slot ``req_idx``. Allocated only when
``CanaryConfig.enable_verify_token_assert`` is True. The plan-side entries
kernel gathers from this pool (via ``kv_token_id_vs_position_offset`` per buffer
group) into ``VerifyPlan.verify_expected_tokens``; the verify kernel then
compares against each canary slot's stored token.
"""
violation_log: ViolationLog
@@ -113,10 +113,19 @@ class CanaryDeviceState:
kernel_run_counters = torch.zeros(num_tags, dtype=torch.int64, device=device)
slot_run_counters = torch.zeros(num_tags, dtype=torch.int64, device=device)
enable_chain_position_assert = torch.ones(1, dtype=torch.int32, device=device)
# The naive build does not run the verify-token-id cross-check, so the source-of-truth
# token pool is never allocated. The field is kept on the dataclass and downstream code
# (plan kernel gather) treats ``None`` as "emit the -1 skip sentinel".
req_to_verify_expected_tokens = None
if config.enable_verify_token_assert:
if req_to_token_alloc_size is None or max_context_len is None:
raise ValueError(
"kv-canary: CanaryDeviceState.allocate requires req_to_token_alloc_size "
"and max_context_len when CanaryConfig.enable_verify_token_assert is on"
)
req_to_verify_expected_tokens = torch.empty(
(req_to_token_alloc_size, max_context_len),
dtype=torch.int32,
device=device,
)
else:
req_to_verify_expected_tokens = None
return cls(
violation_log=violation_log,
kernel_run_counters=kernel_run_counters,
@@ -42,6 +42,9 @@ from sglang.srt.distributed.parallel_state import (
get_tensor_model_parallel_world_size,
)
from sglang.srt.environ import envs
from sglang.srt.kv_canary.req_to_expected_token_ids_manager import (
compute_req_all_ids_info,
)
from sglang.srt.layers.dp_attention import (
DpPaddingMode,
get_attention_cp_size,
@@ -587,6 +590,11 @@ class ForwardBatch(ForwardBatchDeepSeekMHAMixin):
ret.rids_int = hashed
ret.bootstrap_room_ids_int = bootstrap_room_ids
if envs.SGLANG_KV_CANARY_ENABLE_VERIFY_TOKEN_ASSERT.get():
ret.req_all_ids_flat, ret.req_all_ids_lens = compute_req_all_ids_info(
batch.reqs
)
if batch.extend_input_logprob_token_ids is not None:
ret.extend_input_logprob_token_ids_gpu = (
batch.extend_input_logprob_token_ids.to(device, non_blocking=True)
+1
View File
@@ -78,6 +78,7 @@ class CanaryE2EBase(CapturedServerE2EBase):
def setUpClass(cls) -> None:
cls._cfg = _MODE_CONFIGS[cls.model_mode]
server_env = os.environ.copy()
server_env.setdefault("SGLANG_KV_CANARY_ENABLE_VERIFY_TOKEN_ASSERT", "1")
server_env.update(cls.extra_env)
if cls.model_mode == "swa":
# SWA mode uses google/gemma-4-E2B-it, whose forward does a
+1
View File
@@ -139,6 +139,7 @@ def make_base_config() -> CanaryConfig:
sweep_interval=0,
real_kv_hash_mode=consts.RealKvHashMode.NONE,
enable_write_input_assert=False,
enable_verify_token_assert=True,
)
@@ -29,6 +29,7 @@ def make_config(
sweep_interval: int = 0,
real_kv_hash_mode: RealKvHashMode = RealKvHashMode.NONE,
enable_write_input_assert: bool = False,
enable_verify_token_assert: bool = True,
) -> CanaryConfig:
return CanaryConfig(
mode=mode,
@@ -36,6 +37,7 @@ def make_config(
sweep_interval=sweep_interval,
real_kv_hash_mode=real_kv_hash_mode,
enable_write_input_assert=enable_write_input_assert,
enable_verify_token_assert=enable_verify_token_assert,
)