Add real-data KV verification to the KV-canary (#26817)

This commit is contained in:
fzyzcjy
2026-05-31 09:58:32 +08:00
committed by GitHub
parent cdee16e144
commit 0ca610a6df
45 changed files with 2695 additions and 103 deletions
@@ -13,6 +13,7 @@ from sglang.jit_kernel.benchmark.kv_canary.utils import (
build_fast_matrix_cases,
build_full_matrix_cases,
cases_to_x_vals,
make_real_kv_sources,
naive_slot_copy_fn,
)
from sglang.jit_kernel.benchmark.utils import (
@@ -24,6 +25,7 @@ from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.kv_canary.verify import (
CANARY_SLOT_BYTES,
CanaryLaunchTag,
RealKvSource,
VerifyOrWriteContext,
VerifyPlan,
launch_canary_verify_kernel,
@@ -40,6 +42,8 @@ _X_NAMES = [
"mode",
"extend_len",
"pool_kind",
"real_kv_kind",
"hash_mode",
]
_X_VALS = cases_to_x_vals(
get_benchmark_range(
@@ -76,6 +80,7 @@ def _build_verify_inputs(case: BenchCase, *, device: torch.device) -> Tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
tuple[RealKvSource, ...],
]:
total_entries = _verify_entry_count(case)
capacity = max(1, total_entries)
@@ -125,6 +130,10 @@ def _build_verify_inputs(case: BenchCase, *, device: torch.device) -> Tuple[
kernel_run_counter = torch.zeros(1, dtype=torch.int64, device=device)
enable_chain_position_assert = torch.ones(1, dtype=torch.int32, device=device)
real_kv_sources = make_real_kv_sources(
kind=case.real_kv_kind, num_slots=num_slots, device=device
)
return (
canary_buf,
plan,
@@ -133,6 +142,7 @@ def _build_verify_inputs(case: BenchCase, *, device: torch.device) -> Tuple[
slot_run_counter,
kernel_run_counter,
enable_chain_position_assert,
real_kv_sources,
)
@@ -144,7 +154,9 @@ def _build_context(
slot_run_counter: torch.Tensor,
kernel_run_counter: torch.Tensor,
enable_chain_position_assert: torch.Tensor,
real_kv_sources: tuple[RealKvSource, ...],
kernel_kind: CanaryLaunchTag,
hash_mode: consts.RealKvHashMode,
) -> VerifyOrWriteContext:
return VerifyOrWriteContext(
canary_buf=canary_buf,
@@ -153,6 +165,8 @@ def _build_context(
violation_write_index=violation_write_index,
slot_run_counter=slot_run_counter,
kernel_run_counter=kernel_run_counter,
real_kv_sources=real_kv_sources,
real_kv_hash_mode=hash_mode,
enable_chain_position_assert=enable_chain_position_assert,
)
@@ -177,6 +191,8 @@ def benchmark(
mode: str,
extend_len: int,
pool_kind: str,
real_kv_kind: str,
hash_mode: str,
provider: str,
) -> Tuple[float, float, float]:
case = BenchCase(
@@ -186,6 +202,8 @@ def benchmark(
mode=mode,
extend_len=extend_len,
pool_kind=pool_kind,
real_kv_kind=real_kv_kind,
hash_mode=hash_mode,
)
device = torch.device(DEFAULT_DEVICE)
@@ -198,7 +216,9 @@ def benchmark(
slot_run_counter,
kernel_run_counter,
enable_chain_position_assert,
real_kv_sources,
) = _build_verify_inputs(case, device=device)
hash_mode_enum = consts.RealKvHashMode[case.hash_mode.upper()]
context = _build_context(
canary_buf=canary_buf,
violation_ring=violation_ring,
@@ -206,7 +226,9 @@ def benchmark(
slot_run_counter=slot_run_counter,
kernel_run_counter=kernel_run_counter,
enable_chain_position_assert=enable_chain_position_assert,
real_kv_sources=real_kv_sources,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
hash_mode=hash_mode_enum,
)
def fn() -> None:
@@ -247,6 +269,8 @@ def benchmark_kernel_kind(
mode="extend",
extend_len=128,
pool_kind="full",
real_kv_kind="none",
hash_mode="none",
)
device = torch.device(DEFAULT_DEVICE)
@@ -258,8 +282,10 @@ def benchmark_kernel_kind(
slot_run_counter,
kernel_run_counter,
enable_chain_position_assert,
real_kv_sources,
) = _build_verify_inputs(case, device=device)
kernel_kind = CanaryLaunchTag[kernel_kind_name]
hash_mode_enum = consts.RealKvHashMode[case.hash_mode.upper()]
context = _build_context(
canary_buf=canary_buf,
violation_ring=violation_ring,
@@ -267,7 +293,9 @@ def benchmark_kernel_kind(
slot_run_counter=slot_run_counter,
kernel_run_counter=kernel_run_counter,
enable_chain_position_assert=enable_chain_position_assert,
real_kv_sources=real_kv_sources,
kernel_kind=kernel_kind,
hash_mode=hash_mode_enum,
)
def fn() -> None:
@@ -13,6 +13,7 @@ from sglang.jit_kernel.benchmark.kv_canary.utils import (
build_fast_matrix_cases,
build_full_matrix_cases,
cases_to_x_vals,
make_real_kv_sources,
naive_slot_copy_fn,
)
from sglang.jit_kernel.benchmark.utils import (
@@ -39,6 +40,8 @@ _X_NAMES = [
"mode",
"extend_len",
"pool_kind",
"real_kv_kind",
"hash_mode",
]
_X_VALS = cases_to_x_vals(
get_benchmark_range(
@@ -142,6 +145,10 @@ def _build_write_inputs(
kernel_run_counter = torch.zeros(1, dtype=torch.int64, device=device)
enable_chain_position_assert = torch.ones(1, dtype=torch.int32, device=device)
real_kv_sources = make_real_kv_sources(
kind=case.real_kv_kind, num_slots=num_slots, device=device
)
return dict(
canary_buf=canary_buf,
plan=plan,
@@ -155,6 +162,7 @@ def _build_write_inputs(
slot_run_counter=slot_run_counter,
kernel_run_counter=kernel_run_counter,
enable_chain_position_assert=enable_chain_position_assert,
real_kv_sources=real_kv_sources,
)
@@ -162,6 +170,7 @@ def _build_context(
*,
inputs: dict,
kernel_kind: CanaryLaunchTag,
hash_mode: consts.RealKvHashMode,
) -> VerifyOrWriteContext:
return VerifyOrWriteContext(
canary_buf=inputs["canary_buf"],
@@ -170,6 +179,8 @@ def _build_context(
violation_write_index=inputs["violation_write_index"],
slot_run_counter=inputs["slot_run_counter"],
kernel_run_counter=inputs["kernel_run_counter"],
real_kv_sources=inputs["real_kv_sources"],
real_kv_hash_mode=hash_mode,
enable_chain_position_assert=inputs["enable_chain_position_assert"],
)
@@ -194,6 +205,8 @@ def benchmark(
mode: str,
extend_len: int,
pool_kind: str,
real_kv_kind: str,
hash_mode: str,
provider: str,
) -> Tuple[float, float, float]:
case = BenchCase(
@@ -203,14 +216,18 @@ def benchmark(
mode=mode,
extend_len=extend_len,
pool_kind=pool_kind,
real_kv_kind=real_kv_kind,
hash_mode=hash_mode,
)
device = torch.device(DEFAULT_DEVICE)
if provider == "canary":
inputs = _build_write_inputs(case, device=device)
hash_mode_enum = consts.RealKvHashMode[case.hash_mode.upper()]
context = _build_context(
inputs=inputs,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
hash_mode=hash_mode_enum,
)
def fn() -> None:
@@ -256,6 +273,8 @@ def benchmark_kernel_kind(
mode="extend",
extend_len=128,
pool_kind="full",
real_kv_kind="none",
hash_mode="none",
)
device = torch.device(DEFAULT_DEVICE)
@@ -264,9 +283,11 @@ def benchmark_kernel_kind(
case, device=device, mirror_expected_inputs=enable_write_verify_inputs
)
kernel_kind = CanaryLaunchTag[kernel_kind_name]
hash_mode_enum = consts.RealKvHashMode[case.hash_mode.upper()]
context = _build_context(
inputs=inputs,
kernel_kind=kernel_kind,
hash_mode=hash_mode_enum,
)
def fn() -> None:
@@ -5,12 +5,14 @@ from typing import Callable
import torch
from sglang.jit_kernel.kv_canary.verify import CANARY_SLOT_BYTES
from sglang.jit_kernel.kv_canary.verify import CANARY_SLOT_BYTES, RealKvSource
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"]
REAL_KV_AXIS: list[str] = ["none", "small_1src", "med_2src", "max_4src"]
HASH_MODE_AXIS: list[str] = ["none", "partial", "all"]
SWA_WINDOW: int = 128
RING_CAPACITY: int = 256
MAX_EXTEND_TOKENS_PER_FORWARD: int = 4096
@@ -24,12 +26,14 @@ class BenchCase:
mode: str
extend_len: int
pool_kind: str
real_kv_kind: str
hash_mode: 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}"
f"_{self.pool_kind}_rkv{self.real_kv_kind}_hash{self.hash_mode}"
)
@@ -41,6 +45,8 @@ def _case(
mode: str,
extend_len: int,
pool_kind: str,
real_kv_kind: str = "none",
hash_mode: str = "none",
) -> BenchCase:
return BenchCase(
scenario=scenario,
@@ -49,6 +55,8 @@ def _case(
mode=mode,
extend_len=extend_len,
pool_kind=pool_kind,
real_kv_kind=real_kv_kind,
hash_mode=hash_mode,
)
@@ -194,6 +202,56 @@ def build_fast_matrix_cases() -> list[BenchCase]:
extend_len=1,
pool_kind="swa_window_128",
),
_case(
scenario="small_extend_batch_hash",
bs=32,
prefix_len=4096,
mode="extend",
extend_len=128,
pool_kind="full",
real_kv_kind="small_1src",
hash_mode="partial",
),
_case(
scenario="e2e_prefill_chunk_hash",
bs=1,
prefix_len=12288,
mode="extend",
extend_len=4096,
pool_kind="full",
real_kv_kind="med_2src",
hash_mode="all",
),
_case(
scenario="e2e_decode_steady_hash",
bs=256,
prefix_len=4096,
mode="decode",
extend_len=1,
pool_kind="full",
real_kv_kind="max_4src",
hash_mode="all",
),
_case(
scenario="swa_decode_long_prefix_hash",
bs=128,
prefix_len=10240,
mode="decode",
extend_len=1,
pool_kind="swa_window_128",
real_kv_kind="med_2src",
hash_mode="partial",
),
_case(
scenario="smoke_decode_empty_hash",
bs=1,
prefix_len=0,
mode="decode",
extend_len=1,
pool_kind="full",
real_kv_kind="small_1src",
hash_mode="all",
),
]
)
@@ -229,12 +287,41 @@ def build_full_matrix_cases() -> list[BenchCase]:
continue
full.append(case)
fast_base_points = [
(c.bs, c.prefix_len, c.mode, c.extend_len, c.pool_kind)
for c in fast
if c.real_kv_kind == "none" and c.hash_mode == "none"
]
for bs, prefix_len, mode, extend_len, pool_kind in fast_base_points:
for hash_mode in HASH_MODE_AXIS:
if hash_mode == "none":
continue
for real_kv_kind in REAL_KV_AXIS:
if real_kv_kind == "none":
continue
case = _case(
scenario="fold_matrix",
bs=bs,
prefix_len=prefix_len,
mode=mode,
extend_len=extend_len,
pool_kind=pool_kind,
real_kv_kind=real_kv_kind,
hash_mode=hash_mode,
)
if not _is_realistic_extend_case(case):
continue
if case.case_id in fast_keys:
continue
full.append(case)
fast_keys.add(case.case_id)
return full
def cases_to_x_vals(
cases: list[BenchCase],
) -> list[tuple[str, int, int, str, int, str]]:
) -> list[tuple[str, int, int, str, int, str, str, str]]:
return [
(
c.scenario,
@@ -243,11 +330,59 @@ def cases_to_x_vals(
c.mode,
c.extend_len,
c.pool_kind,
c.real_kv_kind,
c.hash_mode,
)
for c in cases
]
def _one_real_kv_source(
*, num_slots: int, num_bytes: int, read_bytes: int, device: torch.device
) -> RealKvSource:
tensor = torch.zeros(max(1, num_slots), num_bytes, dtype=torch.uint8, device=device)
return RealKvSource(
tensor=tensor,
page_size=1,
num_bytes_per_token=num_bytes,
read_bytes=read_bytes,
)
def make_real_kv_sources(
*, kind: str, num_slots: int, device: torch.device
) -> tuple[RealKvSource, ...]:
"""Map a ``real_kv_kind`` axis label to a tuple of ``RealKvSource`` configs.
Byte-volume ladder (none -> small_1src -> med_2src -> max_4src) so the bench exposes the
``real_kv_fold_sources`` PARTIAL/ALL cost gradient. ``max_4src`` hits the
``consts.MAX_REAL_KV_SOURCES = 4`` ABI ceiling.
"""
if kind == "none":
return ()
if kind == "small_1src":
return (
_one_real_kv_source(
num_slots=num_slots, num_bytes=16, read_bytes=16, device=device
),
)
if kind == "med_2src":
return tuple(
_one_real_kv_source(
num_slots=num_slots, num_bytes=32, read_bytes=16, device=device
)
for _ in range(2)
)
if kind == "max_4src":
return tuple(
_one_real_kv_source(
num_slots=num_slots, num_bytes=64, read_bytes=32, device=device
)
for _ in range(4)
)
raise ValueError(f"kv-canary bench: unknown real_kv_kind {kind!r}")
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)
@@ -7,6 +7,15 @@
namespace canary {
// Device-side handle for one real-KV source.
struct RealKvSourceHandle {
const uint8_t* tensor; // raw uint8 byte pointer to the source tensor
int32_t row_stride_bytes; // tensor.shape[1] in bytes (may exceed page_size * num_bytes_per_token)
int32_t page_size;
int32_t num_bytes_per_token;
int32_t read_bytes;
};
SGL_DEVICE uint64_t splitmix64(uint64_t x) {
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
@@ -20,6 +29,55 @@ SGL_DEVICE uint64_t splitmix64_mix3(uint64_t a, uint64_t b, uint64_t c) {
return h;
}
// Read 16 aligned bytes from a source as two uint64 little-endian words, following the RealKvSource access
// invariant. The invariant (from kv_canary/verify.py docstring) is:
//
// tensor[slot_idx // page_size,
// (slot_idx % page_size) * num_bytes_per_token + byte_offset]
//
// row_stride_bytes is the dim-1 size of the underlying tensor in bytes (which may exceed
// page_size * num_bytes_per_token; trailing bytes are skipped).
SGL_DEVICE void real_kv_load_uint4(
const RealKvSourceHandle& src, int64_t slot_idx, int64_t byte_offset, uint64_t& word_lo, uint64_t& word_hi) {
const int64_t row = slot_idx / src.page_size;
const int64_t col_within_page = slot_idx % src.page_size;
const int64_t col = col_within_page * src.num_bytes_per_token + byte_offset;
const int64_t flat_index = row * static_cast<int64_t>(src.row_stride_bytes) + col;
const uint4 vec = *reinterpret_cast<const uint4*>(src.tensor + flat_index);
word_lo = static_cast<uint64_t>(vec.x) | (static_cast<uint64_t>(vec.y) << 32);
word_hi = static_cast<uint64_t>(vec.z) | (static_cast<uint64_t>(vec.w) << 32);
}
SGL_DEVICE uint64_t real_kv_fold_one_source(const RealKvSourceHandle& src, int64_t slot_idx, RealKvHashMode mode) {
const int64_t effective_read_bytes = (mode == RealKvHashMode::kPartial) ? static_cast<int64_t>(16) : src.read_bytes;
uint64_t acc = 0ULL;
for (int64_t byte_offset = 0; byte_offset < effective_read_bytes; byte_offset += 16) {
uint64_t word_lo;
uint64_t word_hi;
real_kv_load_uint4(src, slot_idx, byte_offset, word_lo, word_hi);
acc = splitmix64(acc ^ word_lo);
acc = splitmix64(acc ^ word_hi);
}
return acc;
}
// Fold all configured real-KV sources for a given slot. Iterates sequentially and combines each source's
// contribution via acc = splitmix64(acc XOR source_hash); matches _compute_real_kv_hash_scalar in
// kv_canary/verify_ref.py. In OFF mode the function returns 0 unconditionally (the running real_kv_hash
// field is always 0).
SGL_DEVICE uint64_t
real_kv_fold_sources(const RealKvSourceHandle* sources, int num_sources, int64_t slot_idx, RealKvHashMode mode) {
if (mode == RealKvHashMode::kNone || num_sources <= 0) {
return 0ULL;
}
uint64_t acc = 0ULL;
for (int s = 0; s < num_sources; ++s) {
const uint64_t source_hash = real_kv_fold_one_source(sources[s], slot_idx, mode);
acc = splitmix64(acc ^ source_hash);
}
return acc;
}
struct ViolationSink {
int64_t* __restrict__ ring;
int32_t* __restrict__ write_index;
@@ -38,6 +38,11 @@ struct VerifyKernelParams {
// Health counters.
int64_t* slot_run_counter;
int64_t* kernel_run_counter;
// Real-KV sources (fixed-size ABI; padding slots have read_bytes = 0).
RealKvSourceHandle sources[kMaxRealKvSources];
int32_t num_sources;
RealKvHashMode real_kv_hash_mode;
};
template <bool CHECK_VERIFY_EXPECTED_TOKEN>
@@ -76,13 +81,18 @@ __global__ void canary_verify_kernel(const VerifyKernelParams __grid_constant__
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 int64_t stored_real_kv_hash =
canary_load_field(p.canary_buf, slot_idx, p.slot_stride_bytes, kCanaryFieldRealKvHash);
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;
const uint64_t expected_real_kv_hash_u64 =
real_kv_fold_sources(p.sources, p.num_sources, slot_idx, p.real_kv_hash_mode);
const int64_t expected_real_kv_hash = static_cast<int64_t>(expected_real_kv_hash_u64);
FailReason fail_reason_bits{};
if (prev_reachable && stored_chain_hash != expected_chain_hash) {
fail_reason_bits |= FailReason::kVerifyChainHashMismatch;
@@ -95,6 +105,9 @@ __global__ void canary_verify_kernel(const VerifyKernelParams __grid_constant__
if (stored_position != expected_position) {
fail_reason_bits |= FailReason::kVerifyPositionMismatch;
}
if (stored_real_kv_hash != expected_real_kv_hash) {
fail_reason_bits |= FailReason::kVerifyRealKvHashMismatch;
}
if (fail_reason_bits != FailReason{}) {
record_violation(
@@ -124,6 +137,14 @@ __global__ void canary_verify_kernel(const VerifyKernelParams __grid_constant__
} // namespace
// API source of truth: docstring of canary_verify_step in python/sglang/jit_kernel/kv_canary/verify.py.
//
// ABI notes:
// - real_kv_buf_0 .. real_kv_buf_3 are 4 fixed uint8 tensor slots. Unused slots are dummy 1-byte tensors;
// the host wrapper sets real_kv_source_params[s, 2] (read_bytes) = 0 for them.
// - real_kv_source_params is a CPU int32 tensor of shape [kMaxRealKvSources, 3]: per-source (page_size,
// num_bytes_per_token, read_bytes). Lives on CPU because the host wrapper materializes it from a tuple
// of Python dataclasses.
// - num_sources is passed as int64 (tvm-ffi scalar convention) but always in [0, kMaxRealKvSources].
template <bool CHECK_VERIFY_EXPECTED_TOKEN>
struct CanaryVerifyKernel {
static void
@@ -138,7 +159,14 @@ struct CanaryVerifyKernel {
tvm::ffi::TensorView violation_ring,
tvm::ffi::TensorView violation_write_index,
tvm::ffi::TensorView slot_run_counter,
tvm::ffi::TensorView kernel_run_counter) {
tvm::ffi::TensorView kernel_run_counter,
tvm::ffi::TensorView real_kv_buf_0,
tvm::ffi::TensorView real_kv_buf_1,
tvm::ffi::TensorView real_kv_buf_2,
tvm::ffi::TensorView real_kv_buf_3,
tvm::ffi::TensorView real_kv_source_params,
int64_t num_sources,
int64_t real_kv_hash_mode) {
using namespace host;
SymbolicSize N_slots = {"num_canary_slots"};
@@ -171,6 +199,37 @@ struct CanaryVerifyKernel {
.verify(slot_run_counter)
.verify(kernel_run_counter);
// Real-KV source buffers are 2-D uint8 (any shape); dim 1 carries the row-stride in bytes. They live on
// CUDA. real_kv_source_params is a small CPU int32 table of length kMaxRealKvSources * 3.
SymbolicSize N_real_kv_rows_0 = {"real_kv_rows_0"};
SymbolicSize N_real_kv_cols_0 = {"real_kv_cols_0"};
TensorMatcher({N_real_kv_rows_0, N_real_kv_cols_0})
.with_dtype<uint8_t>()
.with_device<kDLCUDA>(device_)
.verify(real_kv_buf_0);
SymbolicSize N_real_kv_rows_1 = {"real_kv_rows_1"};
SymbolicSize N_real_kv_cols_1 = {"real_kv_cols_1"};
TensorMatcher({N_real_kv_rows_1, N_real_kv_cols_1})
.with_dtype<uint8_t>()
.with_device<kDLCUDA>(device_)
.verify(real_kv_buf_1);
SymbolicSize N_real_kv_rows_2 = {"real_kv_rows_2"};
SymbolicSize N_real_kv_cols_2 = {"real_kv_cols_2"};
TensorMatcher({N_real_kv_rows_2, N_real_kv_cols_2})
.with_dtype<uint8_t>()
.with_device<kDLCUDA>(device_)
.verify(real_kv_buf_2);
SymbolicSize N_real_kv_rows_3 = {"real_kv_rows_3"};
SymbolicSize N_real_kv_cols_3 = {"real_kv_cols_3"};
TensorMatcher({N_real_kv_rows_3, N_real_kv_cols_3})
.with_dtype<uint8_t>()
.with_device<kDLCUDA>(device_)
.verify(real_kv_buf_3);
TensorMatcher({static_cast<int64_t>(kMaxRealKvSources), static_cast<int64_t>(kRealKvSourceFieldsPerEntry)})
.with_dtype<int32_t>()
.with_device<kDLCPU>()
.verify(real_kv_source_params);
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());
@@ -182,6 +241,12 @@ struct CanaryVerifyKernel {
static_cast<int64_t>(kCanaryFieldsPerSlot * sizeof(int64_t)),
" bytes per slot, got ",
slot_stride_bytes);
RuntimeCheck(
num_sources >= 0 && num_sources <= static_cast<int64_t>(kMaxRealKvSources),
"canary_verify: num_sources must be in [0, ",
static_cast<int64_t>(kMaxRealKvSources),
"], got ",
num_sources);
VerifyKernelParams p{};
p.canary_buf = static_cast<const uint8_t*>(canary_buf.data_ptr());
@@ -200,6 +265,19 @@ struct CanaryVerifyKernel {
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());
// Materialize the source handle array on the host. The CPU param tensor carries the per-source ints.
const int32_t* params = static_cast<const int32_t*>(real_kv_source_params.data_ptr());
tvm::ffi::TensorView source_bufs[kMaxRealKvSources] = {real_kv_buf_0, real_kv_buf_1, real_kv_buf_2, real_kv_buf_3};
for (int s = 0; s < kMaxRealKvSources; ++s) {
p.sources[s].tensor = static_cast<const uint8_t*>(source_bufs[s].data_ptr());
p.sources[s].row_stride_bytes = static_cast<int32_t>(source_bufs[s].size(1));
p.sources[s].page_size = params[s * kRealKvSourceFieldsPerEntry + kRealKvSourceFieldPageSize];
p.sources[s].num_bytes_per_token = params[s * kRealKvSourceFieldsPerEntry + kRealKvSourceFieldNumBytesPerToken];
p.sources[s].read_bytes = params[s * kRealKvSourceFieldsPerEntry + kRealKvSourceFieldReadBytes];
}
p.num_sources = static_cast<int32_t>(num_sources);
p.real_kv_hash_mode = static_cast<RealKvHashMode>(real_kv_hash_mode);
const uint32_t grid = kPersistentBlocks;
LaunchKernel(grid, kVerifyBlockSize, device)(canary_verify_kernel<CHECK_VERIFY_EXPECTED_TOKEN>, p);
}
@@ -49,6 +49,11 @@ struct WriteKernelParams {
// Gates the chain-step position assert below. Default-on (1); CanaryManager zeros during the
// warmup window and flips back in mark_init_finished().
const int32_t* enable_chain_position_assert;
// Real-KV sources.
RealKvSourceHandle sources[kMaxRealKvSources];
int32_t num_sources;
RealKvHashMode real_kv_hash_mode;
};
__global__ void canary_write_kernel(const WriteKernelParams __grid_constant__ p) {
@@ -98,8 +103,8 @@ __global__ void canary_write_kernel(const WriteKernelParams __grid_constant__ p)
const int64_t token = p.input_ids[entry_idx];
const int64_t position = p.positions[entry_idx];
// field[3] (kCanaryFieldRealKvHash) is always 0 in the naive canary.
const int64_t real_kv_hash = 0;
const uint64_t real_kv_hash_u64 = real_kv_fold_sources(p.sources, p.num_sources, slot, p.real_kv_hash_mode);
const int64_t real_kv_hash = static_cast<int64_t>(real_kv_hash_u64);
if (p.enable_write_input_assert) {
const int64_t expected_token = p.expected_input_tokens[entry_idx];
@@ -163,7 +168,10 @@ __global__ void canary_write_kernel(const WriteKernelParams __grid_constant__ p)
// API source of truth: docstring of canary_write_step in python/sglang/jit_kernel/kv_canary/write.py.
//
// ABI notes:
// ABI notes (same as verify):
// - real_kv_buf_0 .. real_kv_buf_3 are 4 fixed uint8 tensor slots.
// - real_kv_source_params is a CPU int32 [kMaxRealKvSources, 3] table of (page_size, num_bytes_per_token,
// read_bytes) triplets.
// - out_cache_loc is caller-pre-translated for SWA groups; -1 entries mark skip. The kernel does not
// consult any LUT.
inline void canary_write_step_cuda(
@@ -182,7 +190,14 @@ inline void canary_write_step_cuda(
tvm::ffi::TensorView violation_write_index,
tvm::ffi::TensorView slot_run_counter,
tvm::ffi::TensorView kernel_run_counter,
tvm::ffi::TensorView enable_chain_position_assert) {
tvm::ffi::TensorView enable_chain_position_assert,
tvm::ffi::TensorView real_kv_buf_0,
tvm::ffi::TensorView real_kv_buf_1,
tvm::ffi::TensorView real_kv_buf_2,
tvm::ffi::TensorView real_kv_buf_3,
tvm::ffi::TensorView real_kv_source_params,
int64_t num_sources,
int64_t real_kv_hash_mode) {
using namespace host;
SymbolicSize N_slots = {"num_canary_slots"};
@@ -235,6 +250,35 @@ inline void canary_write_step_cuda(
.verify(kernel_run_counter);
TensorMatcher({1}).with_dtype<int32_t>().with_device<kDLCUDA>(device_).verify(enable_chain_position_assert);
SymbolicSize N_real_kv_rows_0 = {"real_kv_rows_0"};
SymbolicSize N_real_kv_cols_0 = {"real_kv_cols_0"};
TensorMatcher({N_real_kv_rows_0, N_real_kv_cols_0})
.with_dtype<uint8_t>()
.with_device<kDLCUDA>(device_)
.verify(real_kv_buf_0);
SymbolicSize N_real_kv_rows_1 = {"real_kv_rows_1"};
SymbolicSize N_real_kv_cols_1 = {"real_kv_cols_1"};
TensorMatcher({N_real_kv_rows_1, N_real_kv_cols_1})
.with_dtype<uint8_t>()
.with_device<kDLCUDA>(device_)
.verify(real_kv_buf_1);
SymbolicSize N_real_kv_rows_2 = {"real_kv_rows_2"};
SymbolicSize N_real_kv_cols_2 = {"real_kv_cols_2"};
TensorMatcher({N_real_kv_rows_2, N_real_kv_cols_2})
.with_dtype<uint8_t>()
.with_device<kDLCUDA>(device_)
.verify(real_kv_buf_2);
SymbolicSize N_real_kv_rows_3 = {"real_kv_rows_3"};
SymbolicSize N_real_kv_cols_3 = {"real_kv_cols_3"};
TensorMatcher({N_real_kv_rows_3, N_real_kv_cols_3})
.with_dtype<uint8_t>()
.with_device<kDLCUDA>(device_)
.verify(real_kv_buf_3);
TensorMatcher({static_cast<int64_t>(kMaxRealKvSources), static_cast<int64_t>(kRealKvSourceFieldsPerEntry)})
.with_dtype<int32_t>()
.with_device<kDLCPU>()
.verify(real_kv_source_params);
const int64_t slot_stride_bytes = N_stride.unwrap();
const int32_t write_req_capacity = static_cast<int32_t>(N_write_reqs.unwrap());
const int32_t ring_capacity = static_cast<int32_t>(N_ring.unwrap());
@@ -252,6 +296,12 @@ inline void canary_write_step_cuda(
static_cast<int64_t>(kCanaryFieldsPerSlot * sizeof(int64_t)),
" bytes per slot, got ",
slot_stride_bytes);
RuntimeCheck(
num_sources >= 0 && num_sources <= static_cast<int64_t>(kMaxRealKvSources),
"canary_write: num_sources must be in [0, ",
static_cast<int64_t>(kMaxRealKvSources),
"], got ",
num_sources);
WriteKernelParams p{};
p.canary_buf = static_cast<uint8_t*>(canary_buf.data_ptr());
@@ -277,6 +327,18 @@ inline void canary_write_step_cuda(
p.kernel_run_counter = static_cast<int64_t*>(kernel_run_counter.data_ptr());
p.enable_chain_position_assert = static_cast<const int32_t*>(enable_chain_position_assert.data_ptr());
const int32_t* params = static_cast<const int32_t*>(real_kv_source_params.data_ptr());
tvm::ffi::TensorView source_bufs[kMaxRealKvSources] = {real_kv_buf_0, real_kv_buf_1, real_kv_buf_2, real_kv_buf_3};
for (int s = 0; s < kMaxRealKvSources; ++s) {
p.sources[s].tensor = static_cast<const uint8_t*>(source_bufs[s].data_ptr());
p.sources[s].row_stride_bytes = static_cast<int32_t>(source_bufs[s].size(1));
p.sources[s].page_size = params[s * kRealKvSourceFieldsPerEntry + kRealKvSourceFieldPageSize];
p.sources[s].num_bytes_per_token = params[s * kRealKvSourceFieldsPerEntry + kRealKvSourceFieldNumBytesPerToken];
p.sources[s].read_bytes = params[s * kRealKvSourceFieldsPerEntry + kRealKvSourceFieldReadBytes];
}
p.num_sources = static_cast<int32_t>(num_sources);
p.real_kv_hash_mode = static_cast<RealKvHashMode>(real_kv_hash_mode);
// Grid: one block per write req capacity slot; the kernel early-exits on r >= write_num_valid_reqs[0].
// Always launch at least one block so the unconditional kernel_run_counter bump runs even when capacity
// == 0.
@@ -46,4 +46,17 @@ constexpr FailReason& operator|=(FailReason& a, FailReason b) {
return a;
}
enum class RealKvHashMode : int32_t {
kNone = 0,
kPartial = 1,
kAll = 2,
};
constexpr int kMaxRealKvSources = 4;
constexpr int kRealKvSourceFieldsPerEntry = 3;
constexpr int kRealKvSourceFieldPageSize = 0;
constexpr int kRealKvSourceFieldNumBytesPerToken = 1;
constexpr int kRealKvSourceFieldReadBytes = 2;
} // namespace canary
+15 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from enum import IntFlag
from enum import IntEnum, IntFlag
from typing import Final
CANARY_CHAIN_ANCHOR: Final[int] = 0xC0FFEE1234567890
@@ -40,6 +40,20 @@ class FailReason(IntFlag):
VERIFY_TOKEN_MISMATCH = 1 << 5
MAX_REAL_KV_SOURCES: Final[int] = 4
REAL_KV_SOURCE_FIELDS_PER_ENTRY: Final[int] = 3
REAL_KV_SOURCE_FIELD_PAGE_SIZE: Final[int] = 0
REAL_KV_SOURCE_FIELD_NUM_BYTES_PER_TOKEN: Final[int] = 1
REAL_KV_SOURCE_FIELD_READ_BYTES: Final[int] = 2
class RealKvHashMode(IntEnum):
NONE = 0
PARTIAL = 1
ALL = 2
_U64_MASK: int = (1 << 64) - 1
+148 -5
View File
@@ -38,6 +38,90 @@ def _assert_contiguous(tensor: torch.Tensor, name: str) -> None:
raise ValueError(f"kv-canary: {name} must be contiguous")
@dataclass(frozen=True, slots=True, kw_only=True)
class RealKvSource:
"""One piece of real KV the canary folds into its fingerprint.
Slot access invariant (must hold for every source, regardless of underlying layout) — for a given slot_idx,
the canary reads exactly these bytes:
tensor[
slot_idx // page_size,
(slot_idx % page_size) * num_bytes_per_token
: ((slot_idx % page_size) + 1) * num_bytes_per_token
]
Note that ``tensor`` may have "holes" in dim 1 — ``tensor.shape[1]`` can exceed ``page_size *
num_bytes_per_token``. Trailing bytes of each row are ignored by the canary; this is exactly how the
abstraction accommodates pools whose per-row layout interleaves canary-relevant bytes with other metadata
(layer-split storage, K/V interleaving, ...). When ``page_size == 1`` the pattern
collapses to the simple ``tensor[slot_idx, :num_bytes_per_token]`` case.
A pool may expose multiple RealKvSource instances per (canary buffer × K/V half) — the launch wrappers
iterate the source list and fold each into the running real_kv_hash via splitmix64 (one int64 fingerprint
per slot, regardless of source count).
Pool patchers construct sources by:
- viewing / reshaping the underlying KV layer into the canonical [num_rows, dim1_bytes] form (no stage-copy
needed when the underlying storage is already row-major contiguous on dim 0),
- choosing ``page_size`` and ``num_bytes_per_token`` so that the access pattern above lands on the bytes
the canary should fingerprint,
- leaving any per-row padding / non-canary bytes in the trailing portion of each row (they will simply be
skipped).
16-byte alignment precondition: the CUDA fold kernel issues 128-bit aligned loads, so ``read_bytes``,
``num_bytes_per_token``, and the row stride (``tensor.shape[1]`` in bytes) must all be positive
multiples of 16. There is no "skip this source" sentinel — callers omit the source from their
``real_kv_sources`` tuple entirely (factory helpers return an empty tuple in that case).
Fields:
tensor: The source tensor, any shape such that the access pattern above yields ``num_bytes_per_token``
uint8 bytes per slot. Dtype is whatever the underlying pool uses; the canary views the relevant
bytes via ``.view(torch.uint8)``.
page_size: Number of slots packed into one row of dim 0. ``>= 1``.
num_bytes_per_token: Bytes per slot in the dim-1 strip the canary reads. Must be a positive
multiple of 16.
read_bytes: Leading bytes (out of ``num_bytes_per_token``) per slot folded into the fingerprint.
Must be a positive multiple of 16, ``<= num_bytes_per_token``.
"""
tensor: torch.Tensor
page_size: int
num_bytes_per_token: int
read_bytes: int
def __post_init__(self) -> None:
if self.page_size < 1:
raise ValueError(
f"kv-canary: RealKvSource.page_size must be >= 1, got {self.page_size}"
)
if self.num_bytes_per_token <= 0 or self.num_bytes_per_token % 16 != 0:
raise ValueError(
f"kv-canary: RealKvSource.num_bytes_per_token must be a positive multiple of 16, "
f"got {self.num_bytes_per_token}"
)
if (
self.read_bytes <= 0
or self.read_bytes > self.num_bytes_per_token
or self.read_bytes % 16 != 0
):
raise ValueError(
f"kv-canary: RealKvSource.read_bytes must be a positive multiple of 16 in "
f"(0, num_bytes_per_token={self.num_bytes_per_token}], got {self.read_bytes}"
)
if self.tensor.ndim < 2:
raise ValueError(
f"kv-canary: RealKvSource.tensor must be at least 2-D, got shape {tuple(self.tensor.shape)}"
)
row_stride_bytes = int(self.tensor.shape[1]) * self.tensor.element_size()
if row_stride_bytes % 16 != 0:
raise ValueError(
f"kv-canary: RealKvSource.tensor dim-1 byte width must be a multiple of 16, "
f"got {row_stride_bytes} bytes (shape={tuple(self.tensor.shape)}, "
f"dtype={self.tensor.dtype})"
)
@dataclass(frozen=True, slots=True, kw_only=True)
class VerifyOrWriteContext:
"""Shared launch context for canary verify/write kernels.
@@ -53,6 +137,10 @@ class VerifyOrWriteContext:
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.
real_kv_sources: Real KV pieces folded into each slot's real_kv_hash, as a tuple of RealKvSource. Empty
tuple disables the mixin. Multiple sources are folded sequentially via splitmix64 to produce one
int64 fingerprint per slot.
real_kv_hash_mode: RealKvHashMode (NONE / PARTIAL / ALL). Applies uniformly across all sources.
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().
@@ -64,6 +152,8 @@ class VerifyOrWriteContext:
violation_write_index: torch.Tensor
slot_run_counter: torch.Tensor
kernel_run_counter: torch.Tensor
real_kv_sources: tuple[RealKvSource, ...]
real_kv_hash_mode: consts.RealKvHashMode
enable_chain_position_assert: torch.Tensor
@@ -159,11 +249,10 @@ def launch_canary_verify_kernel(
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.
any mismatch (chain hash / position / real_kv_hash) 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,
prev_hash, real_kv_hash). 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
@@ -174,7 +263,7 @@ def launch_canary_verify_kernel(
Args:
context: Shared verify/write launch context, including canary buffer, launch tag, violation sink,
and health counters.
health counters, and real KV fingerprint sources.
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
@@ -192,7 +281,9 @@ def launch_canary_verify_kernel(
(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
(c) For each src in real_kv_sources: read src.read_bytes leading bytes from src.tensor[...] (per the
RealKvSource access invariant) and splitmix64-fold into running_real_kv_hash.
- Compare expected vs stored (chain hash, position, real_kv_hash) 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
@@ -213,6 +304,12 @@ def launch_canary_verify_kernel(
byte-for-byte.
"""
canary_buf = context.canary_buf
real_kv_sources = context.real_kv_sources
if len(real_kv_sources) > consts.MAX_REAL_KV_SOURCES:
raise ValueError(
f"kv-canary: at most {consts.MAX_REAL_KV_SOURCES} RealKvSource entries supported by the CUDA ABI, "
f"got {len(real_kv_sources)}"
)
_assert_contiguous(canary_buf, "canary_buf")
_assert_contiguous(plan.verify_slot_indices, "plan.verify_slot_indices")
@@ -226,6 +323,10 @@ def launch_canary_verify_kernel(
_assert_contiguous(context.slot_run_counter, "slot_run_counter")
_assert_contiguous(context.kernel_run_counter, "kernel_run_counter")
padded_bufs, source_params = _build_real_kv_source_abi(
real_kv_sources=real_kv_sources, device=canary_buf.device
)
module = _jit_canary_verify_module(check_verify_expected_token)
module.canary_verify_step_cuda(
canary_buf,
@@ -240,6 +341,13 @@ def launch_canary_verify_kernel(
context.violation_write_index,
context.slot_run_counter,
context.kernel_run_counter,
padded_bufs[0],
padded_bufs[1],
padded_bufs[2],
padded_bufs[3],
source_params,
len(real_kv_sources),
int(context.real_kv_hash_mode),
)
@@ -257,3 +365,38 @@ def _jit_canary_verify_module(check_verify_expected_token: bool) -> "Module":
),
],
)
def _build_real_kv_source_abi(
*,
real_kv_sources: tuple[RealKvSource, ...],
device: torch.device,
) -> tuple[list[torch.Tensor], torch.Tensor]:
padded_bufs: list[torch.Tensor] = []
params = torch.zeros(
(consts.MAX_REAL_KV_SOURCES, consts.REAL_KV_SOURCE_FIELDS_PER_ENTRY),
dtype=torch.int32,
device="cpu",
)
for i, source in enumerate(real_kv_sources):
_assert_contiguous(source.tensor, f"real_kv_sources[{i}].tensor")
source_u8 = source.tensor.view(torch.uint8)
if source_u8.dim() != 2:
raise ValueError(
f"kv-canary: real_kv_sources[{i}].tensor (viewed as uint8) must be 2-D, "
f"got {source_u8.dim()}-D"
)
padded_bufs.append(source_u8)
params[i, consts.REAL_KV_SOURCE_FIELD_PAGE_SIZE] = source.page_size
params[i, consts.REAL_KV_SOURCE_FIELD_NUM_BYTES_PER_TOKEN] = (
source.num_bytes_per_token
)
params[i, consts.REAL_KV_SOURCE_FIELD_READ_BYTES] = source.read_bytes
# Pad bufs (never read by the kernel — num_sources bounds the iteration); params already zero.
dummy = torch.empty((1, 1), dtype=torch.uint8, device=device)
for _ in range(len(real_kv_sources), consts.MAX_REAL_KV_SOURCES):
padded_bufs.append(dummy)
return padded_bufs, params
@@ -5,6 +5,7 @@ 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 (
RealKvSource,
VerifyOrWriteContext,
VerifyPlan,
)
@@ -25,6 +26,8 @@ def launch_canary_verify_kernel_torch_reference(
violation_write_index = context.violation_write_index
slot_run_counter = context.slot_run_counter
kernel_run_counter = context.kernel_run_counter
real_kv_sources = context.real_kv_sources
real_kv_hash_mode = context.real_kv_hash_mode
work_device = torch.device("cpu")
@@ -113,8 +116,13 @@ def launch_canary_verify_kernel_torch_reference(
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
expected_real_kv_hash_u64 = _compute_real_kv_hash_scalar(
slot_idx=slot_idx,
real_kv_sources=real_kv_sources,
real_kv_hash_mode=real_kv_hash_mode,
work_device=work_device,
)
expected_real_kv_hash = _to_signed_int64(expected_real_kv_hash_u64)
fail_reason = consts.FailReason(0)
if prev_reachable and stored_chain_hash != expected_chain_hash:
@@ -180,3 +188,60 @@ def compute_slot_hash(buf_i64: torch.Tensor, source_slot_idx: int) -> int:
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)
def _compute_real_kv_hash_scalar(
*,
slot_idx: int,
real_kv_sources: tuple[RealKvSource, ...],
real_kv_hash_mode: consts.RealKvHashMode,
work_device: torch.device,
) -> int:
mode = int(real_kv_hash_mode)
if mode == int(consts.RealKvHashMode.NONE) or len(real_kv_sources) == 0:
return 0
acc: int = 0
for source in real_kv_sources:
page_size = source.page_size
num_bytes_per_token = source.num_bytes_per_token
read_bytes = source.read_bytes
tensor_u8 = (
source.tensor.detach().to(device=work_device).contiguous().view(torch.uint8)
)
row = slot_idx // page_size
col_within_page = slot_idx % page_size
col_start = col_within_page * num_bytes_per_token
effective_read_bytes = (
16 if mode == int(consts.RealKvHashMode.PARTIAL) else read_bytes
)
raw_bytes: list[int] = []
for b in range(effective_read_bytes):
raw_bytes.append(int(tensor_u8[row, col_start + b].item()))
source_hash = _splitmix64_fold_bytes_scalar(raw_bytes=raw_bytes)
combined = acc ^ source_hash
acc = splitmix64(combined)
return acc
def _splitmix64_fold_bytes_scalar(*, raw_bytes: list[int]) -> int:
read_bytes = len(raw_bytes)
pad = (8 - read_bytes % 8) % 8
padded = raw_bytes + [0] * pad
num_words = len(padded) // 8
acc: int = 0
for w in range(num_words):
word: int = 0
for k in range(8):
word |= padded[w * 8 + k] << (8 * k)
word &= _U64_MASK
acc = splitmix64(acc ^ word)
return acc
+23 -3
View File
@@ -5,9 +5,11 @@ from typing import TYPE_CHECKING
import torch
from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.kv_canary.verify import (
VerifyOrWriteContext,
_assert_contiguous,
_build_real_kv_source_abi,
)
from sglang.jit_kernel.utils import cache_once, load_jit
@@ -92,7 +94,7 @@ def launch_canary_write_kernel(
- ``slot`` = ``out_cache_loc[i]`` (caller-pre-translated for SWA groups; entries set to -1 are skipped).
- ``token / position`` = ``input_ids[i] / positions[i]``.
- ``real_kv_hash`` is always 0 (the naive canary writes a constant 0 into field[3]).
- ``real_kv_hash`` = ``real_kv_fold_sources(real_kv_sources, slot)`` if ``real_kv_hash_mode != NONE`` else 0.
- Store 4 int64s ``(token, position, running_prev_hash, real_kv_hash)`` into ``canary_buf[slot]``.
- Advance ``running_prev_hash = splitmix64_mix3(prev, token, position)``, where
splitmix64_mix3 folds each input via ``acc = splitmix64(acc ^ next)`` starting from ``splitmix64(prev)``.
@@ -121,7 +123,7 @@ def launch_canary_write_kernel(
Args:
context: Shared verify/write launch context, including canary buffer, launch tag, violation sink,
and health counters.
health counters, and real KV fingerprint sources.
plan: Pre-allocated WritePlan.
input_ids: ForwardBatch.input_ids; token ids being written, shape [num_tokens_padded], int64.
Flattened across reqs in plan.write_offsets order; tail beyond
@@ -157,7 +159,8 @@ def launch_canary_write_kernel(
slot = out_cache_loc[i]; // caller-pre-translated; the kernel never consults a LUT
if (slot < 0) continue; // -1 sentinel = skip (SWA out-of-window or padding)
token = input_ids[i]; position = positions[i];
real_kv_hash = 0; // naive canary always stores 0 into field[3]
real_kv_hash = (real_kv_hash_mode == NONE) ? 0 : real_kv_fold_sources(real_kv_sources, slot);
// applies RealKvSource access invariant
if enable_write_input_assert:
if token != expected_input_tokens[i] or position != expected_input_positions[i]:
record_violation(); // chain still advances on the ACTUAL (token, position) below
@@ -181,6 +184,12 @@ def launch_canary_write_kernel(
byte-for-byte.
"""
canary_buf = context.canary_buf
real_kv_sources = context.real_kv_sources
if len(real_kv_sources) > consts.MAX_REAL_KV_SOURCES:
raise ValueError(
f"kv-canary: at most {consts.MAX_REAL_KV_SOURCES} RealKvSource entries supported by the CUDA ABI, "
f"got {len(real_kv_sources)}"
)
_assert_contiguous(canary_buf, "canary_buf")
_assert_contiguous(plan.write_offsets, "plan.write_offsets")
@@ -209,6 +218,10 @@ def launch_canary_write_kernel(
context.enable_chain_position_assert, "enable_chain_position_assert"
)
padded_bufs, source_params = _build_real_kv_source_abi(
real_kv_sources=real_kv_sources, device=canary_buf.device
)
module = _jit_canary_write_module()
module.canary_write_step_cuda(
canary_buf,
@@ -227,6 +240,13 @@ def launch_canary_write_kernel(
context.slot_run_counter,
context.kernel_run_counter,
context.enable_chain_position_assert,
padded_bufs[0],
padded_bufs[1],
padded_bufs[2],
padded_bufs[3],
source_params,
len(real_kv_sources),
int(context.real_kv_hash_mode),
)
@@ -7,6 +7,7 @@ from sglang.jit_kernel.kv_canary.verify import (
VerifyOrWriteContext,
)
from sglang.jit_kernel.kv_canary.verify_ref import (
_compute_real_kv_hash_scalar,
_to_signed_int64,
compute_slot_hash,
splitmix64_mix3,
@@ -31,6 +32,8 @@ def launch_canary_write_kernel_torch_reference(
violation_write_index = context.violation_write_index
slot_run_counter = context.slot_run_counter
kernel_run_counter = context.kernel_run_counter
real_kv_sources = context.real_kv_sources
real_kv_hash_mode = context.real_kv_hash_mode
enable_chain_position_assert_value = int(
context.enable_chain_position_assert.detach().to("cpu").item()
)
@@ -124,6 +127,13 @@ def launch_canary_write_kernel_torch_reference(
token = int(input_ids_host[entry_idx].item())
position = int(positions_host[entry_idx].item())
real_kv_hash_u64 = _compute_real_kv_hash_scalar(
slot_idx=slot,
real_kv_sources=real_kv_sources,
real_kv_hash_mode=real_kv_hash_mode,
work_device=work_device,
)
if enable_write_input_assert:
assert expected_input_tokens_host is not None
assert expected_input_positions_host is not None
@@ -172,7 +182,9 @@ def launch_canary_write_kernel_torch_reference(
buf_i64[slot, consts.CANARY_FIELD_PREV_HASH] = _to_signed_int64(
running_prev_hash
)
buf_i64[slot, consts.CANARY_FIELD_REAL_KV_HASH] = 0
buf_i64[slot, consts.CANARY_FIELD_REAL_KV_HASH] = _to_signed_int64(
real_kv_hash_u64
)
running_prev_hash = splitmix64_mix3(running_prev_hash, token, position)
@@ -16,6 +16,10 @@ from sglang.jit_kernel.tests.kv_canary._constants import (
DEFAULT_RING_CAPACITY,
DEFAULT_SLOT_STRIDE_BYTES,
)
from sglang.jit_kernel.tests.kv_canary._fixtures import (
make_real_kv_source,
make_real_kv_sources,
)
__all__ = [
"FakeViolationLog",
@@ -26,6 +30,8 @@ __all__ = [
"make_canary_buf",
"make_canary_buf_pair",
"make_log_pair",
"make_real_kv_source",
"make_real_kv_sources",
"make_verify_plan",
"make_verify_plan_pair",
"make_write_plan",
@@ -111,7 +117,7 @@ def make_verify_plan(
``expected_input_ids`` defaults to ``[-1] * n_active`` (the verify-kernel
"skip token check" sentinel) so existing tests that only exercise the
chain / position paths keep working unchanged.
chain / position / real-kv-hash paths keep working unchanged.
"""
n_active = len(slot_indices)
if not (len(positions) == n_active and len(prev_slot_indices) == n_active):
@@ -246,13 +252,13 @@ def write_slot_fields(
token: int,
position: int,
prev_hash: int,
real_kv_hash: int,
) -> None:
view = canary_buf.view(torch.int64)
view[slot_idx, 0] = token
view[slot_idx, 1] = position
view[slot_idx, 2] = prev_hash
# field[3] (real_kv_hash) is always 0 in the naive canary.
view[slot_idx, 3] = 0
view[slot_idx, 3] = real_kv_hash
def stamp_pair(
@@ -262,6 +268,7 @@ def stamp_pair(
token: int,
position: int,
prev_hash: int,
real_kv_hash: int = 0,
) -> None:
"""Stamp the same slot fields into both (cuda, ref) canary buffers."""
for buf in buf_pair:
@@ -271,6 +278,7 @@ def stamp_pair(
token=token,
position=position,
prev_hash=prev_hash,
real_kv_hash=real_kv_hash,
)
@@ -288,10 +296,15 @@ def stamp_clean_chain(
slot_indices: list[int],
tokens: list[int],
positions: list[int],
real_kv_hashes: Optional[list[int]] = None,
) -> list[int]:
n = len(tokens)
real_kv_hashes = real_kv_hashes if real_kv_hashes is not None else [0] * n
running_prev_hash = splitmix64(consts.CANARY_CHAIN_ANCHOR)
stored_prev_hashes: list[int] = []
for slot_idx, token, position in zip(slot_indices, tokens, positions):
for slot_idx, token, position, real_kv_hash in zip(
slot_indices, tokens, positions, real_kv_hashes
):
signed_prev = to_signed_int64(running_prev_hash)
for buf in (cuda_buf, ref_buf):
write_slot_fields(
@@ -300,6 +313,7 @@ def stamp_clean_chain(
token=token,
position=position,
prev_hash=signed_prev,
real_kv_hash=to_signed_int64(real_kv_hash),
)
stored_prev_hashes.append(signed_prev)
running_prev_hash = splitmix64_mix3(running_prev_hash, token, position)
@@ -5,12 +5,14 @@ from typing import Any, Callable, Iterator, Optional
import torch
from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.kv_canary.plan import launch_canary_plan_kernels
from sglang.jit_kernel.kv_canary.plan_ref import (
launch_canary_plan_kernels_torch_reference,
)
from sglang.jit_kernel.kv_canary.verify import (
CanaryLaunchTag,
RealKvSource,
VerifyOrWriteContext,
VerifyPlan,
launch_canary_verify_kernel,
@@ -186,6 +188,9 @@ def _run_both_verify(
plan_ref,
cuda_log: FakeViolationLog,
ref_log: FakeViolationLog,
real_kv_sources_cuda: tuple[RealKvSource, ...],
real_kv_sources_ref: tuple[RealKvSource, ...],
real_kv_hash_mode: consts.RealKvHashMode,
kernel_kind: CanaryLaunchTag = CanaryLaunchTag.HEAD_K_FULL,
assert_equal: bool = True,
check_verify_expected_token: bool = True,
@@ -199,6 +204,8 @@ def _run_both_verify(
slot_run_counter=cuda_log.slot_run_counter,
kernel_run_counter=cuda_log.kernel_run_counter,
enable_chain_position_assert=cuda_log.enable_chain_position_assert,
real_kv_sources=real_kv_sources_cuda,
real_kv_hash_mode=real_kv_hash_mode,
),
plan=plan_cuda,
check_verify_expected_token=check_verify_expected_token,
@@ -212,6 +219,8 @@ def _run_both_verify(
slot_run_counter=ref_log.slot_run_counter,
kernel_run_counter=ref_log.kernel_run_counter,
enable_chain_position_assert=ref_log.enable_chain_position_assert,
real_kv_sources=real_kv_sources_ref,
real_kv_hash_mode=real_kv_hash_mode,
),
plan=plan_ref,
check_verify_expected_token=check_verify_expected_token,
@@ -236,6 +245,9 @@ def _run_both_write(
expected_input_positions: torch.Tensor,
cuda_log: FakeViolationLog,
ref_log: FakeViolationLog,
real_kv_sources_cuda: tuple[RealKvSource, ...],
real_kv_sources_ref: tuple[RealKvSource, ...],
real_kv_hash_mode: consts.RealKvHashMode,
kernel_kind: CanaryLaunchTag = CanaryLaunchTag.HEAD_K_FULL,
assert_equal: bool = True,
) -> None:
@@ -254,6 +266,8 @@ def _run_both_write(
slot_run_counter=cuda_log.slot_run_counter,
kernel_run_counter=cuda_log.kernel_run_counter,
enable_chain_position_assert=cuda_log.enable_chain_position_assert,
real_kv_sources=real_kv_sources_cuda,
real_kv_hash_mode=real_kv_hash_mode,
),
plan=plan_cuda,
input_ids=input_ids,
@@ -272,6 +286,8 @@ def _run_both_write(
slot_run_counter=ref_log.slot_run_counter,
kernel_run_counter=ref_log.kernel_run_counter,
enable_chain_position_assert=ref_log.enable_chain_position_assert,
real_kv_sources=real_kv_sources_ref,
real_kv_hash_mode=real_kv_hash_mode,
),
plan=plan_ref,
input_ids=input_ids,
@@ -381,6 +397,20 @@ def _yield_simpler(inputs: Any) -> Iterator[tuple[str, Any]]:
if fields["extras_count"] > 0:
yield from emit("extras_zero", extras_count=0)
if "real_kv_hash_mode" in fields:
cur = fields["real_kv_hash_mode"]
if hasattr(cur, "value"):
cls = cur.__class__
if int(cur) == 2:
yield from emit("hash_mode_bit", real_kv_hash_mode=cls(1))
elif int(cur) == 1:
yield from emit("hash_mode_off", real_kv_hash_mode=cls(0))
if "real_kv_sources" in fields:
srcs = fields["real_kv_sources"]
if isinstance(srcs, tuple) and len(srcs) > 1:
yield from emit("sources_to_one", real_kv_sources=srcs[:1])
if "enable_write_verify_inputs" in fields:
cur = fields["enable_write_verify_inputs"]
if hasattr(cur, "value") and int(cur) != 0:
@@ -398,13 +428,18 @@ def run_verify_diff(
*,
buf_pair: tuple[torch.Tensor, torch.Tensor],
plan_pair: tuple[VerifyPlan, VerifyPlan],
real_kv_sources_pair: tuple[tuple[RealKvSource, ...], tuple[RealKvSource, ...]] = (
(),
(),
),
real_kv_hash_mode: consts.RealKvHashMode = consts.RealKvHashMode.NONE,
kernel_kind: CanaryLaunchTag = CanaryLaunchTag.HEAD_K_FULL,
device: torch.device = _DEVICE,
assert_equal: bool = True,
check_verify_expected_token: bool = True,
) -> tuple[FakeViolationLog, FakeViolationLog]:
"""Thin wrapper around ``_run_both_verify`` that creates a fresh log pair and packs (cuda, ref)
buf/plan arguments into 2-tuples to drop ~8 lines of boilerplate per call site.
buf/plan/source arguments into 2-tuples to drop ~8 lines of boilerplate per call site.
"""
cuda_log, ref_log = make_log_pair(device=device)
_run_both_verify(
@@ -414,6 +449,9 @@ def run_verify_diff(
plan_ref=plan_pair[1],
cuda_log=cuda_log,
ref_log=ref_log,
real_kv_sources_cuda=real_kv_sources_pair[0],
real_kv_sources_ref=real_kv_sources_pair[1],
real_kv_hash_mode=real_kv_hash_mode,
kernel_kind=kernel_kind,
assert_equal=assert_equal,
check_verify_expected_token=check_verify_expected_token,
@@ -431,12 +469,17 @@ def run_write_diff(
expected_input_tokens: torch.Tensor,
expected_input_positions: torch.Tensor,
enable_write_verify_inputs: bool = False,
real_kv_sources_pair: tuple[tuple[RealKvSource, ...], tuple[RealKvSource, ...]] = (
(),
(),
),
real_kv_hash_mode: consts.RealKvHashMode = consts.RealKvHashMode.NONE,
kernel_kind: CanaryLaunchTag = CanaryLaunchTag.HEAD_K_FULL,
device: torch.device = _DEVICE,
assert_equal: bool = True,
) -> tuple[FakeViolationLog, FakeViolationLog]:
"""Thin wrapper around ``_run_both_write`` that creates a fresh log pair and packs (cuda, ref)
buf/plan arguments into 2-tuples to drop ~10 lines of boilerplate per call site.
buf/plan/source arguments into 2-tuples to drop ~10 lines of boilerplate per call site.
"""
cuda_log, ref_log = make_log_pair(device=device)
_run_both_write(
@@ -452,6 +495,9 @@ def run_write_diff(
expected_input_positions=expected_input_positions,
cuda_log=cuda_log,
ref_log=ref_log,
real_kv_sources_cuda=real_kv_sources_pair[0],
real_kv_sources_ref=real_kv_sources_pair[1],
real_kv_hash_mode=real_kv_hash_mode,
kernel_kind=kernel_kind,
assert_equal=assert_equal,
)
@@ -5,8 +5,12 @@ from typing import Literal, Optional
import torch
from sglang.jit_kernel.kv_canary.verify import VerifyPlan
from sglang.jit_kernel.kv_canary.verify import (
RealKvSource,
VerifyPlan,
)
from sglang.jit_kernel.kv_canary.write import WritePlan
from sglang.jit_kernel.tests.kv_canary._constants import DEFAULT_NUM_SLOTS
_DEVICE = torch.device("cuda")
@@ -72,6 +76,83 @@ def make_req_to_token(
return rtt.contiguous()
def make_real_kv_source(
*,
num_slots: int = DEFAULT_NUM_SLOTS,
num_bytes_per_token: int = 16,
page_size: int = 1,
read_bytes: Optional[int] = None,
pad_dim1: int = 0,
device: torch.device,
fill: int = 0,
) -> RealKvSource:
"""Allocate one RealKvSource with the canonical [num_rows, dim1_bytes] uint8 shape.
``pad_dim1`` adds trailing per-row bytes the canary should skip — used by the "holey dim 1" case to
confirm the kernel never reads past ``page_size * num_bytes_per_token``.
"""
num_rows = (num_slots + page_size - 1) // page_size
cols = page_size * num_bytes_per_token + pad_dim1
tensor = torch.full(
(num_rows, cols), fill_value=fill, dtype=torch.uint8, device=device
)
effective_read = read_bytes if read_bytes is not None else num_bytes_per_token
return RealKvSource(
tensor=tensor,
page_size=page_size,
num_bytes_per_token=num_bytes_per_token,
read_bytes=effective_read,
)
FillStrategy = Literal["constant_per_source", "random_bytes"]
def make_real_kv_sources(
*,
count: int,
num_bytes_per_token: int = 16,
page_size: int = 1,
num_slots: int = DEFAULT_NUM_SLOTS,
device: torch.device,
rng: Optional[random.Random] = None,
fill_strategy: FillStrategy = "constant_per_source",
) -> tuple[RealKvSource, ...]:
sources: list[RealKvSource] = []
for i in range(count):
read_bytes_eff = num_bytes_per_token
src = make_real_kv_source(
num_slots=num_slots,
num_bytes_per_token=num_bytes_per_token,
page_size=page_size,
read_bytes=read_bytes_eff,
device=device,
fill=(i + 1) * 17,
)
if fill_strategy == "random_bytes":
if rng is None:
rng = random.Random(0)
seed = rng.randint(0, 0xFFFFFFFF)
gen = torch.Generator(device=device).manual_seed(seed)
src.tensor.random_(generator=gen)
sources.append(src)
return tuple(sources)
def clone_real_kv_sources(
sources: tuple[RealKvSource, ...],
) -> tuple[RealKvSource, ...]:
return tuple(
RealKvSource(
tensor=src.tensor.clone(),
page_size=src.page_size,
num_bytes_per_token=src.num_bytes_per_token,
read_bytes=src.read_bytes,
)
for src in sources
)
PaddingKind = Literal["none", "trailing", "interleaved"]
@@ -0,0 +1,31 @@
"""Hand-computed Python re-implementation of the real-kv-source fold, kept independent from
``verify_ref._splitmix64_fold_bytes_scalar`` so a ref / kernel co-regression cannot silently fix the
diff comparison."""
from __future__ import annotations
from sglang.jit_kernel.kv_canary.consts import splitmix64
def _fold_words(padded: bytes) -> int:
"""Pack padded bytes little-endian into 8-byte words, fold each via splitmix64 from acc=0."""
num_words = len(padded) // 8
acc = 0
for w in range(num_words):
chunk = padded[w * 8 : (w + 1) * 8]
word = sum(b << (8 * k) for k, b in enumerate(chunk))
acc = splitmix64(acc ^ word)
return splitmix64(0 ^ acc)
def _hand_fold_partial(raw_bytes: bytes) -> int:
"""PARTIAL-mode fold: first min(16, len) bytes, little-endian word-pack + splitmix64, same as ALL."""
truncated = raw_bytes[: min(16, len(raw_bytes))]
pad = (8 - len(truncated) % 8) % 8
return _fold_words(bytes(truncated) + bytes(pad))
def _hand_fold_all(raw_bytes: bytes) -> int:
"""ALL-mode fold: pack bytes little-endian into 8-byte words, fold each via splitmix64, then mix into acc=0."""
pad = (8 - len(raw_bytes) % 8) % 8
return _fold_words(raw_bytes + bytes(pad))
@@ -52,7 +52,7 @@ def test_int_consts_sync() -> None:
def test_enums_sync() -> None:
cuh = _CONSTS_CUH.read_text(encoding="utf-8")
for enum_name in ("FailReason",):
for enum_name in ("RealKvHashMode", "FailReason"):
cpp_members = _parse_enum_class(cuh, enum_name)
py_enum = getattr(consts, enum_name)
cpp_normalized = {
@@ -3,6 +3,7 @@ from __future__ import annotations
import pytest
import torch
from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.kv_canary.verify import (
CanaryLaunchTag,
VerifyOrWriteContext,
@@ -119,6 +120,9 @@ def test_verify_byte_equal_across_repeated_launches_10x() -> None:
plan_ref=plan_ref,
cuda_log=cuda_log,
ref_log=ref_log,
real_kv_sources_cuda=(),
real_kv_sources_ref=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
)
@@ -171,6 +175,9 @@ def test_write_byte_equal_across_repeated_launches_10x() -> None:
expected_input_positions=pseudo_pos,
cuda_log=cuda_log,
ref_log=ref_log,
real_kv_sources_cuda=(),
real_kv_sources_ref=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
)
@@ -284,6 +291,8 @@ def test_verify_multi_launch_100x_counter_linear() -> None:
slot_run_counter=cuda_log.slot_run_counter,
kernel_run_counter=cuda_log.kernel_run_counter,
enable_chain_position_assert=cuda_log.enable_chain_position_assert,
real_kv_sources=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
),
plan=plan_cuda,
check_verify_expected_token=True,
@@ -339,6 +348,9 @@ def test_verify_check_disabled_byte_equal() -> None:
plan_ref=plan_true_ref,
cuda_log=cuda_log_true,
ref_log=ref_log_true,
real_kv_sources_cuda=(),
real_kv_sources_ref=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
check_verify_expected_token=True,
)
@@ -349,6 +361,9 @@ def test_verify_check_disabled_byte_equal() -> None:
plan_ref=plan_false_ref,
cuda_log=cuda_log_false,
ref_log=ref_log_false,
real_kv_sources_cuda=(),
real_kv_sources_ref=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
check_verify_expected_token=False,
)
@@ -12,6 +12,7 @@ from sglang.jit_kernel.kv_canary.plan_ref import (
)
from sglang.jit_kernel.kv_canary.verify import (
CanaryLaunchTag,
RealKvSource,
VerifyOrWriteContext,
VerifyPlan,
launch_canary_verify_kernel,
@@ -28,10 +29,12 @@ from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
assert_canary_buf_equal,
assert_canary_state_equal,
make_canary_buf,
make_real_kv_sources,
stamp_clean_chain,
write_slot_fields,
)
from sglang.jit_kernel.tests.kv_canary._fixtures import (
clone_real_kv_sources,
empty_extras,
make_req_to_token,
)
@@ -62,6 +65,8 @@ def _run_pipeline(
enable_write_verify_inputs: bool,
expected_input_tokens: torch.Tensor,
expected_input_positions: torch.Tensor,
real_kv_sources: tuple[RealKvSource, ...],
real_kv_hash_mode: consts.RealKvHashMode,
verify_capacity: int,
write_req_capacity: int,
req_to_verify_expected_tokens: Optional[torch.Tensor] = None,
@@ -115,6 +120,8 @@ def _run_pipeline(
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=real_kv_sources,
real_kv_hash_mode=real_kv_hash_mode,
)
launch_canary_write_kernel(
context=context,
@@ -142,6 +149,8 @@ def _run_pipeline(
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=real_kv_sources,
real_kv_hash_mode=real_kv_hash_mode,
),
plan=plan_w,
input_ids=input_ids,
@@ -160,6 +169,8 @@ def _run_pipeline(
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=real_kv_sources,
real_kv_hash_mode=real_kv_hash_mode,
),
plan=plan_v,
check_verify_expected_token=check_verify_expected_token,
@@ -185,6 +196,9 @@ def _run_both_and_assert_pipeline_equal(
enable_write_verify_inputs: bool = False,
expected_input_tokens: Optional[torch.Tensor] = None,
expected_input_positions: Optional[torch.Tensor] = None,
real_kv_sources_real: tuple[RealKvSource, ...] = (),
real_kv_sources_ref: tuple[RealKvSource, ...] = (),
real_kv_hash_mode: consts.RealKvHashMode = consts.RealKvHashMode.NONE,
ring_capacity: int = 64,
verify_capacity: int = 256,
write_req_capacity: int = 16,
@@ -240,6 +254,7 @@ def _run_both_and_assert_pipeline_equal(
enable_write_verify_inputs=enable_write_verify_inputs,
expected_input_tokens=expected_input_tokens,
expected_input_positions=expected_input_positions,
real_kv_hash_mode=real_kv_hash_mode,
verify_capacity=verify_capacity,
write_req_capacity=write_req_capacity,
req_to_verify_expected_tokens=req_to_verify_expected_tokens,
@@ -251,12 +266,14 @@ def _run_both_and_assert_pipeline_equal(
real=True,
canary_buf=buf_real,
log=log_real,
real_kv_sources=real_kv_sources_real,
**shared,
)
plan_v_ref, plan_w_ref = _run_pipeline(
real=False,
canary_buf=buf_ref,
log=log_ref,
real_kv_sources=real_kv_sources_ref,
**shared,
)
@@ -436,6 +453,35 @@ def test_pipeline_sweep_no_write() -> None:
assert int(log_ref.slot_run_counter[0].item()) == prefix_len
@pytest.mark.parametrize(
"real_kv_hash_mode",
[
consts.RealKvHashMode.NONE,
consts.RealKvHashMode.PARTIAL,
consts.RealKvHashMode.ALL,
],
)
def test_pipeline_real_kv_mode(real_kv_hash_mode: consts.RealKvHashMode) -> None:
"""real_kv_hash_mode OFF/PARTIAL/ALL: real and ref use cloned sources to prevent ALL-mode hash aliasing."""
sources_real = make_real_kv_sources(count=2, num_slots=64, device=_DEVICE)
sources_ref = clone_real_kv_sources(sources_real)
_run_both_and_assert_pipeline_equal(
req_pool_indices=_t([1]),
prefix_lens=_t([0]),
extend_seq_lens=_t([3]),
input_ids=_t([5, 6, 7]),
positions=_t([0, 1, 2]),
out_cache_loc=_contiguous_out_cache_loc(req_pool_idx=1, start=0, count=3),
req_to_token=_linear_r2t(),
num_slots=64,
extras=empty_extras(),
real_kv_sources_real=sources_real,
real_kv_sources_ref=sources_ref,
real_kv_hash_mode=real_kv_hash_mode,
)
def test_pipeline_pseudo_mode_on_match() -> None:
"""enable_write_verify_inputs=ON, expected==actual: zero violations, buf byte-equal."""
input_ids = _t([1, 2, 3, 4])
@@ -560,6 +606,7 @@ def test_pipeline_ring_overflow_via_real_plan() -> None:
token=slot_idx + 1,
position=slot_idx,
prev_hash=0x1234_DEAD_BEEF_0000 + slot_idx,
real_kv_hash=0,
)
# Step 2: run real pipeline (plan + no write + verify); overflow ring capacity=4 with all n_slots violations.
@@ -609,6 +656,8 @@ def test_pipeline_ring_overflow_via_real_plan() -> None:
slot_run_counter=log_real.slot_run_counter,
kernel_run_counter=log_real.kernel_run_counter,
enable_chain_position_assert=log_real.enable_chain_position_assert,
real_kv_sources=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
),
plan=plan_v_real,
check_verify_expected_token=True,
@@ -624,6 +673,8 @@ def test_pipeline_ring_overflow_via_real_plan() -> None:
slot_run_counter=log_ref.slot_run_counter,
kernel_run_counter=log_ref.kernel_run_counter,
enable_chain_position_assert=log_ref.enable_chain_position_assert,
real_kv_sources=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
),
plan=plan_v_ref,
check_verify_expected_token=True,
@@ -649,6 +700,7 @@ def test_pipeline_kernel_kind_propagates(kernel_kind: CanaryLaunchTag) -> None:
token=7,
position=99,
prev_hash=0,
real_kv_hash=0,
)
_, _, log_real, log_ref, _, _, _, _ = _run_both_and_assert_pipeline_equal(
@@ -50,6 +50,8 @@ def test_cases_to_x_vals_includes_scenario_axis() -> None:
case.mode,
case.extend_len,
case.pool_kind,
case.real_kv_kind,
case.hash_mode,
)
]
@@ -9,6 +9,7 @@ import torch
from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.kv_canary.verify import (
CanaryLaunchTag,
RealKvSource,
VerifyPlan,
)
from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
@@ -19,6 +20,10 @@ from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
stamp_clean_chain,
)
from sglang.jit_kernel.tests.kv_canary._differential import _run_both_verify
from sglang.jit_kernel.tests.kv_canary._fixtures import (
clone_real_kv_sources,
make_real_kv_sources,
)
from sglang.jit_kernel.tests.kv_canary._fuzz_driver import (
FUZZ_SEEDS_PR,
run_fuzz_combo,
@@ -41,16 +46,39 @@ class VerifyFuzzInputs:
plan_cuda: VerifyPlan
plan_ref: VerifyPlan
kernel_kind: CanaryLaunchTag
real_kv_sources_cuda: tuple[RealKvSource, ...]
real_kv_sources_ref: tuple[RealKvSource, ...]
real_kv_hash_mode: consts.RealKvHashMode
ring_capacity: int
check_verify_expected_token: bool
def _draw_random_verify_inputs(rng: random.Random) -> VerifyFuzzInputs:
hash_mode = rng.choice(
[
consts.RealKvHashMode.NONE,
consts.RealKvHashMode.PARTIAL,
consts.RealKvHashMode.ALL,
]
)
src_count = rng.choice([1, 2, 4])
page_size = rng.choice([1, 16])
bytes_per = rng.choice([16, 64, 128])
kernel_kind = rng.choice(list(CanaryLaunchTag))
plan_size = rng.randint(0, 32)
num_slots = max(plan_size + 8, 16)
ring_capacity = rng.choice([16, 64, 256])
sources_cuda = make_real_kv_sources(
count=src_count,
num_bytes_per_token=bytes_per,
page_size=page_size,
num_slots=num_slots,
device=_DEVICE,
rng=rng,
)
sources_ref = clone_real_kv_sources(sources_cuda)
cuda_buf = make_canary_buf(
num_slots=num_slots, slot_stride_bytes=32, device=_DEVICE
)
@@ -68,7 +96,7 @@ def _draw_random_verify_inputs(rng: random.Random) -> VerifyFuzzInputs:
else:
prev_slot_indices.append(slot_indices[i - 1])
if plan_size > 0:
if hash_mode == consts.RealKvHashMode.NONE and plan_size > 0:
stamp_clean_chain(
cuda_buf=cuda_buf,
ref_buf=ref_buf,
@@ -112,6 +140,9 @@ def _draw_random_verify_inputs(rng: random.Random) -> VerifyFuzzInputs:
plan_cuda=plan_cuda,
plan_ref=plan_ref,
kernel_kind=kernel_kind,
real_kv_sources_cuda=sources_cuda,
real_kv_sources_ref=sources_ref,
real_kv_hash_mode=hash_mode,
ring_capacity=ring_capacity,
check_verify_expected_token=check_verify_expected_token,
)
@@ -130,6 +161,9 @@ def _run_one(inputs: VerifyFuzzInputs) -> None:
plan_ref=inputs.plan_ref,
cuda_log=cuda_log,
ref_log=ref_log,
real_kv_sources_cuda=inputs.real_kv_sources_cuda,
real_kv_sources_ref=inputs.real_kv_sources_ref,
real_kv_hash_mode=inputs.real_kv_hash_mode,
kernel_kind=inputs.kernel_kind,
assert_equal=False,
check_verify_expected_token=inputs.check_verify_expected_token,
@@ -155,6 +189,8 @@ def _summarize(inputs: VerifyFuzzInputs) -> str:
n_active = int(inputs.plan_cuda.verify_num_valid[0].item())
return (
f"plan_size={n_active} kind={inputs.kernel_kind.name} "
f"hash_mode={inputs.real_kv_hash_mode.name} "
f"sources={len(inputs.real_kv_sources_cuda)} "
f"ring={inputs.ring_capacity} "
f"check_token={inputs.check_verify_expected_token}"
)
@@ -162,7 +198,7 @@ def _summarize(inputs: VerifyFuzzInputs) -> str:
@pytest.mark.parametrize("seed", FUZZ_SEEDS_PR)
def test_verify_fuzz_full_combo(seed: int) -> None:
"""Multi-dim verify fuzzer: random kernel kind × plan size × ring capacity × N iters, byte-equal."""
"""Multi-dim verify fuzzer: random hash mode × kernel kind × page × bytes × N iters, byte-equal."""
run_fuzz_combo(
seed,
draw_fn=_draw_random_verify_inputs,
@@ -12,13 +12,18 @@ 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 (
CanaryLaunchTag,
RealKvSource,
VerifyOrWriteContext,
VerifyPlan,
launch_canary_verify_kernel,
)
from sglang.jit_kernel.kv_canary.verify_ref import (
_compute_real_kv_hash_scalar,
launch_canary_verify_kernel_torch_reference,
)
from sglang.jit_kernel.kv_canary.write_ref import (
launch_canary_write_kernel_torch_reference,
)
from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
FakeViolationLog,
assert_only_bits_set,
@@ -26,8 +31,11 @@ from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
make_canary_buf,
make_canary_buf_pair,
make_log_pair,
make_real_kv_source,
make_real_kv_sources,
make_verify_plan,
make_verify_plan_pair,
make_write_plan,
read_slot_fields,
stamp_clean_chain,
stamp_pair,
@@ -38,6 +46,11 @@ from sglang.jit_kernel.tests.kv_canary._differential import (
_run_both_verify,
run_verify_diff,
)
from sglang.jit_kernel.tests.kv_canary._fixtures import clone_real_kv_sources
from sglang.jit_kernel.tests.kv_canary._hand_oracle import (
_hand_fold_all,
_hand_fold_partial,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
@@ -66,6 +79,7 @@ def _stamp_head(
token: int = 42,
position: int = 0,
prev_hash: int | None = None,
real_kv_hash: int = 0,
) -> None:
"""``stamp_pair`` with ``prev_hash`` defaulting to ``chain_anchor_signed()`` (the chain-head value)."""
stamp_pair(
@@ -74,6 +88,7 @@ def _stamp_head(
token=token,
position=position,
prev_hash=chain_anchor_signed() if prev_hash is None else prev_hash,
real_kv_hash=real_kv_hash,
)
@@ -114,7 +129,7 @@ def _run_both_verify_no_rkv(
assert_equal: bool = True,
kernel_kind: CanaryLaunchTag = CanaryLaunchTag.HEAD_K_FULL,
) -> None:
"""``_run_both_verify`` — the most common in-place verify run."""
"""``_run_both_verify`` with empty real_kv sources / NONE mode — the most common in-place verify run."""
_run_both_verify(
cuda_canary_buf=buf_pair[0],
ref_canary_buf=buf_pair[1],
@@ -122,6 +137,9 @@ def _run_both_verify_no_rkv(
plan_ref=plan_pair[1],
cuda_log=cuda_log,
ref_log=ref_log,
real_kv_sources_cuda=(),
real_kv_sources_ref=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
kernel_kind=kernel_kind,
assert_equal=assert_equal,
)
@@ -132,6 +150,9 @@ class _VerifySingleSlotInput:
token: int = 42
position: int = 0
stored_prev_hash_signed: int
stored_real_kv_hash_signed: int = 0
real_kv_sources: tuple[RealKvSource, ...] = ()
real_kv_hash_mode: consts.RealKvHashMode = consts.RealKvHashMode.NONE
def _run_verify_single_slot_byte_equal(case: _VerifySingleSlotInput) -> None:
@@ -142,14 +163,66 @@ def _run_verify_single_slot_byte_equal(case: _VerifySingleSlotInput) -> None:
token=case.token,
position=case.position,
prev_hash=case.stored_prev_hash_signed,
real_kv_hash=case.stored_real_kv_hash_signed,
)
sources_cuda = case.real_kv_sources
sources_ref = clone_real_kv_sources(sources_cuda)
plan_pair = _plan_pair_single(slot_idx=1, position=case.position)
run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=case.real_kv_hash_mode,
)
def _stamp_clean_kv_chain(
*,
buf_pair: tuple[torch.Tensor, torch.Tensor],
sources_cuda: tuple[RealKvSource, ...],
input_ids: torch.Tensor,
positions: torch.Tensor,
out_cache_loc: torch.Tensor,
real_kv_hash_mode: consts.RealKvHashMode,
) -> None:
"""Use the Python write ref impl to populate the canary buf for a fresh chain.
Lets verify tests start from a known-good chain without re-implementing splitmix64 by hand.
"""
n = int(input_ids.shape[0])
cuda_buf, ref_buf = buf_pair
write_plan = make_write_plan(
write_offsets=[0, n],
seed_slot_indices=[-1],
num_valid_reqs=1,
device=_DEVICE,
)
log = FakeViolationLog.allocate(device=_DEVICE)
# enable_write_input_assert=False is hard-wired here, so the kernel API requires the
# expected_* tensors be None (otherwise it raises ValueError).
launch_canary_write_kernel_torch_reference(
context=VerifyOrWriteContext(
canary_buf=cuda_buf,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
violation_ring=log.ring,
violation_write_index=log.write_index,
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=sources_cuda,
real_kv_hash_mode=real_kv_hash_mode,
),
plan=write_plan,
input_ids=input_ids,
positions=positions,
out_cache_loc=out_cache_loc,
enable_write_input_assert=False,
expected_input_tokens=None,
expected_input_positions=None,
)
ref_buf.copy_(cuda_buf)
# ---------------------------------------------------------------------------
# Kernel-contract invariants.
# ---------------------------------------------------------------------------
@@ -194,11 +267,12 @@ class TestChain:
tokens = [101, 202, 303, 404, 505]
positions = [0, 1, 2, 3, 4]
slot_indices = [1, 2, 3, 4, 5]
real_kv_hashes = [0, 0, 0, 0, 0]
# Step 1: compute the expected stored prev_hash sequence in pure Python via splitmix64.
expected_prev_hashes_u64: list[int] = []
running = splitmix64(consts.CANARY_CHAIN_ANCHOR)
for token, position in zip(tokens, positions):
for token, position, real_kv_hash in zip(tokens, positions, real_kv_hashes):
expected_prev_hashes_u64.append(running)
running = splitmix64_mix3(running, token, position)
expected_prev_hashes_signed = [
@@ -447,7 +521,40 @@ class TestViolationField:
_fail_bits(cuda_log), consts.FailReason.VERIFY_CHAIN_HASH_MISMATCH
)
@pytest.mark.parametrize("bit_to_trigger", ["POSITION", "PREV_HASH"])
def test_violation_real_kv_hash_mismatch(self) -> None:
"""Mutate one byte of a RealKvSource tensor after writing the chain → REAL_KV_HASH bit on verify."""
buf_pair = _buf_pair()
sources_cuda = make_real_kv_sources(count=1, device=_DEVICE)
# Step: write a chain with real_kv_hash mixin, then mutate one byte in the source tensors so the next
# verify reconstructs a hash that differs from the stored one.
_stamp_clean_kv_chain(
buf_pair=buf_pair,
sources_cuda=sources_cuda,
input_ids=torch.tensor([7, 8, 9], dtype=torch.int64, device=_DEVICE),
positions=torch.tensor([0, 1, 2], dtype=torch.int64, device=_DEVICE),
out_cache_loc=torch.tensor([1, 2, 3], dtype=torch.int64, device=_DEVICE),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
# Mutate one byte in BOTH copies so the verify recomputed hash diverges from stored.
sources_ref = clone_real_kv_sources(sources_cuda)
sources_cuda[0].tensor[1, 0] ^= 0xFF
sources_ref[0].tensor.copy_(sources_cuda[0].tensor)
plan_pair = _plan_pair_single(slot_idx=1, position=0)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
assert_only_bits_set(
_fail_bits(cuda_log), consts.FailReason.VERIFY_REAL_KV_HASH_MISMATCH
)
@pytest.mark.parametrize("bit_to_trigger", ["POSITION", "PREV_HASH", "REAL_KV"])
@pytest.mark.parametrize("injection_position", ["head", "mid", "last"])
@pytest.mark.parametrize("ring_state", ["open", "full"])
def test_violation_bit_injection_position_ring_state_matrix(
@@ -468,41 +575,67 @@ class TestViolationField:
expected_bit = {
"POSITION": consts.FailReason.VERIFY_POSITION_MISMATCH,
"PREV_HASH": consts.FailReason.VERIFY_CHAIN_HASH_MISMATCH,
"REAL_KV": consts.FailReason.VERIFY_REAL_KV_HASH_MISMATCH,
}[bit_to_trigger]
cuda_buf, ref_buf = _buf_pair()
buf_pair = (cuda_buf, ref_buf)
stamp_clean_chain(
cuda_buf=cuda_buf,
ref_buf=ref_buf,
tokens=tokens,
positions=positions,
slot_indices=slot_indices,
)
if bit_to_trigger == "POSITION":
stored_token, stored_pos, stored_prev, _ = read_slot_fields(
canary_buf=cuda_buf, slot_idx=corrupt_slot
)
stamp_pair(
buf_pair,
slot_idx=corrupt_slot,
token=stored_token,
position=stored_pos + 99,
prev_hash=stored_prev,
if bit_to_trigger == "REAL_KV":
buf_pair = _buf_pair()
sources_cuda = make_real_kv_sources(count=1, device=_DEVICE)
_stamp_clean_kv_chain(
buf_pair=buf_pair,
sources_cuda=sources_cuda,
input_ids=torch.tensor(tokens, dtype=torch.int64, device=_DEVICE),
positions=torch.tensor(positions, dtype=torch.int64, device=_DEVICE),
out_cache_loc=torch.tensor(
slot_indices, dtype=torch.int64, device=_DEVICE
),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
sources_ref = clone_real_kv_sources(sources_cuda)
sources_cuda[0].tensor[corrupt_slot, 0] ^= 0xFF
sources_ref[0].tensor.copy_(sources_cuda[0].tensor)
real_kv_hash_mode = consts.RealKvHashMode.ALL
real_kv_sources_cuda = sources_cuda
real_kv_sources_ref = sources_ref
else:
stored_token, stored_pos, stored_prev, _ = read_slot_fields(
canary_buf=cuda_buf, slot_idx=corrupt_slot
)
flipped_prev = stored_prev ^ 1
stamp_pair(
buf_pair,
slot_idx=corrupt_slot,
token=stored_token,
position=stored_pos,
prev_hash=flipped_prev,
cuda_buf, ref_buf = _buf_pair()
buf_pair = (cuda_buf, ref_buf)
stamp_clean_chain(
cuda_buf=cuda_buf,
ref_buf=ref_buf,
tokens=tokens,
positions=positions,
slot_indices=slot_indices,
)
real_kv_hash_mode = consts.RealKvHashMode.NONE
real_kv_sources_cuda = ()
real_kv_sources_ref = ()
if bit_to_trigger == "POSITION":
stored_token, stored_pos, stored_prev, stored_rkv = read_slot_fields(
canary_buf=cuda_buf, slot_idx=corrupt_slot
)
stamp_pair(
buf_pair,
slot_idx=corrupt_slot,
token=stored_token,
position=stored_pos + 99,
prev_hash=stored_prev,
real_kv_hash=stored_rkv,
)
else:
stored_token, stored_pos, stored_prev, stored_rkv = read_slot_fields(
canary_buf=cuda_buf, slot_idx=corrupt_slot
)
flipped_prev = stored_prev ^ 1
stamp_pair(
buf_pair,
slot_idx=corrupt_slot,
token=stored_token,
position=stored_pos,
prev_hash=flipped_prev,
real_kv_hash=stored_rkv,
)
ring_capacity = _RING_CAPACITY
cuda_log, ref_log = make_log_pair(capacity=ring_capacity, device=_DEVICE)
@@ -540,6 +673,9 @@ class TestViolationField:
plan_ref=plan_ref,
cuda_log=cuda_log,
ref_log=ref_log,
real_kv_sources_cuda=real_kv_sources_cuda,
real_kv_sources_ref=real_kv_sources_ref,
real_kv_hash_mode=real_kv_hash_mode,
assert_equal=False,
)
@@ -577,7 +713,451 @@ class TestViolationField:
) == 0, f"chain hash bit unexpectedly set: {bits:#b}"
class TestRealKvHash:
def test_real_kv_mode_off_yields_zero(self) -> None:
"""OFF mode → stored real_kv_hash field stays zero post-write; verify with OFF agrees byte-equal."""
buf_pair = _buf_pair()
sources = make_real_kv_sources(count=2, device=_DEVICE)
plan_pair = _plan_pair_single(slot_idx=1, position=0)
_stamp_head(buf_pair, slot_idx=1, token=1)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources, sources),
)
assert _n_violations(cuda_log) == 0
@pytest.mark.parametrize(
"mode",
[
pytest.param(consts.RealKvHashMode.PARTIAL, id="partial"),
pytest.param(consts.RealKvHashMode.ALL, id="all"),
],
)
def test_real_kv_mode_byte_equal(self, mode: consts.RealKvHashMode) -> None:
"""PARTIAL / ALL modes both produce CUDA-vs-ref byte-equal state on a clean 3-step chain."""
buf_pair = _buf_pair()
sources_cuda = make_real_kv_sources(count=2, device=_DEVICE)
sources_ref = clone_real_kv_sources(sources_cuda)
# Write a chain through the ref so both buffers are byte-equal post-write.
_stamp_clean_kv_chain(
buf_pair=buf_pair,
sources_cuda=sources_cuda,
input_ids=torch.tensor([10, 20, 30], dtype=torch.int64, device=_DEVICE),
positions=torch.tensor([0, 1, 2], dtype=torch.int64, device=_DEVICE),
out_cache_loc=torch.tensor([1, 2, 3], dtype=torch.int64, device=_DEVICE),
real_kv_hash_mode=mode,
)
plan_pair = make_verify_plan_pair(
slot_indices=[1, 2, 3],
positions=[0, 1, 2],
prev_slot_indices=[-1, 1, 2],
device=_DEVICE,
)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=mode,
)
assert _n_violations(cuda_log) == 0
@pytest.mark.parametrize("count", [1, 2, 3, 4])
def test_real_kv_sources_fold_1_to_4(self, count: int) -> None:
"""Fold ``count`` sources sequentially → CUDA matches ref for every count in {1..4}."""
buf_pair = _buf_pair()
sources_cuda = make_real_kv_sources(count=count, device=_DEVICE)
sources_ref = clone_real_kv_sources(sources_cuda)
_stamp_clean_kv_chain(
buf_pair=buf_pair,
sources_cuda=sources_cuda,
input_ids=torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE),
positions=torch.tensor([0, 1], dtype=torch.int64, device=_DEVICE),
out_cache_loc=torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
plan_pair = make_verify_plan_pair(
slot_indices=[1, 2],
positions=[0, 1],
prev_slot_indices=[-1, 1],
device=_DEVICE,
)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
assert _n_violations(cuda_log) == 0
@pytest.mark.parametrize(
"mode,fold_fn,expected_hash",
[
pytest.param(
consts.RealKvHashMode.PARTIAL,
_hand_fold_partial,
0x6041580849E6407D,
id="partial",
),
pytest.param(
consts.RealKvHashMode.ALL,
_hand_fold_all,
0x6041580849E6407D,
id="all",
),
],
)
def test_real_kv_hash_fold_mode_hardcoded(
self,
mode: consts.RealKvHashMode,
fold_fn: Callable[[bytes], int],
expected_hash: int,
) -> None:
# Step 1: build one RealKvSource with read_bytes=16 and a fixed byte pattern at slot 1.
_PATTERN = bytes(
[
0x01,
0x02,
0x04,
0x08,
0x10,
0x20,
0x40,
0x80,
0x81,
0x82,
0x84,
0x88,
0x90,
0xA0,
0xC0,
0xFF,
]
)
buf_pair = _buf_pair()
source_cuda = make_real_kv_source(
num_slots=16,
num_bytes_per_token=16,
page_size=1,
read_bytes=16,
device=_DEVICE,
)
source_cuda.tensor[1, :16] = torch.tensor(list(_PATTERN), dtype=torch.uint8)
source_ref = RealKvSource(
tensor=source_cuda.tensor.clone(),
page_size=source_cuda.page_size,
num_bytes_per_token=source_cuda.num_bytes_per_token,
read_bytes=source_cuda.read_bytes,
)
# Step 2: verify hand-computed fold matches the hex literal.
assert fold_fn(_PATTERN) == expected_hash
# Step 3: stamp slot 1 with a chain-head entry whose real_kv_hash equals the expected value.
_stamp_head(
buf_pair,
slot_idx=1,
token=7,
real_kv_hash=to_signed_int64(expected_hash),
)
# Step 4: 1-entry verify plan; no violation because stored matches recomputed.
plan_pair = _plan_pair_single(slot_idx=1, position=0)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=((source_cuda,), (source_ref,)),
real_kv_hash_mode=mode,
assert_equal=False,
)
assert _n_violations(cuda_log) == 0
# Step 5: mutate one byte in the source so the recomputed hash diverges from stored.
source_cuda.tensor[1, 0] ^= 0xFF
source_ref.tensor.copy_(source_cuda.tensor)
plan_pair2 = _plan_pair_single(slot_idx=1, position=0)
cuda_log2, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair2,
real_kv_sources_pair=((source_cuda,), (source_ref,)),
real_kv_hash_mode=mode,
)
assert_only_bits_set(
_fail_bits(cuda_log2), consts.FailReason.VERIFY_REAL_KV_HASH_MISMATCH
)
def test_real_kv_hash_all_mode_with_multiple_sources(self) -> None:
"""ALL mode with count=2 page=16 bytes=128 sources: chain still verifies clean."""
buf_pair = _buf_pair(num_slots=32)
sources_cuda = make_real_kv_sources(
count=2,
num_bytes_per_token=128,
page_size=16,
num_slots=32,
device=_DEVICE,
)
sources_ref = clone_real_kv_sources(sources_cuda)
slot_indices = [1, 2, 3]
tokens = [100, 200, 300]
positions = [0, 1, 2]
running = splitmix64(consts.CANARY_CHAIN_ANCHOR)
real_kv_hashes: list[int] = []
for slot_idx in slot_indices:
real_kv_hashes.append(
_compute_real_kv_hash_scalar(
real_kv_sources=sources_cuda,
real_kv_hash_mode=consts.RealKvHashMode.ALL,
slot_idx=slot_idx,
work_device=torch.device("cpu"),
)
)
for slot_idx, token, position, rkv in zip(
slot_indices, tokens, positions, real_kv_hashes
):
signed_prev = to_signed_int64(running)
stamp_pair(
buf_pair,
slot_idx=slot_idx,
token=token,
position=position,
prev_hash=signed_prev,
real_kv_hash=to_signed_int64(rkv),
)
running = splitmix64_mix3(running, token, position)
plan_pair = make_verify_plan_pair(
slot_indices=slot_indices,
positions=positions,
prev_slot_indices=[-1, 1, 2],
device=_DEVICE,
)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
assert _n_violations(cuda_log) == 0
def test_real_kv_hash_partial_mode_detects_single_bit_flip(self) -> None:
"""PARTIAL mode + 1-bit flip in source tensor → REAL_KV_HASH bit set in violation row."""
buf_pair = _buf_pair()
sources_cuda = make_real_kv_sources(
count=1, num_bytes_per_token=16, device=_DEVICE
)
slot_idx = 3
row_bytes = (
sources_cuda[0]
.tensor[slot_idx, : sources_cuda[0].read_bytes]
.detach()
.cpu()
.tolist()
)
rkv_clean = _hand_fold_partial(bytes(row_bytes))
_stamp_head(
buf_pair,
slot_idx=slot_idx,
real_kv_hash=to_signed_int64(rkv_clean),
)
sources_cuda[0].tensor[slot_idx, 0] ^= 1
sources_ref = clone_real_kv_sources(sources_cuda)
plan_pair = _plan_pair_single(slot_idx=slot_idx, position=0)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.PARTIAL,
)
assert _n_violations(cuda_log) >= 1
bits = _fail_bits(cuda_log)
assert (
bits & consts.FailReason.VERIFY_REAL_KV_HASH_MISMATCH
), f"expected REAL_KV_HASH bit, got {bits:#b}"
def test_real_kv_off_does_not_deref_real_kv_sources(self) -> None:
buf_pair = _buf_pair(num_slots=8)
_stamp_head(buf_pair, slot_idx=1, token=1)
garbage_source = make_real_kv_source(
num_slots=8,
num_bytes_per_token=16,
page_size=1,
read_bytes=16,
device=_DEVICE,
fill=0xDE,
)
plan_pair = _plan_pair_single(slot_idx=1, position=0)
cuda_log, ref_log = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=((garbage_source,), (garbage_source,)),
assert_equal=False,
)
assert _n_violations(cuda_log) == 0
assert _n_violations(ref_log) == 0
class TestRealKvSource:
def test_real_kv_source_rejects_zero_read_bytes(self) -> None:
"""RealKvSource has no \"skip me\" sentinel — read_bytes=0 must raise rather than silently pass."""
with pytest.raises(ValueError, match="read_bytes"):
RealKvSource(
tensor=torch.zeros((1, 16), dtype=torch.uint8, device=_DEVICE),
page_size=1,
num_bytes_per_token=16,
read_bytes=0,
)
def test_real_kv_source_padding_below_4(self) -> None:
"""Host wrapper pads to 4 slots when fewer sources are supplied; dummy slots are never dereferenced."""
buf_pair = _buf_pair()
sources = make_real_kv_sources(count=2, device=_DEVICE)
plan_pair = _plan_pair_single(slot_idx=1, position=0)
_stamp_head(buf_pair, slot_idx=1, token=1)
run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources, sources),
)
def test_real_kv_source_above_4_raises(self) -> None:
"""``len(real_kv_sources) > 4`` → host wrapper raises ValueError before launching."""
canary_buf = make_canary_buf(device=_DEVICE)
plan = make_verify_plan(
slot_indices=[1], positions=[0], prev_slot_indices=[-1], device=_DEVICE
)
log = FakeViolationLog.allocate(device=_DEVICE)
sources = make_real_kv_sources(count=4, device=_DEVICE)
extra = make_real_kv_source(device=_DEVICE)
too_many = sources + (extra,)
with pytest.raises(ValueError, match="at most 4 RealKvSource"):
launch_canary_verify_kernel(
context=VerifyOrWriteContext(
canary_buf=canary_buf,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
violation_ring=log.ring,
violation_write_index=log.write_index,
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=too_many,
real_kv_hash_mode=consts.RealKvHashMode.NONE,
),
plan=plan,
check_verify_expected_token=True,
)
def test_real_kv_source_holey_dim1(self) -> None:
"""``tensor.shape[1] > page_size * num_bytes_per_token`` → trailing bytes are skipped."""
buf_pair = _buf_pair()
holey_source = make_real_kv_source(
num_slots=16,
num_bytes_per_token=16,
page_size=1,
read_bytes=16,
pad_dim1=16, # 16 trailing pad bytes per row; must be skipped.
device=_DEVICE,
)
trailing_start = holey_source.page_size * holey_source.num_bytes_per_token
# Fill those skipped trailing bytes with garbage; CUDA must not read them.
holey_source.tensor[:, trailing_start:].fill_(0xAA)
sources = (holey_source,)
sources_ref = clone_real_kv_sources(sources)
_stamp_clean_kv_chain(
buf_pair=buf_pair,
sources_cuda=sources,
input_ids=torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE),
positions=torch.tensor([0, 1], dtype=torch.int64, device=_DEVICE),
out_cache_loc=torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
plan_pair = make_verify_plan_pair(
slot_indices=[1, 2],
positions=[0, 1],
prev_slot_indices=[-1, 1],
device=_DEVICE,
)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
assert _n_violations(cuda_log) == 0
class TestLayoutAndScheduling:
def test_page_size_gt_1_access_pattern(self) -> None:
"""``page_size > 1`` → byte access follows ``(row=slot//page, col=(slot%page)*bpt:)``."""
buf_pair = _buf_pair(num_slots=8)
src = make_real_kv_source(
num_slots=8,
num_bytes_per_token=16,
page_size=4, # 2 rows × 4 slots/page × 16 bytes/slot.
read_bytes=16,
device=_DEVICE,
)
# Each slot's 16 bytes get a slot-specific signature so kernel mis-indexing would shift the hash.
flat = src.tensor.view(-1)
for slot_idx in range(8):
row = slot_idx // src.page_size
col = (slot_idx % src.page_size) * src.num_bytes_per_token
for k in range(src.num_bytes_per_token):
flat_index = row * (src.page_size * src.num_bytes_per_token) + col + k
flat[flat_index] = (slot_idx * 13 + k) & 0xFF
sources = (src,)
sources_ref = clone_real_kv_sources(sources)
_stamp_clean_kv_chain(
buf_pair=buf_pair,
sources_cuda=sources,
input_ids=torch.tensor([1, 2], dtype=torch.int64, device=_DEVICE),
positions=torch.tensor([0, 1], dtype=torch.int64, device=_DEVICE),
out_cache_loc=torch.tensor([1, 5], dtype=torch.int64, device=_DEVICE),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
plan_pair = make_verify_plan_pair(
slot_indices=[1, 5],
positions=[0, 1],
prev_slot_indices=[-1, 1],
device=_DEVICE,
)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
assert _n_violations(cuda_log) == 0
def test_swa_translated_slot_indices(self) -> None:
"""SWA-translated slots already passed in plan; verify kernel does no further translation."""
# SWA verify plans carry pre-translated slot indices — the verify kernel never sees the FULL slot
@@ -615,6 +1195,7 @@ class TestLayoutAndScheduling:
token=999,
position=123,
prev_hash=to_signed_int64(0xDEADBEEF),
real_kv_hash=0,
)
plan = make_verify_plan(
slot_indices=[0],
@@ -633,6 +1214,8 @@ class TestLayoutAndScheduling:
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
),
plan=plan,
check_verify_expected_token=True,
@@ -659,6 +1242,7 @@ class TestLayoutAndScheduling:
token=42,
position=1,
prev_hash=to_signed_int64(0x1234),
real_kv_hash=0,
)
plan = make_verify_plan(
slot_indices=[slot_idx],
@@ -687,6 +1271,8 @@ class TestLayoutAndScheduling:
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
),
plan=plan,
check_verify_expected_token=True,
@@ -702,6 +1288,65 @@ class TestLayoutAndScheduling:
== int(kernel_run_before[0].item()) + 1
)
def test_paged_layout_page_size_16(self) -> None:
"""page_size=16: slot→page mapping doesn't change verify chain semantics on a clean chain."""
buf_pair = _buf_pair(num_slots=64)
sources_cuda = make_real_kv_sources(
count=1,
num_bytes_per_token=16,
page_size=16,
num_slots=64,
device=_DEVICE,
)
sources_ref = clone_real_kv_sources(sources_cuda)
# Step: cross a page boundary by writing slots [15, 16] which straddle pages 0 and 1.
slot_indices = [15, 16]
tokens = [77, 88]
positions = [0, 1]
running = splitmix64(consts.CANARY_CHAIN_ANCHOR)
# Use the reference fold (8-byte little-endian word pack + splitmix64), not a
# byte-by-byte loop, so the stamped real_kv_hash matches what the kernel /
# verify reference will recompute. A byte-by-byte fold was the previous bug
# here and triggered REAL_KV_HASH violations on otherwise clean chains.
rkv_values = [
_compute_real_kv_hash_scalar(
slot_idx=slot_idx,
real_kv_sources=sources_cuda,
real_kv_hash_mode=consts.RealKvHashMode.ALL,
work_device=_DEVICE,
)
for slot_idx in slot_indices
]
for slot_idx, token, position, rkv in zip(
slot_indices, tokens, positions, rkv_values
):
signed_prev = to_signed_int64(running)
stamp_pair(
buf_pair,
slot_idx=slot_idx,
token=token,
position=position,
prev_hash=signed_prev,
real_kv_hash=to_signed_int64(rkv),
)
running = splitmix64_mix3(running, token, position)
plan_pair = make_verify_plan_pair(
slot_indices=slot_indices,
positions=positions,
prev_slot_indices=[-1, 15],
device=_DEVICE,
)
cuda_log, _ = run_verify_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
assert _n_violations(cuda_log) == 0
def test_empty_plan_keeps_slot_counter_unchanged(self) -> None:
buf_pair = _buf_pair(num_slots=8)
_stamp_head(buf_pair, slot_idx=1, token=7)
@@ -1166,6 +1811,22 @@ class TestBoundarySweep:
)
)
@pytest.mark.parametrize(
"stored_rkv_val",
[0, 1, 0xFFFFFFFFFFFFFFFF, 0x8000000000000000],
)
def test_real_kv_hash_boundary_byte_equal_sweep(self, stored_rkv_val: int) -> None:
"""Sweep real_kv_hash boundary values; assert CUDA vs ref state byte-equal."""
sources_cuda = make_real_kv_sources(count=1, device=_DEVICE)
_run_verify_single_slot_byte_equal(
_VerifySingleSlotInput(
stored_prev_hash_signed=chain_anchor_signed(),
stored_real_kv_hash_signed=to_signed_int64(stored_rkv_val),
real_kv_sources=sources_cuda,
real_kv_hash_mode=consts.RealKvHashMode.PARTIAL,
)
)
class TestVerifyExpectedInputIds:
"""Cover the new verify-time token-id check via VerifyPlan.verify_expected_tokens."""
@@ -6,7 +6,11 @@ from dataclasses import dataclass
import pytest
import torch
from sglang.jit_kernel.kv_canary.verify import CanaryLaunchTag
from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.kv_canary.verify import (
CanaryLaunchTag,
RealKvSource,
)
from sglang.jit_kernel.kv_canary.write import WritePlan
from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
FakeViolationLog,
@@ -16,6 +20,10 @@ from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
stamp_pair,
)
from sglang.jit_kernel.tests.kv_canary._differential import _run_both_write
from sglang.jit_kernel.tests.kv_canary._fixtures import (
clone_real_kv_sources,
make_real_kv_sources,
)
from sglang.jit_kernel.tests.kv_canary._fuzz_driver import (
FUZZ_SEEDS_PR,
run_fuzz_combo,
@@ -44,11 +52,24 @@ class WriteFuzzInputs:
enable_write_verify_inputs: bool
expected_input_tokens: torch.Tensor
expected_input_positions: torch.Tensor
real_kv_sources_cuda: tuple[RealKvSource, ...]
real_kv_sources_ref: tuple[RealKvSource, ...]
real_kv_hash_mode: consts.RealKvHashMode
ring_capacity: int
def _draw_random_write_inputs(rng: random.Random) -> WriteFuzzInputs:
enable_write_verify_inputs = rng.choice([False, True])
hash_mode = rng.choice(
[
consts.RealKvHashMode.NONE,
consts.RealKvHashMode.PARTIAL,
consts.RealKvHashMode.ALL,
]
)
src_count = rng.choice([1, 2, 4])
page_size = rng.choice([1, 16])
bytes_per = rng.choice([16, 64, 128])
kernel_kind = rng.choice(list(CanaryLaunchTag))
ring_capacity = rng.choice([16, 64, 256])
@@ -57,6 +78,16 @@ def _draw_random_write_inputs(rng: random.Random) -> WriteFuzzInputs:
total_tokens = sum(per_req_tokens)
num_slots = max(total_tokens + 8, 16)
sources_cuda = make_real_kv_sources(
count=src_count,
num_bytes_per_token=bytes_per,
page_size=page_size,
num_slots=num_slots,
device=_DEVICE,
rng=rng,
)
sources_ref = clone_real_kv_sources(sources_cuda)
cuda_buf = make_canary_buf(
num_slots=num_slots, slot_stride_bytes=32, device=_DEVICE
)
@@ -140,6 +171,9 @@ def _draw_random_write_inputs(rng: random.Random) -> WriteFuzzInputs:
enable_write_verify_inputs=enable_write_verify_inputs,
expected_input_tokens=expected_input_tokens,
expected_input_positions=expected_input_positions,
real_kv_sources_cuda=sources_cuda,
real_kv_sources_ref=sources_ref,
real_kv_hash_mode=hash_mode,
ring_capacity=ring_capacity,
)
@@ -163,6 +197,9 @@ def _run_one(inputs: WriteFuzzInputs) -> None:
expected_input_positions=inputs.expected_input_positions,
cuda_log=cuda_log,
ref_log=ref_log,
real_kv_sources_cuda=inputs.real_kv_sources_cuda,
real_kv_sources_ref=inputs.real_kv_sources_ref,
real_kv_hash_mode=inputs.real_kv_hash_mode,
kernel_kind=inputs.kernel_kind,
assert_equal=False,
)
@@ -196,13 +233,14 @@ def _summarize(inputs: WriteFuzzInputs) -> str:
total = int(inputs.plan_cuda.write_offsets[n_active].item())
return (
f"n_reqs={n_active} total_tokens={total} kind={inputs.kernel_kind.name} "
f"pseudo={inputs.enable_write_verify_inputs}"
f"pseudo={inputs.enable_write_verify_inputs} hash_mode={inputs.real_kv_hash_mode.name} "
f"sources={len(inputs.real_kv_sources_cuda)}"
)
@pytest.mark.parametrize("seed", FUZZ_SEEDS_PR)
def test_write_fuzz_full_combo(seed: int) -> None:
"""Multi-dim write fuzzer: random pseudo/kernel × N iters, byte-equal."""
"""Multi-dim write fuzzer: random pseudo/hash/kernel/page/source × N iters, byte-equal."""
run_fuzz_combo(
seed,
draw_fn=_draw_random_write_inputs,
@@ -1,6 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
from unittest.mock import patch
import pytest
@@ -12,6 +13,7 @@ from sglang.jit_kernel.kv_canary.consts import splitmix64, splitmix64_mix3
from sglang.jit_kernel.kv_canary.verify import (
CANARY_SLOT_BYTES,
CanaryLaunchTag,
RealKvSource,
VerifyOrWriteContext,
launch_canary_verify_kernel,
)
@@ -26,6 +28,8 @@ from sglang.jit_kernel.tests.kv_canary._canary_helpers import (
make_canary_buf,
make_canary_buf_pair,
make_log_pair,
make_real_kv_source,
make_real_kv_sources,
make_verify_plan,
make_write_plan,
make_write_plan_pair,
@@ -38,8 +42,13 @@ from sglang.jit_kernel.tests.kv_canary._differential import (
run_write_diff,
)
from sglang.jit_kernel.tests.kv_canary._fixtures import (
clone_real_kv_sources,
dummy_pseudo_tensors,
)
from sglang.jit_kernel.tests.kv_canary._hand_oracle import (
_hand_fold_all,
_hand_fold_partial,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
@@ -73,6 +82,10 @@ def _run_write(
enable_write_verify_inputs: bool = False,
expected_input_tokens: torch.Tensor | None = None,
expected_input_positions: torch.Tensor | None = None,
real_kv_sources_pair: (
tuple[tuple[RealKvSource, ...], tuple[RealKvSource, ...]] | None
) = None,
real_kv_hash_mode: consts.RealKvHashMode = consts.RealKvHashMode.NONE,
assert_equal: bool = True,
) -> tuple[FakeViolationLog, FakeViolationLog]:
"""Shared scaffold: build write plan + pseudo tensors and call ``run_write_diff``.
@@ -117,6 +130,9 @@ def _run_write(
if expected_input_positions is None:
expected_input_positions = pseudo_positions
extra_kwargs: dict = {}
if real_kv_sources_pair is not None:
extra_kwargs["real_kv_sources_pair"] = real_kv_sources_pair
return run_write_diff(
buf_pair=buf_pair,
plan_pair=plan_pair,
@@ -126,7 +142,9 @@ def _run_write(
enable_write_verify_inputs=enable_write_verify_inputs,
expected_input_tokens=expected_input_tokens,
expected_input_positions=expected_input_positions,
real_kv_hash_mode=real_kv_hash_mode,
assert_equal=assert_equal,
**extra_kwargs,
)
@@ -135,6 +153,8 @@ class _WriteSingleSlotInput:
token: int = 42
position: int = 0
enable_write_verify_inputs: bool = False
real_kv_sources: tuple[RealKvSource, ...] = ()
real_kv_hash_mode: consts.RealKvHashMode = consts.RealKvHashMode.NONE
class _RecordingWriteModule:
@@ -146,12 +166,16 @@ class _RecordingWriteModule:
def _run_write_single_slot_byte_equal(case: _WriteSingleSlotInput) -> None:
sources_cuda = case.real_kv_sources
sources_ref = clone_real_kv_sources(sources_cuda)
_run_write(
buf_pair=_make_default_buf_pair(),
input_ids=[case.token],
positions=[case.position],
out_cache_loc=[0],
enable_write_verify_inputs=case.enable_write_verify_inputs,
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=case.real_kv_hash_mode,
)
@@ -242,6 +266,8 @@ class TestSeedSlot:
slot_run_counter=verify_log.slot_run_counter,
kernel_run_counter=verify_log.kernel_run_counter,
enable_chain_position_assert=verify_log.enable_chain_position_assert,
real_kv_sources=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
),
plan=verify_plan,
check_verify_expected_token=True,
@@ -270,11 +296,12 @@ class TestSeedSlot:
tokens = [101, 202, 303, 404, 505]
positions = [11, 12, 13, 14, 15]
real_kv = [0, 0, 0, 0, 0]
out_cache_loc = [0, 1, 2, 3, 4]
expected_prev_hashes: list[int] = []
running = predecessor_advance
for t, p in zip(tokens, positions):
for t, p, r in zip(tokens, positions, real_kv):
expected_prev_hashes.append(running)
running = splitmix64_mix3(running, t, p)
@@ -305,6 +332,7 @@ class TestSeedSlot:
seed_slot = 3
seed_token = 7
seed_position = 1
seed_real_kv = 0
expected_seed_prev_hash = splitmix64(consts.CANARY_CHAIN_ANCHOR)
stamp_pair(
self.buf_pair,
@@ -312,6 +340,7 @@ class TestSeedSlot:
token=seed_token,
position=seed_position,
prev_hash=to_signed_int64(expected_seed_prev_hash),
real_kv_hash=to_signed_int64(seed_real_kv),
)
new_slot = 4
@@ -355,11 +384,12 @@ class TestChain:
tokens = [101, 202, 303, 404, 505]
positions = [0, 1, 2, 3, 4]
out_cache_loc = [0, 1, 2, 3, 4]
real_kv_hashes = [0, 0, 0, 0, 0]
# Step 1: compute the expected stored prev_hash sequence in pure Python via splitmix64.
expected_prev_hashes_u64: list[int] = []
running = splitmix64(consts.CANARY_CHAIN_ANCHOR)
for token, position in zip(tokens, positions):
for token, position, real_kv_hash in zip(tokens, positions, real_kv_hashes):
expected_prev_hashes_u64.append(running)
running = splitmix64_mix3(running, token, position)
expected_prev_hashes_signed = [
@@ -385,6 +415,41 @@ class TestChain:
assert stored_prev_hash == expected_prev_signed
assert stored_real_kv_hash == 0
def test_chain_advances_with_real_kv_hash_all(self) -> None:
"""ALL mode + 2 sources + 5-step chain: stored prev_hash recoverable from seed."""
cuda_buf = self.buf_pair[0]
sources_cuda = make_real_kv_sources(
count=2,
num_bytes_per_token=16,
page_size=1,
num_slots=16,
device=_DEVICE,
)
sources_ref = clone_real_kv_sources(sources_cuda)
slot_indices = [1, 2, 3, 4, 5]
tokens = [11, 22, 33, 44, 55]
positions = [0, 1, 2, 3, 4]
_run_write(
buf_pair=self.buf_pair,
input_ids=tokens,
positions=positions,
out_cache_loc=slot_indices,
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
running = splitmix64(consts.CANARY_CHAIN_ANCHOR)
for slot_idx, token, position in zip(slot_indices, tokens, positions):
stored_prev_signed, stored_real_kv_hash = read_slot_fields(
canary_buf=cuda_buf, slot_idx=slot_idx
)[2:]
assert stored_prev_signed == to_signed_int64(
running
), f"slot {slot_idx}: stored prev_hash != recomputed chain step"
running = splitmix64_mix3(running, token, position)
class TestMockMode:
def setup_method(self) -> None:
@@ -496,6 +561,8 @@ class TestMockMode:
slot_run_counter=verify_log.slot_run_counter,
kernel_run_counter=verify_log.kernel_run_counter,
enable_chain_position_assert=verify_log.enable_chain_position_assert,
real_kv_sources=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
),
plan=verify_plan,
check_verify_expected_token=True,
@@ -686,6 +753,265 @@ class TestSlotHandling:
), f"slot {slot} from earlier bs=8 run was overwritten by bs=3 run"
class TestRealKvHash:
def setup_method(self) -> None:
self.buf_pair = _make_default_buf_pair()
def test_real_kv_mode_off_writes_zero(self) -> None:
"""``consts.RealKvHashMode.NONE`` → ``real_kv_hash`` field is written as 0 regardless of source presence."""
sources = make_real_kv_sources(count=2, device=_DEVICE)
_run_write(
buf_pair=self.buf_pair,
input_ids=[1, 2],
positions=[0, 1],
out_cache_loc=[0, 1],
real_kv_sources_pair=(sources, sources),
)
_, _, _, real_kv_0 = read_slot_fields(canary_buf=self.buf_pair[0], slot_idx=0)
_, _, _, real_kv_1 = read_slot_fields(canary_buf=self.buf_pair[0], slot_idx=1)
assert real_kv_0 == 0
assert real_kv_1 == 0
@pytest.mark.parametrize(
"mode",
[
pytest.param(consts.RealKvHashMode.PARTIAL, id="partial"),
pytest.param(consts.RealKvHashMode.ALL, id="all"),
],
)
def test_real_kv_mode_byte_equal(self, mode: consts.RealKvHashMode) -> None:
"""PARTIAL / ALL modes both produce CUDA-vs-ref byte-equal write state on a 3-entry chain."""
sources_cuda = make_real_kv_sources(count=2, device=_DEVICE)
sources_ref = clone_real_kv_sources(sources_cuda)
_run_write(
buf_pair=self.buf_pair,
input_ids=[10, 20, 30],
positions=[0, 1, 2],
out_cache_loc=[0, 1, 2],
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=mode,
)
@pytest.mark.parametrize("count", [1, 2, 3, 4])
def test_real_kv_sources_fold_1_to_4(self, count: int) -> None:
"""Folding ``count`` sources sequentially → CUDA matches ref for every count in {1..4}."""
sources_cuda = make_real_kv_sources(count=count, device=_DEVICE)
sources_ref = clone_real_kv_sources(sources_cuda)
_run_write(
buf_pair=self.buf_pair,
input_ids=[1, 2],
positions=[0, 1],
out_cache_loc=[0, 1],
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
def test_real_kv_source_above_4_raises(self) -> None:
"""``len(real_kv_sources) > 4`` → host wrapper raises ValueError before launching."""
cuda_buf = make_canary_buf(device=_DEVICE)
plan = make_write_plan(
write_offsets=[0, 1],
seed_slot_indices=[-1],
num_valid_reqs=1,
device=_DEVICE,
)
input_ids = _int32_tensor([1])
positions = _int32_tensor([0])
out_cache_loc = _int32_tensor([0])
log = FakeViolationLog.allocate(device=_DEVICE)
sources = make_real_kv_sources(count=4, device=_DEVICE)
extra = make_real_kv_source(device=_DEVICE)
too_many = sources + (extra,)
with pytest.raises(ValueError, match="at most 4 RealKvSource"):
launch_canary_write_kernel(
context=VerifyOrWriteContext(
canary_buf=cuda_buf,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
violation_ring=log.ring,
violation_write_index=log.write_index,
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=too_many,
real_kv_hash_mode=consts.RealKvHashMode.NONE,
),
plan=plan,
input_ids=input_ids,
positions=positions,
out_cache_loc=out_cache_loc,
enable_write_input_assert=False,
expected_input_tokens=None,
expected_input_positions=None,
)
@pytest.mark.parametrize(
"mode,fold_fn,expected_hash",
[
pytest.param(
consts.RealKvHashMode.PARTIAL,
_hand_fold_partial,
0x6041580849E6407D,
id="partial",
),
pytest.param(
consts.RealKvHashMode.ALL,
_hand_fold_all,
0x6041580849E6407D,
id="all",
),
],
)
def test_real_kv_hash_fold_mode_writes_expected_hash_hardcoded(
self,
mode: consts.RealKvHashMode,
fold_fn: Callable[[bytes], int],
expected_hash: int,
) -> None:
# Step 1: build one RealKvSource with read_bytes=16 and a fixed byte pattern at slot 0.
_PATTERN = bytes(
[
0x01,
0x02,
0x04,
0x08,
0x10,
0x20,
0x40,
0x80,
0x81,
0x82,
0x84,
0x88,
0x90,
0xA0,
0xC0,
0xFF,
]
)
# Step 2: verify hand-computed fold matches the hex literal.
assert fold_fn(_PATTERN) == expected_hash
source_cuda = make_real_kv_source(
num_slots=16,
num_bytes_per_token=16,
page_size=1,
read_bytes=16,
device=_DEVICE,
)
source_cuda.tensor[0, :16] = torch.tensor(list(_PATTERN), dtype=torch.uint8)
source_ref = RealKvSource(
tensor=source_cuda.tensor.clone(),
page_size=source_cuda.page_size,
num_bytes_per_token=source_cuda.num_bytes_per_token,
read_bytes=source_cuda.read_bytes,
)
# Step 3: run write kernel on slot 0 with the given mode.
_run_write(
buf_pair=self.buf_pair,
input_ids=[7],
positions=[0],
out_cache_loc=[0],
real_kv_sources_pair=((source_cuda,), (source_ref,)),
real_kv_hash_mode=mode,
)
# Step 4: assert stored real_kv_hash equals the hand-computed hex literal.
_, _, _, stored_real_kv_hash = read_slot_fields(
canary_buf=self.buf_pair[0], slot_idx=0
)
assert stored_real_kv_hash == to_signed_int64(
expected_hash
), f"stored_real_kv_hash={stored_real_kv_hash:#x} expected={to_signed_int64(expected_hash):#x}"
def test_paged_real_kv_hash_consistent_across_slots(self) -> None:
"""page=16: writing two slots inside same page yields independent real_kv_hash per slot."""
sources_cuda = make_real_kv_sources(
count=1,
num_bytes_per_token=16,
page_size=16,
num_slots=16,
device=_DEVICE,
)
pattern_slot3 = bytes(range(1, 17))
pattern_slot7 = bytes(range(101, 117))
sources_cuda[0].tensor[0, 3 * 16 : 4 * 16] = torch.tensor(
list(pattern_slot3), dtype=torch.uint8, device=_DEVICE
)
sources_cuda[0].tensor[0, 7 * 16 : 8 * 16] = torch.tensor(
list(pattern_slot7), dtype=torch.uint8, device=_DEVICE
)
sources_ref = clone_real_kv_sources(sources_cuda)
_run_write(
buf_pair=self.buf_pair,
input_ids=[42, 84],
positions=[0, 1],
out_cache_loc=[3, 7],
real_kv_sources_pair=(sources_cuda, sources_ref),
real_kv_hash_mode=consts.RealKvHashMode.ALL,
)
slot3 = read_slot_fields(canary_buf=self.buf_pair[0], slot_idx=3)
slot7 = read_slot_fields(canary_buf=self.buf_pair[0], slot_idx=7)
assert slot3[3] == to_signed_int64(_hand_fold_all(pattern_slot3))
assert slot7[3] == to_signed_int64(_hand_fold_all(pattern_slot7))
assert slot3[3] != slot7[3]
def test_multi_source_real_kv_fold_order_matters(self) -> None:
"""Two sources folded in reverse order yields a different real_kv_hash (fold is ordered)."""
sources_a = make_real_kv_sources(
count=2, num_bytes_per_token=16, num_slots=8, device=_DEVICE
)
sources_b = tuple(reversed(sources_a))
def _run_with(srcs: tuple[RealKvSource, ...]) -> tuple[int, int, int, int]:
buf = make_canary_buf(num_slots=16, slot_stride_bytes=32, device=_DEVICE)
plan = make_write_plan(
write_offsets=[0, 1],
seed_slot_indices=[-1],
num_valid_reqs=1,
device=_DEVICE,
)
log = FakeViolationLog.allocate(device=_DEVICE)
launch_canary_write_kernel(
context=VerifyOrWriteContext(
canary_buf=buf,
kernel_kind=CanaryLaunchTag.HEAD_K_FULL,
violation_ring=log.ring,
violation_write_index=log.write_index,
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=srcs,
real_kv_hash_mode=consts.RealKvHashMode.ALL,
),
plan=plan,
input_ids=_int32_tensor([1]),
positions=_int32_tensor([0]),
out_cache_loc=_int32_tensor([2]),
enable_write_input_assert=False,
expected_input_tokens=None,
expected_input_positions=None,
)
torch.cuda.synchronize()
return read_slot_fields(canary_buf=buf, slot_idx=2)
fields_a = _run_with(sources_a)
fields_b = _run_with(sources_b)
assert fields_a[3] != 0
assert fields_b[3] != 0
assert (
fields_a[3] != fields_b[3]
), "reversing source order must change real_kv_hash (fold is ordered)"
class TestRunCounter:
def setup_method(self) -> None:
self.buf_pair = _make_default_buf_pair()
@@ -718,6 +1044,9 @@ class TestRunCounter:
expected_input_positions=pseudo_positions,
cuda_log=cuda_log,
ref_log=ref_log,
real_kv_sources_cuda=(),
real_kv_sources_ref=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
assert_equal=False,
)
@@ -806,6 +1135,8 @@ class TestMisc:
slot_run_counter=log.slot_run_counter,
kernel_run_counter=log.kernel_run_counter,
enable_chain_position_assert=log.enable_chain_position_assert,
real_kv_sources=(),
real_kv_hash_mode=consts.RealKvHashMode.NONE,
)
module = _RecordingWriteModule()
+13 -4
View File
@@ -6,6 +6,8 @@ from typing import Optional
import torch
from sglang.jit_kernel.kv_canary.verify import RealKvSource
class PoolKind(IntEnum):
"""Which attention regime a canary group belongs to.
@@ -22,12 +24,13 @@ class PoolKind(IntEnum):
@dataclass(frozen=True, slots=True, kw_only=True)
class CanaryBufferGroup:
"""Canary buffers for one (PoolKind × K-half | V-half) on a pool.
"""Canary buffers + real-KV sources for one (PoolKind × K-half | V-half) on a pool.
Each (head | tail) launch sees a single 2-D uint8 buf for the canary. Head and tail use separate canary
buffers so they can be staged at different points in the forward pass without overwriting each other.
Each (head | tail) launch sees a single 2-D uint8 buf for the canary, plus a list of RealKvSource for the
real-KV mixin. Head and tail use separate canary buffers so they can be staged at different points in the
forward pass without overwriting each other.
MLA-style pools have no V half (v_head / v_tail = None). SWA pools have two
MLA-style pools have no V half (v_head / v_tail = None; real_kv_sources_v is empty). SWA pools have two
CanaryBufferGroup instances (FULL sized to the full sub-pool, SWA sized to the swa sub-pool).
Fields:
@@ -36,6 +39,10 @@ class CanaryBufferGroup:
k_tail: Tail canary buffer for K-half launches, same shape, uint8.
v_head: Same for V-half, or None for MLA-style pools.
v_tail: Same for V-half, or None.
real_kv_sources_k: Real KV pieces folded into the K-half canary's real_kv_hash. Tuple length is
pool-specific (1 for simple MHA, more for multi-layer / weird-layout pools). Empty tuple =
real-KV mixin disabled for this half.
real_kv_sources_v: Same for V-half. Empty tuple iff v_head is None or the mixin is disabled.
swa_index_lut: SWA full-to-swa index mapping LUT, shape [full_pool_size + 1], int64, or None for FULL
groups. Used by launch_canary_plan_kernels to translate verify/seed slot indices at plan time, and by
launch_canary_write_kernel to translate write slots inline. None iff kind == PoolKind.FULL.
@@ -50,6 +57,8 @@ class CanaryBufferGroup:
k_tail: torch.Tensor
v_head: Optional[torch.Tensor]
v_tail: Optional[torch.Tensor]
real_kv_sources_k: tuple[RealKvSource, ...]
real_kv_sources_v: tuple[RealKvSource, ...]
swa_index_lut: Optional[torch.Tensor]
kv_token_id_vs_position_offset: int
+9
View File
@@ -4,6 +4,9 @@ from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING
from sglang.jit_kernel.kv_canary.consts import (
RealKvHashMode,
)
from sglang.srt.environ import envs
if TYPE_CHECKING:
@@ -33,6 +36,8 @@ class CanaryConfig:
sweep_interval: 0 disables sweep entirely; positive N means every N-th forward step the runner
additionally walks all radix-tree-held slots (overlap with per-forward HEAD/TAIL is harmless
redundancy) and verifies them.
real_kv_hash_mode: RealKvHashMode (NONE / PARTIAL / ALL). Uniform across head/tail/sweep launches;
PARTIAL (first 16B, hard cap) is cheap enough for production defaults.
enable_write_input_assert: bool. True = launch_canary_write_kernel additionally compares
forward_batch.input_ids[i] / positions[i] against caller-supplied expected_input_tokens[i] /
expected_input_positions[i]; mismatch records a violation. Only useful when something else
@@ -43,6 +48,7 @@ class CanaryConfig:
mode: CanaryMode
ring_capacity: int
sweep_interval: int
real_kv_hash_mode: RealKvHashMode
enable_write_input_assert: bool
@classmethod
@@ -53,9 +59,12 @@ class CanaryConfig:
f"kv-canary: kv_canary must be one of none/log/raise, got {mode_raw!r}"
)
real_kv_raw = server_args.kv_canary_real_data.strip().upper()
return cls(
mode=CanaryMode(mode_raw),
ring_capacity=envs.SGLANG_KV_CANARY_RING_CAPACITY.get(),
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(),
)
+26
View File
@@ -5,8 +5,12 @@ from typing import Optional
import torch
from sglang.jit_kernel.kv_canary.consts import (
RealKvHashMode,
)
from sglang.jit_kernel.kv_canary.verify import (
CanaryLaunchTag,
RealKvSource,
VerifyOrWriteContext,
VerifyPlan,
launch_canary_verify_kernel,
@@ -28,6 +32,7 @@ class CanaryEndpoint:
kernel_kind: CanaryLaunchTag
canary_buf: torch.Tensor
full_to_swa_index_mapping: Optional[torch.Tensor]
real_kv_sources: tuple[RealKvSource, ...]
slot_run_counter_view: torch.Tensor
kernel_run_counter_view: torch.Tensor
enable_chain_position_assert: torch.Tensor
@@ -44,6 +49,7 @@ class CanaryEndpoint:
enable_verify_token_assert: bool,
expected_inputs: ExpectedInputs,
violation_log: ViolationLog,
real_kv_hash_mode: RealKvHashMode,
) -> None:
if _is_sweep_tag(self.kernel_kind):
raise NotImplementedError(
@@ -52,6 +58,7 @@ class CanaryEndpoint:
context = self._make_verify_or_write_context(
violation_log=violation_log,
real_kv_hash_mode=real_kv_hash_mode,
)
launch_canary_verify_kernel(
context=context,
@@ -87,6 +94,7 @@ class CanaryEndpoint:
*,
verify_plan: VerifyPlan,
violation_log: ViolationLog,
real_kv_hash_mode: RealKvHashMode,
) -> None:
if not _is_sweep_tag(self.kernel_kind):
raise NotImplementedError(
@@ -96,6 +104,7 @@ class CanaryEndpoint:
launch_canary_verify_kernel(
context=self._make_verify_or_write_context(
violation_log=violation_log,
real_kv_hash_mode=real_kv_hash_mode,
),
plan=verify_plan,
check_verify_expected_token=False,
@@ -105,6 +114,7 @@ class CanaryEndpoint:
self,
*,
violation_log: ViolationLog,
real_kv_hash_mode: RealKvHashMode,
) -> VerifyOrWriteContext:
return VerifyOrWriteContext(
canary_buf=self.canary_buf,
@@ -113,6 +123,8 @@ class CanaryEndpoint:
violation_write_index=violation_log.violation_write_index,
slot_run_counter=self.slot_run_counter_view,
kernel_run_counter=self.kernel_run_counter_view,
real_kv_sources=self.real_kv_sources,
real_kv_hash_mode=real_kv_hash_mode,
enable_chain_position_assert=self.enable_chain_position_assert,
)
@@ -141,6 +153,16 @@ def _resolve_canary_buf(
return group.v_tail
def _resolve_real_kv_sources(
*,
half: str,
group: CanaryBufferGroup,
) -> tuple[RealKvSource, ...]:
if half == "K":
return group.real_kv_sources_k
return group.real_kv_sources_v
_FULL_LAYOUT: tuple[tuple[CanaryLaunchTag, str, str], ...] = (
(CanaryLaunchTag.HEAD_K_FULL, "HEAD", "K"),
(CanaryLaunchTag.HEAD_V_FULL, "HEAD", "V"),
@@ -177,6 +199,9 @@ def build_endpoints_from_group(
buf_slot = "TAIL" if slot == "SWEEP" else slot
canary_buf = _resolve_canary_buf(slot=buf_slot, half=half, group=group)
real_kv_sources = (
() if slot == "HEAD" else _resolve_real_kv_sources(half=half, group=group)
)
lut = group.swa_index_lut if pool_kind is PoolKind.SWA else None
slot_view = device_state.slot_run_counters[tag.value : tag.value + 1]
kernel_view = device_state.kernel_run_counters[tag.value : tag.value + 1]
@@ -185,6 +210,7 @@ def build_endpoints_from_group(
kernel_kind=tag,
canary_buf=canary_buf,
full_to_swa_index_mapping=lut,
real_kv_sources=real_kv_sources,
slot_run_counter_view=slot_view,
kernel_run_counter_view=kernel_view,
enable_chain_position_assert=device_state.enable_chain_position_assert,
+96 -2
View File
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Optional
import torch
from sglang.jit_kernel.kv_canary.verify import RealKvSource
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup, PoolKind
from sglang.srt.kv_canary.perturb.config import PerturbConfig, TargetGroupKind
@@ -90,18 +91,111 @@ def pick_target_group(
buffer_groups: tuple[CanaryBufferGroup, ...],
target_kind: TargetGroupKind,
) -> Optional[CanaryBufferGroup]:
"""Filter buffer_groups by target_kind.
"""Filter buffer_groups by target_kind restricted to groups with non-empty real_kv_sources_k.
Returns None if no group matches.
"""
eligible = [group for group in buffer_groups if group.real_kv_sources_k]
if not eligible:
return None
if target_kind == TargetGroupKind.FULL:
want = PoolKind.FULL
elif target_kind == TargetGroupKind.SWA:
want = PoolKind.SWA
else:
raise ValueError(f"Unsupported target_group_kind: {target_kind!r}")
filtered = [group for group in buffer_groups if group.kind == want]
filtered = [group for group in eligible if group.kind == want]
if not filtered:
return None
pick = random.randrange(len(filtered))
return filtered[pick]
def flip_random_source_byte_and_log(
*,
perturb_name: str,
group: CanaryBufferGroup,
slot_idx: int,
) -> None:
"""Pick a random K-half real_kv source on group, flip byte 0 of slot_idx's tile
in it, and log the result. Logs and returns silently when the group has no
real_kv_sources_k or the slot cannot be mapped into the chosen source."""
if not group.real_kv_sources_k:
logger.info(
"kv_canary perturb %s: skipped because group=%s has no real_kv_sources_k",
perturb_name,
group.kind.name,
)
return
source_pick = random.randrange(len(group.real_kv_sources_k))
source = group.real_kv_sources_k[source_pick]
flip_result = flip_first_byte_in_source(
group=group, source=source, slot_idx=slot_idx
)
if flip_result is None:
logger.info(
"kv_canary perturb %s: skipped because slot=%d could not be mapped "
"into group=%s source_idx=%d",
perturb_name,
slot_idx,
group.kind.name,
source_pick,
)
return
row, col, original_byte = flip_result
logger.info(
"kv_canary perturb %s: group=%s source_idx=%d slot=%d row=%d col=%d "
"original_byte=0x%02X new_byte=0x%02X",
perturb_name,
group.kind.name,
source_pick,
slot_idx,
row,
col,
original_byte,
original_byte ^ 0xFF,
)
def flip_first_byte_in_source(
*,
group: CanaryBufferGroup,
source: RealKvSource,
slot_idx: int,
slot_is_physical: bool = False,
) -> Optional[tuple[int, int, int]]:
"""XOR 0xFF on byte 0 of slot_idx's tile in source.tensor (column
`(physical_slot % page_size) * num_bytes_per_token`, not row offset 0).
For SWA groups, slot_idx is translated through group.swa_index_lut before computing
(row, col). Returns (row, col, original_byte) for logging, or None if the slot is
out-of-range / source is degenerate.
"""
if source.num_bytes_per_token <= 0 or source.read_bytes <= 0:
return None
physical_slot = slot_idx
if (
not slot_is_physical
and group.kind == PoolKind.SWA
and group.swa_index_lut is not None
):
lut = group.swa_index_lut
if slot_idx < 0 or slot_idx >= int(lut.shape[0]):
return None
physical_slot = int(lut[slot_idx].detach().to("cpu").item())
if physical_slot < 0:
return None
page_size = max(1, source.page_size)
row = physical_slot // page_size
col = (physical_slot % page_size) * source.num_bytes_per_token
if row < 0 or row >= int(source.tensor.shape[0]):
return None
if col < 0 or col >= int(source.tensor.shape[1]):
return None
flat = source.tensor
original_byte = int(flat[row, col].item())
flat[row, col] = original_byte ^ 0xFF
return row, col, original_byte
@@ -11,13 +11,19 @@ def attach_dsv4(
*,
pool: object,
device: torch.device,
read_bytes: int,
kv_token_id_vs_position_offset: int,
) -> tuple[CanaryBufferGroup, ...]:
"""Attach canary buffers to a DeepSeekV4TokenToKVPool.
TODO: only the swa_kv_pool sub-pool is wired; c4_kv_pool / c128_kv_pool /
c4_indexer_kv_pool / compress state pools are left uncovered.
TODO: even on swa_kv_pool, real-KV fingerprint is disabled (read_bytes is
ignored). DSV4 stores 584 B/token which is not 16-aligned (584 % 16 == 8),
so num_bytes_per_token cannot satisfy the 128-bit load alignment precondition.
"""
del read_bytes
sub_pool = pool.swa_kv_pool
num_slots = int(sub_pool.size)
@@ -30,6 +36,8 @@ def attach_dsv4(
k_tail=k_tail,
v_head=None,
v_tail=None,
real_kv_sources_k=(),
real_kv_sources_v=(),
swa_index_lut=pool.full_to_swa_index_mapping,
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
)
@@ -4,13 +4,17 @@ import torch
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup, PoolKind
from sglang.srt.kv_canary.pool_patcher.buf_info_splice import patch_buf_info_method
from sglang.srt.kv_canary.pool_patcher.buffer_alloc import alloc_canary_buf
from sglang.srt.kv_canary.pool_patcher.buffer_alloc import (
alloc_canary_buf,
make_row_source,
)
def attach_mha(
*,
pool: object,
device: torch.device,
read_bytes: int,
kv_token_id_vs_position_offset: int,
) -> tuple[CanaryBufferGroup, ...]:
num_slots = int(pool.k_buffer[0].shape[0])
@@ -25,6 +29,12 @@ def attach_mha(
k_tail=k_tail,
v_head=v_head,
v_tail=v_tail,
real_kv_sources_k=make_row_source(
layer_buffer=pool.k_buffer[0], read_bytes=read_bytes
),
real_kv_sources_v=make_row_source(
layer_buffer=pool.v_buffer[0], read_bytes=read_bytes
),
swa_index_lut=None,
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
)
@@ -6,19 +6,24 @@ import torch
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup, PoolKind
from sglang.srt.kv_canary.pool_patcher.buf_info_splice import patch_buf_info_method
from sglang.srt.kv_canary.pool_patcher.buffer_alloc import alloc_canary_buf
from sglang.srt.kv_canary.pool_patcher.buffer_alloc import (
alloc_canary_buf,
make_row_source,
)
def attach_swa(
*,
pool: object,
device: torch.device,
read_bytes: int,
kv_token_id_vs_position_offset: int,
) -> tuple[CanaryBufferGroup, ...]:
full_group = _build_subpool_group(
sub_pool=pool.full_kv_pool,
kind=PoolKind.FULL,
device=device,
read_bytes=read_bytes,
swa_lut=None,
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
)
@@ -26,6 +31,7 @@ def attach_swa(
sub_pool=pool.swa_kv_pool,
kind=PoolKind.SWA,
device=device,
read_bytes=read_bytes,
swa_lut=pool.full_to_swa_index_mapping,
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
)
@@ -52,6 +58,7 @@ def _build_subpool_group(
sub_pool: object,
kind: PoolKind,
device: torch.device,
read_bytes: int,
swa_lut: Optional[torch.Tensor],
kv_token_id_vs_position_offset: int,
) -> CanaryBufferGroup:
@@ -66,6 +73,12 @@ def _build_subpool_group(
k_tail=k_tail,
v_head=v_head,
v_tail=v_tail,
real_kv_sources_k=make_row_source(
layer_buffer=sub_pool.k_buffer[0], read_bytes=read_bytes
),
real_kv_sources_v=make_row_source(
layer_buffer=sub_pool.v_buffer[0], read_bytes=read_bytes
),
swa_index_lut=swa_lut,
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
)
@@ -10,6 +10,7 @@ from sglang.srt.kv_canary.config import CanaryConfig
from sglang.srt.kv_canary.pool_patcher.adapters.dsv4 import attach_dsv4
from sglang.srt.kv_canary.pool_patcher.adapters.mha import attach_mha
from sglang.srt.kv_canary.pool_patcher.adapters.swa import attach_swa
from sglang.srt.kv_canary.pool_patcher.buffer_alloc import resolve_real_kv_read_bytes
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
from sglang.srt.mem_cache.memory_pool import (
KVCache,
@@ -53,16 +54,19 @@ def attach_canary_buffers(
f"supported: {sorted(cls.__name__ for cls in _POOL_ATTACHERS)}"
)
read_bytes = resolve_real_kv_read_bytes(config)
groups = attacher(
pool=pool,
device=device,
read_bytes=read_bytes,
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
)
logger.info(
"attach_canary_buffers: pool=%s attacher=%s n_groups=%d kinds=%s "
"attach_canary_buffers: pool=%s attacher=%s read_bytes=%d n_groups=%d kinds=%s "
"kv_token_id_vs_position_offset=%d",
type(pool).__name__,
attacher.__name__,
read_bytes,
len(groups),
[g.kind.name for g in groups],
kv_token_id_vs_position_offset,
@@ -1,8 +1,27 @@
from __future__ import annotations
import sys
from typing import Tuple
import torch
from sglang.jit_kernel.kv_canary.verify import CANARY_SLOT_BYTES
from sglang.jit_kernel.kv_canary.consts import RealKvHashMode
from sglang.jit_kernel.kv_canary.verify import (
CANARY_SLOT_BYTES,
RealKvSource,
)
from sglang.srt.kv_canary.config import CanaryConfig
_PARTIAL_REAL_KV_READ_BYTES = 16
_REAL_KV_READ_ALIGN = 16
def resolve_real_kv_read_bytes(config: CanaryConfig) -> int:
if config.real_kv_hash_mode is RealKvHashMode.NONE:
return 0
if config.real_kv_hash_mode is RealKvHashMode.ALL:
return sys.maxsize
return _PARTIAL_REAL_KV_READ_BYTES
def alloc_canary_buf(
@@ -11,3 +30,86 @@ def alloc_canary_buf(
device: torch.device,
) -> torch.Tensor:
return torch.zeros(num_slots, CANARY_SLOT_BYTES, dtype=torch.uint8, device=device)
def _clip_read_bytes_aligned(*, requested: int, num_bytes_per_token: int) -> int:
"""Validate and clip read_bytes for the CUDA fold kernel's 128-bit aligned loads.
Normalizes sentinels (``sys.maxsize`` -> ``num_bytes_per_token``, ``0`` -> ``0``) and
rejects negative / unaligned / oversized requests.
"""
if num_bytes_per_token <= 0 or num_bytes_per_token % _REAL_KV_READ_ALIGN != 0:
raise ValueError(
"kv-canary: num_bytes_per_token must be a positive multiple of "
f"{_REAL_KV_READ_ALIGN}, got {num_bytes_per_token}"
)
if requested == 0:
return 0
if requested == sys.maxsize:
return num_bytes_per_token
if requested < 0:
raise ValueError(f"kv-canary: read_bytes must be non-negative, got {requested}")
if requested > num_bytes_per_token:
raise ValueError(
"kv-canary: read_bytes must be <= num_bytes_per_token "
f"({num_bytes_per_token}), got {requested}"
)
if requested % _REAL_KV_READ_ALIGN != 0:
raise ValueError(
"kv-canary: read_bytes must be a multiple of "
f"{_REAL_KV_READ_ALIGN}, got {requested}"
)
return requested
def make_row_source(
*,
layer_buffer: torch.Tensor,
read_bytes: int,
) -> Tuple[RealKvSource, ...]:
contiguous = layer_buffer.contiguous()
num_slots = int(contiguous.shape[0])
if num_slots == 0 or read_bytes == 0:
return ()
flat = contiguous.view(torch.uint8).reshape(num_slots, -1)
num_bytes_per_token = int(flat.shape[1])
clipped = _clip_read_bytes_aligned(
requested=read_bytes, num_bytes_per_token=num_bytes_per_token
)
if clipped == 0:
return ()
return (
RealKvSource(
tensor=flat,
page_size=1,
num_bytes_per_token=num_bytes_per_token,
read_bytes=clipped,
),
)
def make_packed_source(
*,
page_buffer: torch.Tensor,
page_size: int,
bytes_per_token: int,
read_bytes: int,
) -> Tuple[RealKvSource, ...]:
if read_bytes == 0 or page_buffer.numel() == 0:
return ()
flat = page_buffer.contiguous().view(torch.uint8)
if flat.ndim == 1:
flat = flat.reshape(1, -1)
clipped = _clip_read_bytes_aligned(
requested=read_bytes, num_bytes_per_token=bytes_per_token
)
if clipped == 0:
return ()
return (
RealKvSource(
tensor=flat,
page_size=page_size,
num_bytes_per_token=bytes_per_token,
read_bytes=clipped,
),
)
@@ -4,6 +4,9 @@ from typing import TYPE_CHECKING, Callable, Optional
import torch
from sglang.jit_kernel.kv_canary.consts import (
RealKvHashMode,
)
from sglang.jit_kernel.kv_canary.plan import launch_canary_plan_kernels
from sglang.jit_kernel.kv_canary.verify import (
CanaryLaunchTag,
@@ -64,6 +67,7 @@ def launch_endpoints_per_forward(
forward_batch: "ForwardBatch",
expected_inputs: ExpectedInputs,
violation_log: ViolationLog,
real_kv_hash_mode: RealKvHashMode,
enable_write_input_assert: bool,
enable_verify_token_assert: bool,
) -> None:
@@ -104,6 +108,7 @@ def launch_endpoints_per_forward(
enable_verify_token_assert=enable_verify_token_assert,
expected_inputs=expected_inputs,
violation_log=violation_log,
real_kv_hash_mode=real_kv_hash_mode,
)
@@ -113,6 +118,7 @@ def launch_endpoints_sweep(
group: CanaryBufferGroup,
verify_plan: VerifyPlan,
violation_log: ViolationLog,
real_kv_hash_mode: RealKvHashMode,
) -> None:
active_endpoints = [
endpoint
@@ -127,6 +133,7 @@ def launch_endpoints_sweep(
endpoint.launch_sweep(
verify_plan=verify_plan,
violation_log=violation_log,
real_kv_hash_mode=real_kv_hash_mode,
)
@@ -73,6 +73,7 @@ class SweepOrchestrator:
group=group,
verify_plan=verify_plan,
violation_log=violation_log,
real_kv_hash_mode=self._config.real_kv_hash_mode,
)
self._sweep_passes += 1
@@ -201,6 +201,7 @@ class SingleForwardManager:
forward_batch=forward_batch,
expected_inputs=expected_inputs_slice,
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,
)
@@ -238,6 +239,7 @@ class SingleForwardManager:
forward_batch=forward_batch,
expected_inputs=expected_inputs_slice,
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,
)
+14
View File
@@ -29,6 +29,7 @@ import uuid
from functools import cached_property
from typing import Any, Callable, Dict, List, Literal, Optional, Union
from sglang.jit_kernel.kv_canary.consts import RealKvHashMode
from sglang.srt.arg_groups.argparse_actions import (
DeprecatedAction,
DeprecatedAliasStoreAction,
@@ -780,6 +781,7 @@ class ServerArgs:
disable_attn_tp_gather: bool = False
gc_threshold: Optional[List[int]] = None
kv_canary: str = "none"
kv_canary_real_data: str = "none"
kv_canary_sweep_interval: int = 0
# Context parallelism used in the long sequence prefill phase of DeepSeek v3.2
enable_dsa_prefill_context_parallel: bool = False
@@ -6321,6 +6323,18 @@ class ServerArgs:
"'raise' fails the server on the first detected mismatch (CI lane)."
),
)
parser.add_argument(
"--kv-canary-real-data",
type=str,
default=ServerArgs.kv_canary_real_data,
choices=[m.name.lower() for m in RealKvHashMode],
help=(
"Check the real KV-cache in the canary. "
"'none' (default) disables the feature. "
"'partial' checks the first 16 bytes of each real-KV slot. "
"'all' checks the full real-KV slot."
),
)
parser.add_argument(
"--kv-canary-sweep-interval",
type=int,
+18 -1
View File
@@ -6,7 +6,8 @@ from typing import List, Optional
import torch
from sglang.jit_kernel.kv_canary.verify import CANARY_SLOT_BYTES
from sglang.jit_kernel.kv_canary import consts
from sglang.jit_kernel.kv_canary.verify import CANARY_SLOT_BYTES, RealKvSource
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup, PoolKind
from sglang.srt.kv_canary.config import CanaryConfig, CanaryMode
from sglang.srt.kv_canary.pool_patcher.adapters.mha import attach_mha
@@ -136,6 +137,7 @@ def make_base_config() -> CanaryConfig:
mode=CanaryMode.RAISE,
ring_capacity=1024,
sweep_interval=0,
real_kv_hash_mode=consts.RealKvHashMode.NONE,
enable_write_input_assert=False,
)
@@ -223,6 +225,8 @@ def make_buffer_group(
device: torch.device = DEFAULT_DEVICE,
kind: PoolKind = PoolKind.FULL,
has_v: bool = True,
has_real_kv: bool = False,
real_kv_source: Optional[RealKvSource] = None,
swa_index_lut: Optional[torch.Tensor] = None,
num_slots: int = 4,
kv_token_id_vs_position_offset: int = 0,
@@ -232,12 +236,25 @@ def make_buffer_group(
num_slots, CANARY_SLOT_BYTES, dtype=torch.uint8, device=device
)
if has_real_kv:
source = real_kv_source or RealKvSource(
tensor=torch.zeros(num_slots, 16, dtype=torch.uint8, device=device),
page_size=1,
num_bytes_per_token=16,
read_bytes=16,
)
real_kv_sources = (source,)
else:
real_kv_sources = ()
return CanaryBufferGroup(
kind=kind,
k_head=_zero(),
k_tail=_zero(),
v_head=_zero() if has_v else None,
v_tail=_zero() if has_v else None,
real_kv_sources_k=real_kv_sources,
real_kv_sources_v=real_kv_sources if has_v else (),
swa_index_lut=swa_index_lut,
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
)
@@ -5,6 +5,7 @@ from unittest.mock import patch
import torch
from sglang.jit_kernel.kv_canary.consts import RealKvHashMode
from sglang.jit_kernel.kv_canary.verify import CanaryLaunchTag
from sglang.srt.kv_canary import endpoint as endpoint_module
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup
@@ -26,12 +27,14 @@ def make_config(
mode: CanaryMode = CanaryMode.RAISE,
ring_capacity: int = 1024,
sweep_interval: int = 0,
real_kv_hash_mode: RealKvHashMode = RealKvHashMode.NONE,
enable_write_input_assert: bool = False,
) -> CanaryConfig:
return CanaryConfig(
mode=mode,
ring_capacity=ring_capacity,
sweep_interval=sweep_interval,
real_kv_hash_mode=real_kv_hash_mode,
enable_write_input_assert=enable_write_input_assert,
)