Add the KV-canary verify JIT kernel and reference implementation (#26805)

This commit is contained in:
fzyzcjy
2026-05-31 09:52:52 +08:00
committed by GitHub
parent 8a20c58252
commit 16950954c6
13 changed files with 1527 additions and 0 deletions
@@ -0,0 +1,286 @@
from __future__ import annotations
from typing import Tuple
import torch
import triton
import triton.testing
from sglang.jit_kernel.benchmark.kv_canary.utils import (
RING_CAPACITY,
SWA_WINDOW,
BenchCase,
build_fast_matrix_cases,
build_full_matrix_cases,
cases_to_x_vals,
naive_slot_copy_fn,
)
from sglang.jit_kernel.benchmark.utils import (
DEFAULT_DEVICE,
get_benchmark_range,
run_benchmark,
)
from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.kv_canary.verify import (
CANARY_SLOT_BYTES,
CanaryLaunchTag,
VerifyOrWriteContext,
VerifyPlan,
launch_canary_verify_kernel,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=900, suite="nightly-kernel-1-gpu", nightly=True)
_X_NAMES = [
"scenario",
"bs",
"prefix_len",
"mode",
"extend_len",
"pool_kind",
]
_X_VALS = cases_to_x_vals(
get_benchmark_range(
full_range=build_full_matrix_cases(),
ci_range=build_fast_matrix_cases(),
)
)
_KERNEL_KIND_X_NAMES = ["kernel_kind_name"]
_KERNEL_KIND_X_VALS = [(tag.name,) for tag in CanaryLaunchTag]
def _verify_entry_count(case: BenchCase) -> int:
if case.pool_kind == "swa_window_128":
per_req = min(case.prefix_len, SWA_WINDOW)
else:
per_req = case.prefix_len
return case.bs * per_req
def _verify_num_slots(case: BenchCase) -> int:
if case.pool_kind == "swa_window_128":
per_req_slots = SWA_WINDOW
else:
per_req_slots = max(1, case.prefix_len)
return max(2, case.bs * per_req_slots + 1)
def _build_verify_inputs(case: BenchCase, *, device: torch.device) -> Tuple[
torch.Tensor,
VerifyPlan,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
]:
total_entries = _verify_entry_count(case)
capacity = max(1, total_entries)
num_slots = _verify_num_slots(case)
canary_buf = torch.zeros(
num_slots, CANARY_SLOT_BYTES, dtype=torch.uint8, device=device
)
slot_indices = torch.empty(capacity, dtype=torch.int64, device=device)
positions = torch.empty(capacity, dtype=torch.int64, device=device)
prev_slots = torch.empty(capacity, dtype=torch.int64, device=device)
if total_entries > 0:
flat_idx = torch.arange(total_entries, device=device, dtype=torch.int64)
per_req = total_entries // case.bs if case.bs > 0 else 0
slot_indices[:total_entries] = (flat_idx % max(num_slots - 1, 1)).to(
torch.int64
)
positions[:total_entries] = (flat_idx % max(per_req, 1)).to(torch.int64)
is_head = (flat_idx % max(per_req, 1)) == 0
prev_seq = (flat_idx - 1) % max(num_slots - 1, 1)
prev_slots[:total_entries] = torch.where(
is_head, torch.full_like(flat_idx, -1), prev_seq
).to(torch.int64)
if capacity > total_entries:
slot_indices[total_entries:] = 0
positions[total_entries:] = 0
prev_slots[total_entries:] = -1
num_valid = torch.tensor([total_entries], dtype=torch.int32, device=device)
enable = torch.ones(1, dtype=torch.int32, device=device)
expected_input_ids = torch.full((capacity,), -1, dtype=torch.int64, device=device)
plan = VerifyPlan(
verify_slot_indices=slot_indices,
verify_expected_tokens=expected_input_ids,
verify_expected_positions=positions,
verify_prev_slot_indices=prev_slots,
verify_num_valid=num_valid,
enable=enable,
)
violation_ring = torch.zeros(
RING_CAPACITY, consts.VIOLATION_FIELDS, dtype=torch.int64, device=device
)
violation_write_index = torch.zeros(1, dtype=torch.int32, device=device)
slot_run_counter = torch.zeros(1, dtype=torch.int64, device=device)
kernel_run_counter = torch.zeros(1, dtype=torch.int64, device=device)
enable_chain_position_assert = torch.ones(1, dtype=torch.int32, device=device)
return (
canary_buf,
plan,
violation_ring,
violation_write_index,
slot_run_counter,
kernel_run_counter,
enable_chain_position_assert,
)
def _build_context(
*,
canary_buf: torch.Tensor,
violation_ring: torch.Tensor,
violation_write_index: torch.Tensor,
slot_run_counter: torch.Tensor,
kernel_run_counter: torch.Tensor,
enable_chain_position_assert: torch.Tensor,
kernel_kind: CanaryLaunchTag,
) -> VerifyOrWriteContext:
return VerifyOrWriteContext(
canary_buf=canary_buf,
kernel_kind=kernel_kind,
violation_ring=violation_ring,
violation_write_index=violation_write_index,
slot_run_counter=slot_run_counter,
kernel_run_counter=kernel_run_counter,
enable_chain_position_assert=enable_chain_position_assert,
)
@triton.testing.perf_report(
triton.testing.Benchmark(
x_names=_X_NAMES,
x_vals=_X_VALS,
line_arg="provider",
line_vals=["canary", "naive"],
line_names=["canary_verify_step", "naive index_copy_"],
styles=[("blue", "-"), ("red", "--")],
ylabel="us",
plot_name="kv-canary-verify-perf",
args={},
)
)
def benchmark(
scenario: str,
bs: int,
prefix_len: int,
mode: str,
extend_len: int,
pool_kind: str,
provider: str,
) -> Tuple[float, float, float]:
case = BenchCase(
scenario=scenario,
bs=bs,
prefix_len=prefix_len,
mode=mode,
extend_len=extend_len,
pool_kind=pool_kind,
)
device = torch.device(DEFAULT_DEVICE)
if provider == "canary":
(
canary_buf,
plan,
violation_ring,
violation_write_index,
slot_run_counter,
kernel_run_counter,
enable_chain_position_assert,
) = _build_verify_inputs(case, device=device)
context = _build_context(
canary_buf=canary_buf,
violation_ring=violation_ring,
violation_write_index=violation_write_index,
slot_run_counter=slot_run_counter,
kernel_run_counter=kernel_run_counter,
enable_chain_position_assert=enable_chain_position_assert,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
)
def fn() -> None:
violation_write_index.zero_()
launch_canary_verify_kernel(
context=context,
plan=plan,
check_verify_expected_token=True,
)
else:
fn = naive_slot_copy_fn(total=_verify_entry_count(case), device=device)
return run_benchmark(fn)
@triton.testing.perf_report(
triton.testing.Benchmark(
x_names=_KERNEL_KIND_X_NAMES,
x_vals=_KERNEL_KIND_X_VALS,
line_arg="provider",
line_vals=["canary"],
line_names=["canary_verify_step"],
styles=[("blue", "-")],
ylabel="us",
plot_name="kv-canary-verify-kernel-kind-perf",
args={},
)
)
def benchmark_kernel_kind(
kernel_kind_name: str,
provider: str,
) -> Tuple[float, float, float]:
case = BenchCase(
scenario="kernel_kind",
bs=32,
prefix_len=4096,
mode="extend",
extend_len=128,
pool_kind="full",
)
device = torch.device(DEFAULT_DEVICE)
(
canary_buf,
plan,
violation_ring,
violation_write_index,
slot_run_counter,
kernel_run_counter,
enable_chain_position_assert,
) = _build_verify_inputs(case, device=device)
kernel_kind = CanaryLaunchTag[kernel_kind_name]
context = _build_context(
canary_buf=canary_buf,
violation_ring=violation_ring,
violation_write_index=violation_write_index,
slot_run_counter=slot_run_counter,
kernel_run_counter=kernel_run_counter,
enable_chain_position_assert=enable_chain_position_assert,
kernel_kind=kernel_kind,
)
def fn() -> None:
violation_write_index.zero_()
launch_canary_verify_kernel(
context=context,
plan=plan,
check_verify_expected_token=True,
)
return run_benchmark(fn)
if __name__ == "__main__":
benchmark.run(print_data=True)
benchmark_kernel_kind.run(print_data=True)
@@ -0,0 +1,269 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
import torch
from sglang.jit_kernel.kv_canary.verify import CANARY_SLOT_BYTES
BS_AXIS: list[int] = [1, 4, 32, 128, 256, 1024]
PREFIX_AXIS: list[int] = [0, 128, 1024, 4096, 10240, 16384]
EXTEND_LEN_AXIS: list[int] = [128, 512, 4096, 16384]
POOL_AXIS: list[str] = ["full", "swa_window_128"]
SWA_WINDOW: int = 128
RING_CAPACITY: int = 256
MAX_EXTEND_TOKENS_PER_FORWARD: int = 4096
@dataclass(frozen=True, slots=True, kw_only=True)
class BenchCase:
scenario: str
bs: int
prefix_len: int
mode: str
extend_len: int
pool_kind: str
@property
def case_id(self) -> str:
return (
f"{self.scenario}_bs{self.bs}_prefix{self.prefix_len}_{self.mode}{self.extend_len}"
f"_{self.pool_kind}"
)
def _case(
*,
scenario: str,
bs: int,
prefix_len: int,
mode: str,
extend_len: int,
pool_kind: str,
) -> BenchCase:
return BenchCase(
scenario=scenario,
bs=bs,
prefix_len=prefix_len,
mode=mode,
extend_len=extend_len,
pool_kind=pool_kind,
)
def _is_realistic_extend_case(case: BenchCase) -> bool:
if case.mode != "extend":
return True
return case.bs * case.extend_len <= MAX_EXTEND_TOKENS_PER_FORWARD
def _dedupe_cases(cases: list[BenchCase]) -> list[BenchCase]:
seen: set[str] = set()
result: list[BenchCase] = []
for case in cases:
if case.case_id in seen:
continue
seen.add(case.case_id)
result.append(case)
return result
def build_fast_matrix_cases() -> list[BenchCase]:
return _dedupe_cases(
[
_case(
scenario="smoke_decode_empty",
bs=1,
prefix_len=0,
mode="decode",
extend_len=1,
pool_kind="full",
),
_case(
scenario="small_extend_batch",
bs=32,
prefix_len=4096,
mode="extend",
extend_len=128,
pool_kind="full",
),
_case(
scenario="e2e_decode_steady",
bs=256,
prefix_len=4096,
mode="decode",
extend_len=1,
pool_kind="full",
),
_case(
scenario="decode_large_batch_short_prefix",
bs=1024,
prefix_len=1024,
mode="decode",
extend_len=1,
pool_kind="full",
),
_case(
scenario="e2e_prefill_chunk_first",
bs=1,
prefix_len=0,
mode="extend",
extend_len=4096,
pool_kind="full",
),
_case(
scenario="e2e_prefill_chunk_mid",
bs=1,
prefix_len=8192,
mode="extend",
extend_len=4096,
pool_kind="full",
),
_case(
scenario="e2e_prefill_chunk_last",
bs=1,
prefix_len=12288,
mode="extend",
extend_len=4096,
pool_kind="full",
),
_case(
scenario="e2e_decode_tail",
bs=1,
prefix_len=5120,
mode="decode",
extend_len=1,
pool_kind="full",
),
_case(
scenario="swa_decode_long_prefix",
bs=128,
prefix_len=10240,
mode="decode",
extend_len=1,
pool_kind="swa_window_128",
),
_case(
scenario="small_extend_single_req",
bs=1,
prefix_len=128,
mode="extend",
extend_len=128,
pool_kind="full",
),
_case(
scenario="medium_extend_chunk",
bs=4,
prefix_len=1024,
mode="extend",
extend_len=512,
pool_kind="full",
),
_case(
scenario="decode_mid_batch",
bs=128,
prefix_len=4096,
mode="decode",
extend_len=1,
pool_kind="full",
),
_case(
scenario="e2e_prefill_chunk_second",
bs=1,
prefix_len=4096,
mode="extend",
extend_len=4096,
pool_kind="full",
),
_case(
scenario="swa_decode_short_prefix",
bs=256,
prefix_len=128,
mode="decode",
extend_len=1,
pool_kind="swa_window_128",
),
_case(
scenario="swa_decode_tail",
bs=4,
prefix_len=10240,
mode="decode",
extend_len=1,
pool_kind="swa_window_128",
),
]
)
def build_full_matrix_cases() -> list[BenchCase]:
"""Full matrix plus targeted e2e points.
Extend cases are pruned to a maximum token chunk per forward because the scheduler chunks long
prefills; for example, a 4096-token extend is represented as ``bs=1``, not ``bs=32``.
"""
fast = build_fast_matrix_cases()
fast_keys = {c.case_id for c in fast}
full: list[BenchCase] = list(fast)
for bs in BS_AXIS:
for prefix_len in PREFIX_AXIS:
for pool_kind in POOL_AXIS:
for mode, extend_len in (
("decode", 1),
*(("extend", e) for e in EXTEND_LEN_AXIS),
):
case = _case(
scenario="matrix",
bs=bs,
prefix_len=prefix_len,
mode=mode,
extend_len=extend_len,
pool_kind=pool_kind,
)
if not _is_realistic_extend_case(case):
continue
if case.case_id in fast_keys:
continue
full.append(case)
return full
def cases_to_x_vals(
cases: list[BenchCase],
) -> list[tuple[str, int, int, str, int, str]]:
return [
(
c.scenario,
c.bs,
c.prefix_len,
c.mode,
c.extend_len,
c.pool_kind,
)
for c in cases
]
def naive_slot_copy_fn(*, total: int, device: torch.device) -> Callable[[], None]:
n_slots = max(total, 1)
payload = torch.zeros(n_slots, CANARY_SLOT_BYTES, dtype=torch.uint8, device=device)
sink = torch.zeros_like(payload)
indices = torch.arange(n_slots, device=device, dtype=torch.int64) % sink.shape[0]
def baseline() -> None:
sink.index_copy_(0, indices, payload)
return baseline
def naive_cumsum_fn(*, bs: int, device: torch.device) -> Callable[[], None]:
counts = torch.zeros(max(bs, 1), dtype=torch.int32, device=device)
def baseline() -> None:
torch.cumsum(counts, dim=0)
return baseline
@@ -0,0 +1,78 @@
#pragma once
#include <sgl_kernel/utils.cuh> // For SGL_DEVICE
#include "consts.cuh"
#include <cstdint>
namespace canary {
SGL_DEVICE uint64_t splitmix64(uint64_t x) {
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
return x ^ (x >> 31);
}
SGL_DEVICE uint64_t splitmix64_mix3(uint64_t a, uint64_t b, uint64_t c) {
uint64_t h = splitmix64(a);
h = splitmix64(h ^ b);
h = splitmix64(h ^ c);
return h;
}
struct ViolationSink {
int64_t* __restrict__ ring;
int32_t* __restrict__ write_index;
int32_t ring_capacity;
int32_t kernel_kind;
};
struct ViolationRow {
int64_t slot_idx;
int64_t position;
int64_t stored_token;
int64_t expected_token;
int64_t stored_chain_hash;
int64_t expected_aux;
int64_t fail_reason_bits;
};
SGL_DEVICE void record_violation(const ViolationSink& sink, const ViolationRow& row) {
const int32_t seq = atomicAdd(sink.write_index, 1);
if (seq < sink.ring_capacity) {
int64_t* dst = sink.ring + static_cast<int64_t>(seq) * kViolationFields;
dst[kViolationFieldKernelKind] = static_cast<int64_t>(sink.kernel_kind);
dst[kViolationFieldSlotIdx] = row.slot_idx;
dst[kViolationFieldPosition] = row.position;
dst[kViolationFieldStoredToken] = row.stored_token;
dst[kViolationFieldExpectedToken] = row.expected_token;
dst[kViolationFieldStoredChainHash] = row.stored_chain_hash;
dst[kViolationFieldExpectedAux] = row.expected_aux;
dst[kViolationFieldFailReasonBits] = row.fail_reason_bits;
__threadfence_system();
}
}
SGL_DEVICE int64_t canary_load_field(const uint8_t* buf, int64_t slot_idx, int64_t slot_stride_bytes, int field) {
const int64_t* p = reinterpret_cast<const int64_t*>(buf + slot_idx * slot_stride_bytes);
return p[field];
}
SGL_DEVICE void
canary_store_field(uint8_t* buf, int64_t slot_idx, int64_t slot_stride_bytes, int field, int64_t value) {
int64_t* p = reinterpret_cast<int64_t*>(buf + slot_idx * slot_stride_bytes);
p[field] = value;
}
SGL_DEVICE uint64_t compute_slot_hash(const uint8_t* canary_buf, int64_t slot_stride_bytes, int64_t source_slot_idx) {
if (source_slot_idx < 0) {
return splitmix64(kCanaryChainAnchor);
}
const int64_t token = canary_load_field(canary_buf, source_slot_idx, slot_stride_bytes, kCanaryFieldToken);
const int64_t position = canary_load_field(canary_buf, source_slot_idx, slot_stride_bytes, kCanaryFieldPosition);
const int64_t prev_hash = canary_load_field(canary_buf, source_slot_idx, slot_stride_bytes, kCanaryFieldPrevHash);
return splitmix64_mix3(
static_cast<uint64_t>(prev_hash), static_cast<uint64_t>(token), static_cast<uint64_t>(position));
}
} // namespace canary
@@ -0,0 +1,208 @@
#pragma once
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // For div_ceil, RuntimeCheck
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include "canary_common.cuh"
#include <cstdint>
namespace canary {
namespace {
constexpr uint32_t kVerifyBlockSize = 512;
constexpr uint32_t kPersistentBlocks = 64;
struct VerifyKernelParams {
// Canary buffer this launch verifies. Read-only.
const uint8_t* canary_buf;
int64_t slot_stride_bytes;
// Plan tensors.
const int64_t* verify_slot_indices;
const int64_t* verify_expected_tokens;
const int64_t* verify_expected_positions;
const int64_t* verify_prev_slot_indices;
const int32_t* verify_num_valid;
const int32_t* verify_enable;
int32_t verify_capacity;
// Violation sink (ring + write_index + capacity + kernel_kind bundled in canary_common.cuh).
ViolationSink violation_sink;
// Health counters.
int64_t* slot_run_counter;
int64_t* kernel_run_counter;
};
template <bool CHECK_VERIFY_EXPECTED_TOKEN>
__global__ void canary_verify_kernel(const VerifyKernelParams __grid_constant__ p) {
const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t stride = gridDim.x * blockDim.x;
if (tid == 0) {
atomicAdd(reinterpret_cast<unsigned long long*>(p.kernel_run_counter), 1ULL);
}
if (*p.verify_enable == 0) {
return;
}
const int32_t active = min(*p.verify_num_valid, p.verify_capacity);
uint32_t local_active_count = 0;
for (uint32_t entry_idx = tid; entry_idx < static_cast<uint32_t>(active); entry_idx += stride) {
++local_active_count;
const int64_t slot_idx = p.verify_slot_indices[entry_idx];
const int64_t expected_position = p.verify_expected_positions[entry_idx];
const int64_t prev_slot_idx = p.verify_prev_slot_indices[entry_idx];
int64_t expected_input_id = -1;
if constexpr (CHECK_VERIFY_EXPECTED_TOKEN) {
expected_input_id = p.verify_expected_tokens[entry_idx];
}
if (slot_idx == kTokenToKvSlotPadding) {
continue;
}
const int64_t stored_token = canary_load_field(p.canary_buf, slot_idx, p.slot_stride_bytes, kCanaryFieldToken);
const int64_t stored_position =
canary_load_field(p.canary_buf, slot_idx, p.slot_stride_bytes, kCanaryFieldPosition);
const int64_t stored_chain_hash =
canary_load_field(p.canary_buf, slot_idx, p.slot_stride_bytes, kCanaryFieldPrevHash);
// field[3] (kCanaryFieldRealKvHash) is always 0 in the naive canary; not loaded or checked.
const bool prev_reachable = (prev_slot_idx != kTokenToKvSlotPadding);
const int64_t expected_chain_hash =
prev_reachable ? static_cast<int64_t>(compute_slot_hash(p.canary_buf, p.slot_stride_bytes, prev_slot_idx))
: stored_chain_hash;
FailReason fail_reason_bits{};
if (prev_reachable && stored_chain_hash != expected_chain_hash) {
fail_reason_bits |= FailReason::kVerifyChainHashMismatch;
}
if constexpr (CHECK_VERIFY_EXPECTED_TOKEN) {
if (expected_input_id != -1 && stored_token != expected_input_id) {
fail_reason_bits |= FailReason::kVerifyTokenMismatch;
}
}
if (stored_position != expected_position) {
fail_reason_bits |= FailReason::kVerifyPositionMismatch;
}
if (fail_reason_bits != FailReason{}) {
record_violation(
p.violation_sink,
ViolationRow{
/* slot_idx = */ slot_idx,
/* position = */ stored_position,
/* stored_token = */ stored_token,
/* expected_token = */ expected_input_id,
/* stored_chain_hash = */ stored_chain_hash,
/* expected_aux = */ expected_chain_hash,
/* fail_reason_bits = */ static_cast<int64_t>(fail_reason_bits),
});
}
}
uint32_t warp_active_count = local_active_count;
for (int offset = 16; offset > 0; offset >>= 1) {
warp_active_count += __shfl_down_sync(0xFFFFFFFFu, warp_active_count, offset);
}
if ((threadIdx.x & 31u) == 0u && warp_active_count != 0u) {
atomicAdd(
reinterpret_cast<unsigned long long*>(p.slot_run_counter), static_cast<unsigned long long>(warp_active_count));
}
}
} // namespace
// API source of truth: docstring of canary_verify_step in python/sglang/jit_kernel/kv_canary/verify.py.
template <bool CHECK_VERIFY_EXPECTED_TOKEN>
struct CanaryVerifyKernel {
static void
run(tvm::ffi::TensorView canary_buf,
tvm::ffi::TensorView verify_slot_indices,
tvm::ffi::TensorView verify_expected_tokens,
tvm::ffi::TensorView verify_expected_positions,
tvm::ffi::TensorView verify_prev_slot_indices,
tvm::ffi::TensorView verify_num_valid,
tvm::ffi::TensorView verify_enable,
int64_t kernel_kind,
tvm::ffi::TensorView violation_ring,
tvm::ffi::TensorView violation_write_index,
tvm::ffi::TensorView slot_run_counter,
tvm::ffi::TensorView kernel_run_counter) {
using namespace host;
SymbolicSize N_slots = {"num_canary_slots"};
SymbolicSize N_stride = {"slot_stride_bytes"};
SymbolicSize N_verify = {"verify_capacity"};
SymbolicDevice device_;
device_.set_options<kDLCUDA>();
TensorMatcher({N_slots, N_stride}).with_dtype<uint8_t>().with_device<kDLCUDA>(device_).verify(canary_buf);
TensorMatcher({N_verify})
.with_dtype<int64_t>()
.with_device<kDLCUDA>(device_)
.verify(verify_slot_indices)
.verify(verify_expected_tokens)
.verify(verify_expected_positions)
.verify(verify_prev_slot_indices);
TensorMatcher({1}).with_dtype<int32_t>().with_device<kDLCUDA>(device_).verify(verify_num_valid);
TensorMatcher({1}).with_dtype<int32_t>().with_device<kDLCUDA>(device_).verify(verify_enable);
TensorMatcher({1}).with_dtype<int32_t>().with_device<kDLCUDA>(device_).verify(violation_write_index);
SymbolicSize N_ring = {"ring_capacity"};
TensorMatcher({N_ring, static_cast<int64_t>(kViolationFields)})
.with_dtype<int64_t>()
.with_device<kDLCUDA>(device_)
.verify(violation_ring);
TensorMatcher({1})
.with_dtype<int64_t>()
.with_device<kDLCUDA>(device_)
.verify(slot_run_counter)
.verify(kernel_run_counter);
const int64_t slot_stride_bytes = N_stride.unwrap();
const int32_t verify_capacity = static_cast<int32_t>(N_verify.unwrap());
const int32_t ring_capacity = static_cast<int32_t>(N_ring.unwrap());
const DLDevice device = device_.unwrap();
RuntimeCheck(
slot_stride_bytes >= static_cast<int64_t>(kCanaryFieldsPerSlot * sizeof(int64_t)),
"canary_verify: slot_stride_bytes must hold at least ",
static_cast<int64_t>(kCanaryFieldsPerSlot * sizeof(int64_t)),
" bytes per slot, got ",
slot_stride_bytes);
VerifyKernelParams p{};
p.canary_buf = static_cast<const uint8_t*>(canary_buf.data_ptr());
p.slot_stride_bytes = slot_stride_bytes;
p.verify_slot_indices = static_cast<const int64_t*>(verify_slot_indices.data_ptr());
p.verify_expected_tokens = static_cast<const int64_t*>(verify_expected_tokens.data_ptr());
p.verify_expected_positions = static_cast<const int64_t*>(verify_expected_positions.data_ptr());
p.verify_prev_slot_indices = static_cast<const int64_t*>(verify_prev_slot_indices.data_ptr());
p.verify_num_valid = static_cast<const int32_t*>(verify_num_valid.data_ptr());
p.verify_enable = static_cast<const int32_t*>(verify_enable.data_ptr());
p.verify_capacity = verify_capacity;
p.violation_sink.ring = static_cast<int64_t*>(violation_ring.data_ptr());
p.violation_sink.write_index = static_cast<int32_t*>(violation_write_index.data_ptr());
p.violation_sink.ring_capacity = ring_capacity;
p.violation_sink.kernel_kind = static_cast<int32_t>(kernel_kind);
p.slot_run_counter = static_cast<int64_t*>(slot_run_counter.data_ptr());
p.kernel_run_counter = static_cast<int64_t*>(kernel_run_counter.data_ptr());
const uint32_t grid = kPersistentBlocks;
LaunchKernel(grid, kVerifyBlockSize, device)(canary_verify_kernel<CHECK_VERIFY_EXPECTED_TOKEN>, p);
}
};
} // namespace canary
@@ -0,0 +1,49 @@
#pragma once
#include <cstdint>
namespace canary {
constexpr uint64_t kCanaryChainAnchor = 0xC0FFEE1234567890ULL;
// Mirrors SGLang's TokenToKVPoolAllocator contract: token-to-KV slot 0 is reserved for padded-token dummy
// writes. Since req_to_token stores token-to-KV slot ids and is zero-initialized, canary slot 0 is skipped
// instead of treating unfilled entries as real KV slots.
constexpr int64_t kTokenToKvSlotPadding = 0;
constexpr int64_t kReqPoolIdxPadding = 0;
constexpr int kCanaryFieldsPerSlot = 4;
constexpr int kCanaryFieldToken = 0;
constexpr int kCanaryFieldPosition = 1;
constexpr int kCanaryFieldPrevHash = 2;
constexpr int kCanaryFieldRealKvHash = 3;
constexpr int kViolationFields = 8;
constexpr int kViolationFieldKernelKind = 0;
constexpr int kViolationFieldSlotIdx = 1;
constexpr int kViolationFieldPosition = 2;
constexpr int kViolationFieldStoredToken = 3;
constexpr int kViolationFieldExpectedToken = 4;
constexpr int kViolationFieldStoredChainHash = 5;
constexpr int kViolationFieldExpectedAux = 6;
constexpr int kViolationFieldFailReasonBits = 7;
enum class FailReason : int64_t {
kVerifyChainHashMismatch = 1LL << 0,
kVerifyPositionMismatch = 1LL << 1,
kVerifyRealKvHashMismatch = 1LL << 2,
kWriteTokenMismatch = 1LL << 3,
kWritePositionMismatch = 1LL << 4,
kVerifyTokenMismatch = 1LL << 5,
};
constexpr FailReason operator|(FailReason a, FailReason b) {
return static_cast<FailReason>(static_cast<int64_t>(a) | static_cast<int64_t>(b));
}
constexpr FailReason& operator|=(FailReason& a, FailReason b) {
a = a | b;
return a;
}
} // namespace canary
@@ -0,0 +1,57 @@
from __future__ import annotations
from enum import IntFlag
from typing import Final
CANARY_CHAIN_ANCHOR: Final[int] = 0xC0FFEE1234567890
# Mirrors SGLang's ReqToTokenPool contract: req_pool_idx 0 is the CUDA-graph padding row, while real
# request rows start at 1.
REQ_POOL_IDX_PADDING: Final[int] = 0
# Mirrors SGLang's TokenToKVPoolAllocator contract: token-to-KV slot 0 is reserved for padded-token dummy
# writes. Since req_to_token stores token-to-KV slot ids and is zero-initialized, canary slot 0 is skipped
# instead of treating unfilled entries as real KV slots.
TOKEN_TO_KV_SLOT_PADDING: Final[int] = 0
CANARY_FIELDS_PER_SLOT: Final[int] = 4
CANARY_FIELD_TOKEN: Final[int] = 0
CANARY_FIELD_POSITION: Final[int] = 1
CANARY_FIELD_PREV_HASH: Final[int] = 2
CANARY_FIELD_REAL_KV_HASH: Final[int] = 3
VIOLATION_FIELDS: Final[int] = 8
VIOLATION_FIELD_KERNEL_KIND: Final[int] = 0
VIOLATION_FIELD_SLOT_IDX: Final[int] = 1
VIOLATION_FIELD_POSITION: Final[int] = 2
VIOLATION_FIELD_STORED_TOKEN: Final[int] = 3
VIOLATION_FIELD_EXPECTED_TOKEN: Final[int] = 4
VIOLATION_FIELD_STORED_CHAIN_HASH: Final[int] = 5
VIOLATION_FIELD_EXPECTED_AUX: Final[int] = 6
VIOLATION_FIELD_FAIL_REASON_BITS: Final[int] = 7
class FailReason(IntFlag):
VERIFY_CHAIN_HASH_MISMATCH = 1 << 0
VERIFY_POSITION_MISMATCH = 1 << 1
VERIFY_REAL_KV_HASH_MISMATCH = 1 << 2
WRITE_TOKEN_MISMATCH = 1 << 3
WRITE_POSITION_MISMATCH = 1 << 4
VERIFY_TOKEN_MISMATCH = 1 << 5
_U64_MASK: int = (1 << 64) - 1
def splitmix64(value: int) -> int:
x = value & _U64_MASK
x = ((x ^ (x >> 30)) * 0xBF58476D1CE4E5B9) & _U64_MASK
x = ((x ^ (x >> 27)) * 0x94D049BB133111EB) & _U64_MASK
return (x ^ (x >> 31)) & _U64_MASK
def splitmix64_mix3(a: int, b: int, c: int) -> int:
h = splitmix64(a & _U64_MASK)
h = splitmix64(h ^ (b & _U64_MASK))
h = splitmix64(h ^ (c & _U64_MASK))
return h
@@ -0,0 +1,254 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import IntEnum
from typing import TYPE_CHECKING, Final
import torch
from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.utils import cache_once, load_jit, make_cpp_args
if TYPE_CHECKING:
from tvm_ffi.module import Module
# Bytes per canary slot = CANARY_FIELDS_PER_SLOT * 8.
CANARY_SLOT_BYTES: Final[int] = consts.CANARY_FIELDS_PER_SLOT * 8
class CanaryLaunchTag(IntEnum):
"""Unique tag per (head | tail) × (K | V) × (FULL | SWA) launch."""
HEAD_K_FULL = 0
HEAD_V_FULL = 1
TAIL_K_FULL = 2
TAIL_V_FULL = 3
HEAD_K_SWA = 4
HEAD_V_SWA = 5
TAIL_K_SWA = 6
TAIL_V_SWA = 7
def _assert_contiguous(tensor: torch.Tensor, name: str) -> None:
if not tensor.is_contiguous():
raise ValueError(f"kv-canary: {name} must be contiguous")
@dataclass(frozen=True, slots=True, kw_only=True)
class VerifyOrWriteContext:
"""Shared launch context for canary verify/write kernels.
Fields:
canary_buf: Canary buffer this launch verifies or writes, shape [num_slots, slot_stride_bytes], uint8.
slot_stride_bytes is read from canary_buf.shape[1].
kernel_kind: CanaryLaunchTag identifying which launch fired. Stamped (as int) into every violation row
so host can attribute a violation back to its source launch.
violation_ring: Global append-only sink, shape [ring_capacity, VIOLATION_FIELDS], int64. Shared across
all canary launches; fill-once.
violation_write_index: Global monotonic violation counter, shape [1], int32.
slot_run_counter: Health counter, shape [1], int64. Verify increments by active entries processed;
write increments by write entries processed.
kernel_run_counter: Health counter, shape [1], int64. Incremented by 1 per call.
enable_chain_position_assert: int32 [1] device flag gating the write kernel's chain-step
write_position assert. 0 during warmup / cuda-graph capture; flipped to 1 in
CanaryManager.mark_init_finished().
"""
canary_buf: torch.Tensor
kernel_kind: CanaryLaunchTag
violation_ring: torch.Tensor
violation_write_index: torch.Tensor
slot_run_counter: torch.Tensor
kernel_run_counter: torch.Tensor
enable_chain_position_assert: torch.Tensor
@dataclass(frozen=True, slots=True, kw_only=True)
class VerifyPlan:
"""Flat verify entries consumed by launch_canary_verify_kernel.
Each row is a self-contained (slot_idx, position, prev_slot_idx) triple, so the verify kernel makes no
assumption about the entry's source. prev_slot_idx == -1 flags a chain-seed entry (kernel
anchors on the hardcoded CANARY_CHAIN_ANCHOR constant instead of reading a predecessor).
Sized to a cuda-graph-captured capacity; active prefix is verify_num_valid[0]. Padding tail entries are
unspecified — kernel skips tid >= verify_num_valid[0].
Fields:
verify_slot_indices: Canary slot index per entry, shape [verify_capacity], int64. Already SWA-translated
for the SWA group.
verify_expected_tokens: Source-of-truth token id per entry, shape [verify_capacity], int64.
The plan-side entries kernel gathers from
``CanaryDeviceState.req_to_verify_expected_tokens[rp, position + kv_token_id_vs_position_offset]``;
entries that fall outside the pool's row (e.g. EAGLE draft's last slot rotating in a bonus
token, or padding beyond the per-req length) get the ``-1`` sentinel. The verify kernel
compares against the stored canary token and skips when this is ``-1``.
verify_expected_positions: Expected sequence position per entry, shape [verify_capacity], int64.
verify_prev_slot_indices: Chain predecessor slot per entry, shape [verify_capacity], int64. -1 = chain
head (anchor on CANARY_CHAIN_ANCHOR). Explicit (not derived from verify_slot_indices[i-1])
because chain heads, SWA window starts, cross-req boundaries, and radix-orphan extras break the
"predecessor == previous array entry" assumption.
verify_num_valid: Active entry count, shape [1], int32. Clamped by the plan kernel to
min(total_requested, verify_capacity) so the verify kernel grid never reads past the buffer.
enable: Run-this-step flag, shape [1], int32. 1 = verify kernel runs as usual; 0 = the plan kernel
detected overflow (requested > verify_capacity) and the entire verify launch is skipped this step.
Allocated as 1 by default; the plan kernel rewrites it every step.
"""
verify_slot_indices: torch.Tensor
verify_expected_tokens: torch.Tensor
verify_expected_positions: torch.Tensor
verify_prev_slot_indices: torch.Tensor
verify_num_valid: torch.Tensor
enable: torch.Tensor
@classmethod
def allocate(cls, *, verify_capacity: int, device: torch.device) -> "VerifyPlan":
if verify_capacity <= 0:
raise ValueError(
f"kv-canary: VerifyPlan verify_capacity must be positive, got {verify_capacity}"
)
return cls(
verify_slot_indices=torch.empty(
verify_capacity, dtype=torch.int64, device=device
),
verify_expected_tokens=torch.empty(
verify_capacity, dtype=torch.int64, device=device
),
verify_expected_positions=torch.empty(
verify_capacity, dtype=torch.int64, device=device
),
verify_prev_slot_indices=torch.empty(
verify_capacity, dtype=torch.int64, device=device
),
verify_num_valid=torch.empty(1, dtype=torch.int32, device=device),
# enable defaults to 1 ("run verify") so test helpers that build a VerifyPlan
# directly (no plan kernel) don't have to remember to set it. Plan kernel always
# overwrites this so the default is observable only when no plan kernel runs.
enable=torch.ones(1, dtype=torch.int32, device=device),
)
def zero_for_testing_(self) -> "VerifyPlan":
"""WARN: ONLY use it when testing plan kernel. Do not use it when testing verify or
write kernel to avoid hiding bugs."""
self.verify_slot_indices.zero_()
# Test helpers expect the "skip token check" sentinel after zero-out, matching
# the verify-kernel contract.
self.verify_expected_tokens.fill_(-1)
self.verify_expected_positions.zero_()
self.verify_prev_slot_indices.zero_()
self.verify_num_valid.zero_()
self.enable.zero_()
return self
def launch_canary_verify_kernel(
*,
context: VerifyOrWriteContext,
plan: VerifyPlan,
check_verify_expected_token: bool,
) -> None:
"""Verify one canary buffer against a VerifyPlan.
A fixed persistent grid of `kPersistentBlocks * kVerifyBlockSize` CUDA threads grid-strides over active
verify entries. Each thread reads the slot's 4 stored int64 fields (token_id, position, prev_hash,
real_kv_hash), recomputes the expected prev_hash from the predecessor slot (or from
splitmix64(CANARY_CHAIN_ANCHOR) for chain heads, signaled by prev_slot_idx == -1), and atomically appends
any mismatch (chain hash / position) to violation_ring. Read-only on canary_buf.
Canary slot layout: each slot is canary_buf.shape[1] bytes holding 4 int64 fields (token_id, position,
prev_hash, real_kv_hash). The real_kv_hash field is always 0. Chain link: next.prev_hash ==
splitmix64_mix3(this.prev_hash, this.token_id,
this.position), where splitmix64_mix3 folds each input into a running accumulator
via ``acc = splitmix64(acc ^ next)`` starting from ``splitmix64(prev_hash)``. ``real_kv_hash`` is NOT
folded into the chain (see ``compute_slot_hash`` rationale: keeps chain content-only and immune to
legitimate radix prefix folding). Chain head anchors on
splitmix64(CANARY_CHAIN_ANCHOR), where CANARY_CHAIN_ANCHOR is a hardcoded module-level constant (no
runtime seed parameter — the canary is for bug detection, not adversarial security, so a fixed anchor
is sufficient).
Args:
context: Shared verify/write launch context, including canary buffer, launch tag, violation sink,
and health counters.
plan: Pre-allocated VerifyPlan; addresses baked into cuda-graph capture.
Token-to-KV slot 0 is unconditionally skipped by the verify kernel: SGLang's TokenToKVPoolAllocator
reserves it for padded-token dummy writes, and zero-initialized req_to_token entries therefore point to
a non-real KV slot. Canary-attached pools mirror that contract by reserving canary slot 0.
Implementation:
- CUDA __global__ `canary_verify_kernel`: fixed 1-D grid `(kPersistentBlocks=64, 1, 1)` blocks ×
`(kVerifyBlockSize=512, 1, 1)` threads (= 32768 threads total). Each thread grid-strides over
verify entries `entry_idx ∈ [tid, tid + grid_threads, ...)` until
`min(plan.verify_num_valid[0], verify_capacity)`.
- Per thread, gather:
(a) self_slot fields: 4 separate ``canary_load_field`` int64 loads from
canary_buf[plan.verify_slot_indices[tid]] for (token, position, prev_hash, real_kv_hash).
(b) expected_prev_hash = compute_slot_hash(canary_buf, slot_stride_bytes, prev_slot_idx), which
folds only (token, position, prev_hash) from canary_buf[plan.verify_prev_slot_indices[tid]];
prev_slot_idx == -1 anchors at splitmix64(CANARY_CHAIN_ANCHOR).
- Compare expected vs stored (chain hash, position) and accumulate fail_reason bits; if
non-zero → record_violation().
- record_violation(): idx = atomicAdd(violation_write_index, 1); if idx < ring_capacity, atomic-write
the 8 int64 fields to violation_ring[idx] (kernel_kind, slot_idx, position, stored vs expected
fields, fail_reason).
- Counters: each thread maintains a local count of active entries it processed, warp-reduces via
``__shfl_down_sync`` (offsets 16..1), then the warp leader (lane 0) does a single atomicAdd of the
warp's summed count into slot_run_counter. kernel_run_counter += 1: single thread (tid == 0) does an
atomicAdd once per launch.
Calling contract:
- Pure side-effect; never raises. Host polls violation_write_index[0] > 0 for is_errored and
violation_ring[0] for the first violation.
- kernel_run_counter is bumped every call (canary-ran health signal).
- Safe in cuda-graph capture; caller refills plan in-place before replay.
Pinned by torch reference
:func:`sglang.jit_kernel.kv_canary.verify_ref.launch_canary_verify_kernel_torch_reference`; CUDA must match
byte-for-byte.
"""
canary_buf = context.canary_buf
_assert_contiguous(canary_buf, "canary_buf")
_assert_contiguous(plan.verify_slot_indices, "plan.verify_slot_indices")
_assert_contiguous(plan.verify_expected_tokens, "plan.verify_expected_tokens")
_assert_contiguous(plan.verify_expected_positions, "plan.verify_expected_positions")
_assert_contiguous(plan.verify_prev_slot_indices, "plan.verify_prev_slot_indices")
_assert_contiguous(plan.verify_num_valid, "plan.verify_num_valid")
_assert_contiguous(plan.enable, "plan.enable")
_assert_contiguous(context.violation_ring, "violation_ring")
_assert_contiguous(context.violation_write_index, "violation_write_index")
_assert_contiguous(context.slot_run_counter, "slot_run_counter")
_assert_contiguous(context.kernel_run_counter, "kernel_run_counter")
module = _jit_canary_verify_module(check_verify_expected_token)
module.canary_verify_step_cuda(
canary_buf,
plan.verify_slot_indices,
plan.verify_expected_tokens,
plan.verify_expected_positions,
plan.verify_prev_slot_indices,
plan.verify_num_valid,
plan.enable,
int(context.kernel_kind),
context.violation_ring,
context.violation_write_index,
context.slot_run_counter,
context.kernel_run_counter,
)
@cache_once
def _jit_canary_verify_module(check_verify_expected_token: bool) -> "Module":
args = make_cpp_args(check_verify_expected_token)
return load_jit(
"kv_canary_verify",
*args,
cuda_files=["kv_canary/canary_verify.cuh"],
cuda_wrappers=[
(
"canary_verify_step_cuda",
f"canary::CanaryVerifyKernel<{args}>::run",
),
],
)
@@ -0,0 +1,182 @@
from __future__ import annotations
import torch
from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.kv_canary.consts import splitmix64, splitmix64_mix3
from sglang.jit_kernel.kv_canary.verify import (
VerifyOrWriteContext,
VerifyPlan,
)
_U64_MASK: int = (1 << 64) - 1
_I64_SIGN_BIT: int = 1 << 63
def launch_canary_verify_kernel_torch_reference(
*,
context: VerifyOrWriteContext,
plan: VerifyPlan,
check_verify_expected_token: bool,
) -> None:
canary_buf = context.canary_buf
kernel_kind = context.kernel_kind
violation_ring = context.violation_ring
violation_write_index = context.violation_write_index
slot_run_counter = context.slot_run_counter
kernel_run_counter = context.kernel_run_counter
work_device = torch.device("cpu")
kernel_run_counter.add_(1)
enable = int(plan.enable.detach().to("cpu").item())
if enable == 0:
return
num_valid = int(
plan.verify_slot_indices.new_empty(()).copy_(plan.verify_num_valid[0]).item()
)
capacity = int(plan.verify_slot_indices.shape[0])
active = max(0, min(num_valid, capacity))
if active <= 0:
return
slot_indices_host = plan.verify_slot_indices[:active].to(
device=work_device, dtype=torch.int64
)
if check_verify_expected_token:
expected_input_ids_host = plan.verify_expected_tokens[:active].to(
device=work_device, dtype=torch.int64
)
else:
expected_input_ids_host = torch.full(
(active,), -1, dtype=torch.int64, device=work_device
)
expected_positions_host = plan.verify_expected_positions[:active].to(
device=work_device, dtype=torch.int64
)
prev_slot_indices_host = plan.verify_prev_slot_indices[:active].to(
device=work_device, dtype=torch.int64
)
slot_run_counter.add_(active)
kept_slots: list[int] = []
kept_expected_positions: list[int] = []
kept_expected_input_ids: list[int] = []
kept_prev_slots: list[int] = []
for k in range(active):
s = int(slot_indices_host[k].item())
# Skip SGLang's padded-token dummy KV slot so unfilled req_to_token entries (zero-initialized) do not
# produce spurious chain_hash / position violations.
if s != consts.TOKEN_TO_KV_SLOT_PADDING:
kept_slots.append(s)
kept_expected_positions.append(int(expected_positions_host[k].item()))
kept_expected_input_ids.append(int(expected_input_ids_host[k].item()))
kept_prev_slots.append(int(prev_slot_indices_host[k].item()))
active = len(kept_slots)
if active <= 0:
return
slot_indices_list: list[int] = kept_slots
expected_positions_list: list[int] = kept_expected_positions
expected_input_ids_list: list[int] = kept_expected_input_ids
prev_slot_indices_list: list[int] = kept_prev_slots
buf_i64 = canary_buf.detach().to(device=work_device).contiguous().view(torch.int64)
slot_stride_i64 = int(buf_i64.shape[1])
if slot_stride_i64 < 4:
raise ValueError(
f"kv-canary: canary_buf slot stride must hold at least 4 int64 fields, got {slot_stride_i64}"
)
violation_rows: list[list[int]] = []
for k in range(active):
slot_idx = slot_indices_list[k]
expected_position = expected_positions_list[k]
expected_input_id = expected_input_ids_list[k]
prev_slot = prev_slot_indices_list[k]
stored_token = int(buf_i64[slot_idx, consts.CANARY_FIELD_TOKEN].item())
stored_position = int(buf_i64[slot_idx, consts.CANARY_FIELD_POSITION].item())
stored_chain_hash = int(buf_i64[slot_idx, consts.CANARY_FIELD_PREV_HASH].item())
stored_real_kv_hash = int(
buf_i64[slot_idx, consts.CANARY_FIELD_REAL_KV_HASH].item()
)
prev_reachable = prev_slot != consts.TOKEN_TO_KV_SLOT_PADDING
if prev_reachable:
expected_chain_hash = _to_signed_int64(
compute_slot_hash(buf_i64, prev_slot)
)
else:
expected_chain_hash = stored_chain_hash
# real_kv_hash is always 0 in the naive canary; field[3] is stored as 0 by write.
expected_real_kv_hash = 0
fail_reason = consts.FailReason(0)
if prev_reachable and stored_chain_hash != expected_chain_hash:
fail_reason |= consts.FailReason.VERIFY_CHAIN_HASH_MISMATCH
if check_verify_expected_token:
if expected_input_id != -1 and stored_token != expected_input_id:
fail_reason |= consts.FailReason.VERIFY_TOKEN_MISMATCH
if stored_position != expected_position:
fail_reason |= consts.FailReason.VERIFY_POSITION_MISMATCH
if stored_real_kv_hash != expected_real_kv_hash:
fail_reason |= consts.FailReason.VERIFY_REAL_KV_HASH_MISMATCH
if fail_reason != consts.FailReason(0):
row = [0] * consts.VIOLATION_FIELDS
row[consts.VIOLATION_FIELD_KERNEL_KIND] = int(kernel_kind)
row[consts.VIOLATION_FIELD_SLOT_IDX] = slot_idx
row[consts.VIOLATION_FIELD_POSITION] = stored_position
row[consts.VIOLATION_FIELD_STORED_TOKEN] = stored_token
row[consts.VIOLATION_FIELD_EXPECTED_TOKEN] = expected_input_id
row[consts.VIOLATION_FIELD_STORED_CHAIN_HASH] = stored_chain_hash
row[consts.VIOLATION_FIELD_EXPECTED_AUX] = expected_chain_hash
row[consts.VIOLATION_FIELD_FAIL_REASON_BITS] = int(fail_reason)
violation_rows.append(row)
if len(violation_rows) == 0:
return
num_new_violations = len(violation_rows)
base_idx = int(
violation_write_index.new_empty(()).copy_(violation_write_index[0]).item()
)
ring_capacity = int(violation_ring.shape[0])
new_rows = torch.zeros(
(num_new_violations, consts.VIOLATION_FIELDS), dtype=torch.int64
)
for v, row in enumerate(violation_rows):
for f in range(consts.VIOLATION_FIELDS):
new_rows[v, f] = row[f]
write_count_in_ring = max(0, min(num_new_violations, ring_capacity - base_idx))
if write_count_in_ring > 0:
ring_host = violation_ring.detach().to(device=work_device)
ring_host[base_idx : base_idx + write_count_in_ring, :] = new_rows[
:write_count_in_ring, :
]
violation_ring.copy_(ring_host.to(violation_ring.device))
violation_write_index[0] = violation_write_index[0] + num_new_violations
def _to_signed_int64(value: int) -> int:
value &= _U64_MASK
if value >= _I64_SIGN_BIT:
value -= 1 << 64
return value
def compute_slot_hash(buf_i64: torch.Tensor, source_slot_idx: int) -> int:
if source_slot_idx < 0:
return splitmix64(consts.CANARY_CHAIN_ANCHOR)
token = int(buf_i64[source_slot_idx, consts.CANARY_FIELD_TOKEN].item())
position = int(buf_i64[source_slot_idx, consts.CANARY_FIELD_POSITION].item())
prev_hash = int(buf_i64[source_slot_idx, consts.CANARY_FIELD_PREV_HASH].item())
return splitmix64_mix3(prev_hash, token, position)
@@ -0,0 +1,12 @@
from __future__ import annotations
from sglang.jit_kernel.kv_canary.verify import CANARY_SLOT_BYTES
# Default fixture sizes — small enough for fast tests, large enough that ring overflow / multi-req cases
# stay realistic without bloating the assertion surface.
DEFAULT_RING_CAPACITY: int = 64
DEFAULT_NUM_SLOTS: int = 32
DEFAULT_SLOT_STRIDE_BYTES: int = CANARY_SLOT_BYTES
_U64_MASK: int = (1 << 64) - 1
_I64_SIGN_BIT: int = 1 << 63
@@ -0,0 +1,70 @@
from __future__ import annotations
import re
from pathlib import Path
from sglang.jit_kernel.kv_canary import consts
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=5, suite="base-b-kernel-unit-1-gpu-large")
_CONSTS_CUH: Path = (
Path(__file__).resolve().parents[2] / "csrc" / "kv_canary" / "consts.cuh"
)
def _camel_to_upper_snake(name: str) -> str:
return re.sub(r"([A-Z])", r"_\1", name).lstrip("_").upper()
def _decode(expr: str) -> int:
expr = expr.strip().rstrip("UuLl")
if "<<" in expr:
return 1 << int(expr.split("<<")[1].strip())
return int(expr, 0)
def _parse_constexpr_ints(source: str) -> dict[str, int]:
pattern = re.compile(r"constexpr\s+(?:[\w:]+)\s+(k[A-Za-z]\w*)\s*=\s*([^;]+);")
return {name: _decode(rhs) for name, rhs in pattern.findall(source)}
def _parse_enum_class(source: str, enum_name: str) -> dict[str, int]:
pattern = re.compile(
r"enum\s+class\s+" + re.escape(enum_name) + r"\s*:\s*[^\{]+\{([^}]+)\}"
)
body = pattern.search(source).group(1)
member_re = re.compile(r"(k[A-Za-z]\w*)\s*=\s*([^,]+)")
return {name: _decode(rhs) for name, rhs in member_re.findall(body)}
def test_int_consts_sync() -> None:
cpp = _parse_constexpr_ints(_CONSTS_CUH.read_text(encoding="utf-8"))
cpp_normalized = {_camel_to_upper_snake(n[1:]): v for n, v in cpp.items()}
py = {
n: v
for n, v in vars(consts).items()
if isinstance(v, int) and not isinstance(v, bool) and not n.startswith("_")
}
assert cpp_normalized == py
def test_enums_sync() -> None:
cuh = _CONSTS_CUH.read_text(encoding="utf-8")
for enum_name in ("FailReason",):
cpp_members = _parse_enum_class(cuh, enum_name)
py_enum = getattr(consts, enum_name)
cpp_normalized = {
_camel_to_upper_snake(n[1:]): v for n, v in cpp_members.items()
}
py_normalized = {m.name: int(m.value) for m in py_enum}
assert cpp_normalized == py_normalized
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,62 @@
from __future__ import annotations
from sglang.jit_kernel.benchmark.kv_canary.utils import (
MAX_EXTEND_TOKENS_PER_FORWARD,
build_fast_matrix_cases,
build_full_matrix_cases,
cases_to_x_vals,
)
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=3, suite="base-a-test-cpu")
def test_fast_matrix_cases_include_e2e_decode_and_chunked_prefill_scenarios() -> None:
cases = build_fast_matrix_cases()
scenarios = {case.scenario for case in cases}
assert {
"e2e_decode_steady",
"e2e_decode_tail",
"e2e_prefill_chunk_first",
"e2e_prefill_chunk_second",
"e2e_prefill_chunk_mid",
"e2e_prefill_chunk_last",
} <= scenarios
def test_extend_cases_are_bounded_to_scheduler_chunk_size() -> None:
cases = build_full_matrix_cases()
bad_cases = [
case
for case in cases
if case.mode == "extend"
and case.bs * case.extend_len > MAX_EXTEND_TOKENS_PER_FORWARD
]
assert bad_cases == []
def test_cases_to_x_vals_includes_scenario_axis() -> None:
case = build_fast_matrix_cases()[0]
x_vals = cases_to_x_vals([case])
assert x_vals == [
(
case.scenario,
case.bs,
case.prefix_len,
case.mode,
case.extend_len,
case.pool_kind,
)
]
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main([__file__, "-v"]))