kv-landlords / scripts /validate_store_equiv.py
a1exxd0's picture
Upload kv-quant (INT4/NVFP4 KIVI) work + vLLM fork source
6e668dc verified
Raw
History Blame Contribute Delete
4.12 kB
"""Golden-equivalence test for int4_kivi_store (guards the per-channel-K path).
validate_paged_decode CANNOT catch store bugs: it compares fused-decode vs
dense-gather, but BOTH read the same cache, so a wrong store agrees with itself.
This test instead pins the *store output*: it quantizes K/V with int4_kivi_store
and dequantizes the cache with int4_kivi_gather_dequant, then compares against a
saved golden (captured from the known-good store). Any change to the store that
alters the written bytes (e.g. the sync-free full-block detection) must reproduce
the golden bit-exactly.
cd /tmp && CUDA_HOME=/usr/local/cuda-12.8 .venv-vllm/bin/python \
.../scripts/validate_store_equiv.py # compare (or save if missing)
... validate_store_equiv.py --save # force (re)capture golden
"""
from __future__ import annotations
import sys
import torch
from vllm.v1.attention.ops.triton_int4_kivi import (
int4_kivi_gather_dequant,
int4_kivi_store,
)
DEV = "cuda"
HK, D = 8, 128
PAGE = 16
FULL_DIM = D // 2 + D // 16
GOLDEN = "/tmp/int4_store_golden.pt"
def store_prefill(L, seed):
"""Store one length-L sequence prefill-style (contiguous slots from a block
boundary -> full blocks per-channel, trailing partial block per-token)."""
g = torch.Generator(device=DEV).manual_seed(seed)
nb = (L + PAGE - 1) // PAGE
kv = torch.zeros((nb + 2, 2, PAGE, HK, FULL_DIM), dtype=torch.uint8, device=DEV)
bt = torch.zeros((1, nb), dtype=torch.int32, device=DEV)
phys = list(range(1, 1 + nb))
for j, p in enumerate(phys):
bt[0, j] = p
k = torch.randn(L, HK, D, generator=g, device=DEV, dtype=torch.bfloat16)
v = torch.randn(L, HK, D, generator=g, device=DEV, dtype=torch.bfloat16)
slots = torch.tensor(
[phys[t // PAGE] * PAGE + (t % PAGE) for t in range(L)],
dtype=torch.int64, device=DEV,
)
int4_kivi_store(k, v, kv, slots, D)
sl = torch.tensor([L], dtype=torch.int32, device=DEV)
kd, vd = int4_kivi_gather_dequant(kv, bt, sl, D, HK, L)
return kd.float().cpu(), vd.float().cpu()
def store_scattered(B, seed):
"""Decode-like store: B tokens at scattered partial-block slots (token 3 of
B distinct blocks) -> no full blocks, all per-token K (exercises the masks)."""
g = torch.Generator(device=DEV).manual_seed(seed)
kv = torch.zeros((B + 2, 2, PAGE, HK, FULL_DIM), dtype=torch.uint8, device=DEV)
k = torch.randn(B, HK, D, generator=g, device=DEV, dtype=torch.bfloat16)
v = torch.randn(B, HK, D, generator=g, device=DEV, dtype=torch.bfloat16)
slots = (torch.arange(B, dtype=torch.int64, device=DEV) + 1) * PAGE + 3
int4_kivi_store(k, v, kv, slots, D)
return kv.clone().cpu()
CASES = {
"prefill L=512": ("p", 512),
"prefill L=500": ("p", 500),
"prefill L=257": ("p", 257),
"prefill L=33": ("p", 33),
"prefill L=16": ("p", 16),
"prefill L=15": ("p", 15),
"scattered B=16": ("s", 16),
"scattered B=40": ("s", 40),
}
def run():
out = {}
for i, (name, (kind, n)) in enumerate(CASES.items()):
seed = 1234 + i # deterministic across processes (hash() is not)
if kind == "p":
kd, vd = store_prefill(n, seed)
out[name] = (kd, vd)
else:
out[name] = store_scattered(n, seed)
return out
if __name__ == "__main__":
import os
res = run()
if "--save" in sys.argv or not os.path.exists(GOLDEN):
torch.save(res, GOLDEN)
print(f"saved golden -> {GOLDEN} ({len(res)} cases)")
sys.exit(0)
gold = torch.load(GOLDEN)
ok = True
for name in CASES:
a, b = res[name], gold[name]
if isinstance(a, tuple):
dk = (a[0] - b[0]).abs().max().item()
dv = (a[1] - b[1]).abs().max().item()
d = max(dk, dv)
else:
d = (a.float() - b.float()).abs().max().item()
passed = d == 0.0
ok = ok and passed
print(f"[{'ok ' if passed else 'FAIL'}] {name:18s} max|Δ vs golden|={d:.3e}")
print("STORE BIT-IDENTICAL" if ok else "STORE CHANGED — REVIEW")