File size: 13,751 Bytes
6e668dc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | """Long-range NEEDLE retrieval for code: BF16 vs INT4-KIVI on Laguna-XS.2.
The meaningful long-context test for a KV-cache quantizer: can the model attend
back to a UNIQUE definition placed FAR earlier in the context after the cache
has been quantized to INT4? We build a long synthetic Python "codebase" of
filler functions padding to a target token length, embed a single
non-guessable needle at a controlled DEPTH near the start, and at the very end
ask the model to recall it. Grading is by EXACT value / executed equality --
the answer is only right if the model actually attended to the early definition
(it cannot be guessed).
Two needle kinds (both exact-graded):
* "const" : ``MAGIC_SEED_<id> = <7-digit prime-ish int>`` -> ask the value.
* "transform" : ``def secret_transform_<id>(x): return x * A + B`` -> ask the
model to compute ``secret_transform_<id>(N)``; correct answer
is the exact integer A*N+B.
For each (context_length, depth) cell we run several trials (distinct needle
values / filler) and report retrieval accuracy for BF16 and INT4-KIVI plus
their agreement. Batch-1 greedy only (the regime Int4KiviCache supports).
Usage
-----
.venv/bin/python scripts/needle_code_bench.py --lengths 8000,16000 \
--depths 0.05 --trials 3 --max-new 24
"""
from __future__ import annotations
import argparse
import random
import re
import sys
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, DynamicCache
ROOT = "/home/alex/poolside-hackathon-kv-quant"
sys.path.insert(0, ROOT)
from int4_kivi.hf_cache import Int4KiviCache
MODEL = "poolside/Laguna-XS.2"
# --------------------------------------------------------------------------- #
# Synthetic codebase / filler
# --------------------------------------------------------------------------- #
_VERBS = ["compute", "process", "build", "merge", "filter", "scan", "encode",
"decode", "reduce", "expand", "rotate", "shuffle", "balance", "score",
"align", "cluster", "sample", "weight", "smooth", "sharpen"]
_NOUNS = ["matrix", "vector", "buffer", "token", "graph", "tensor", "window",
"stream", "record", "payload", "segment", "lattice", "kernel",
"gradient", "index", "shard", "bucket", "frame", "channel", "node"]
def _filler_fn(rng: random.Random, i: int) -> str:
"""A plausible, in-distribution Python helper that carries no needle info."""
v = rng.choice(_VERBS)
n = rng.choice(_NOUNS)
a = rng.randint(2, 9)
b = rng.randint(1, 20)
return (
f"def {v}_{n}_{i}(items):\n"
f' """{v.capitalize()} the {n} {i} with a small affine pass."""\n'
f" out = []\n"
f" for k, it in enumerate(items):\n"
f" out.append(it * {a} + {b} + k)\n"
f" return out\n"
)
def _make_needle(rng: random.Random, kind: str):
"""Return (needle_source, question_text, expected_answer_str, meta)."""
nid = rng.randint(100000, 999999)
if kind == "const":
# 8-digit non-guessable value.
val = rng.randint(10_000_000, 99_999_999)
src = (
f"# Important runtime configuration constant.\n"
f"MAGIC_SEED_{nid} = {val}\n"
)
q = (
f"In the reference code above there is a constant named "
f"MAGIC_SEED_{nid}. Reply with ONLY its exact integer value and "
f"nothing else."
)
return src, q, str(val), {"nid": nid, "val": val}
elif kind == "transform":
# Use a unique multiplier constant so recall is non-guessable. We ask
# the model to recall the *added* offset literal, which is a pure
# attend-back retrieval (no arithmetic), keeping this a clean needle
# test rather than a reasoning test.
a = rng.choice([3, 7, 11, 13])
b = rng.randint(1_000_000, 9_999_999) # 7-digit, non-guessable
src = (
f"# Domain-specific transform used by the pipeline.\n"
f"def secret_transform_{nid}(x):\n"
f" return x * {a} + {b}\n"
)
q = (
f"In the reference code above there is a function named "
f"secret_transform_{nid} whose body is `return x * {a} + OFFSET`. "
f"Reply with ONLY the exact integer OFFSET value and nothing else."
)
return src, q, str(b), {"nid": nid, "a": a, "b": b}
else:
raise ValueError(kind)
# --------------------------------------------------------------------------- #
# Build a context of ~target_tokens with the needle at a given depth fraction
# --------------------------------------------------------------------------- #
def build_context(tok, target_tokens: int, depth: float, kind: str,
rng: random.Random):
"""Return (reference_code_text, question, expected_str, meta, n_pre_tokens).
``depth`` is the fraction of the *filler* placed BEFORE the needle (0.0 =
needle right at the start, 0.5 = needle in the middle). We grow filler
functions until the whole reference block reaches target_tokens.
"""
needle_src, question, expected, meta = _make_needle(rng, kind)
# Generate a large pool of filler, then split around the needle by depth.
fillers: list[str] = []
i = 0
# Build until the joined text (incl. needle) is at/above target.
# Estimate ~ a few tokens per char; just append and recount periodically.
pre, post = [], []
# First, mass-produce filler text and measure.
block_parts: list[str] = []
while True:
fn = _filler_fn(rng, i)
block_parts.append(fn)
i += 1
if i % 40 == 0:
joined = "\n".join(block_parts)
ntok = tok(joined, return_tensors="pt").input_ids.shape[1]
if ntok >= target_tokens:
break
if i > 200000: # safety
break
# Now place needle at the requested depth among the filler functions.
n_fn = len(block_parts)
split = int(n_fn * depth)
parts = block_parts[:split] + [needle_src] + block_parts[split:]
reference = "\n".join(parts)
# Trim to roughly target_tokens (keep the needle!). We trim from the END so
# the needle (near the front for small depth) is preserved.
ids = tok(reference, return_tensors="pt").input_ids[0]
if ids.shape[0] > target_tokens * 1.15:
ids = ids[: int(target_tokens * 1.15)]
reference = tok.decode(ids, skip_special_tokens=True)
# Re-confirm needle survived; if trimmed off (shouldn't for depth<0.7),
# re-insert near the front.
if meta_token(meta, kind) not in reference:
reference = needle_src + "\n" + reference
n_pre = tok(reference, return_tensors="pt").input_ids.shape[1]
return reference, question, expected, meta, n_pre
def meta_token(meta, kind: str) -> str:
if kind == "const":
return f"MAGIC_SEED_{meta['nid']}"
return f"secret_transform_{meta['nid']}"
# --------------------------------------------------------------------------- #
# Prompt + generation
# --------------------------------------------------------------------------- #
def build_input_ids(tok, reference: str, question: str, device):
sys_msg = (
"You are a careful code assistant. You will be shown a long Python "
"reference codebase, then asked one question about a specific symbol "
"defined in it. Answer using ONLY the information in the reference."
)
user_msg = (
"Reference codebase:\n\n"
f"```python\n{reference}\n```\n\n"
f"{question}"
)
msgs = [
{"role": "system", "content": sys_msg},
{"role": "user", "content": user_msg},
]
return tok.apply_chat_template(
msgs, add_generation_prompt=True, return_tensors="pt", return_dict=False,
).to(device)
@torch.no_grad()
def generate(model, input_ids, max_new, mode, config):
cache = Int4KiviCache(config=config) if mode == "int4" else DynamicCache()
out = model.generate(
input_ids, max_new_tokens=max_new, past_key_values=cache,
use_cache=True, do_sample=False, num_beams=1,
)
return out[0, input_ids.shape[1]:].tolist(), cache
def grade(response: str, expected: str) -> bool:
"""Exact-value match: the expected integer must appear as a standalone
number in the response (and be the first number, to avoid echoing the
function's coefficients)."""
nums = re.findall(r"-?\d+", response.replace(",", ""))
if not nums:
return False
# Correct if the expected value is the first integer emitted, OR appears
# and no other distinct integer precedes it. We accept "first integer".
return nums[0] == expected
# --------------------------------------------------------------------------- #
# Main sweep
# --------------------------------------------------------------------------- #
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--lengths", type=str, default="8000,16000,32000")
ap.add_argument("--depths", type=str, default="0.05")
ap.add_argument("--trials", type=int, default=3)
ap.add_argument("--kinds", type=str, default="const,transform")
ap.add_argument("--max-new", type=int, default=24)
ap.add_argument("--seed", type=int, default=1234)
args = ap.parse_args()
lengths = [int(x) for x in args.lengths.split(",") if x]
depths = [float(x) for x in args.depths.split(",") if x]
kinds = [k for k in args.kinds.split(",") if k]
print(f"[load] {MODEL} ...", flush=True)
tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(
MODEL, dtype=torch.bfloat16, device_map="auto"
)
model.eval()
device = next(model.parameters()).device
print(f"\n{'':#<92}")
print(" NEEDLE-IN-CODEBASE RETRIEVAL (exact-value graded, greedy, batch-1)")
print(f"{'':#<92}")
header = (f" {'len':>7} {'depth':>6} {'kind':>10} {'ctx':>8} "
f"{'BF16':>10} {'INT4':>10} {'agree':>7} {'ratio':>6} {'t/gen':>6}")
print(header)
print(f"{'':-<92}")
rows = []
example = None # (length, kind, question, expected, bf16_resp, int4_resp)
for length in lengths:
for depth in depths:
for kind in kinds:
rng = random.Random(args.seed + length + int(depth * 1000)
+ hash(kind) % 1000)
bf_ok = iq_ok = agree = 0
ctxs, ratios, times = [], [], []
for t in range(args.trials):
reference, question, expected, meta, n_pre = build_context(
tok, length, depth, kind, rng
)
input_ids = build_input_ids(tok, reference, question, device)
ctx = int(input_ids.shape[1])
ctxs.append(ctx)
res = {}
for mode in ("bf16", "int4"):
t0 = time.time()
ids, cache = generate(model, input_ids, args.max_new,
mode, model.config)
dt = time.time() - t0
resp = tok.decode(ids, skip_special_tokens=True)
ok = grade(resp, expected)
res[mode] = (ok, resp, dt)
if mode == "int4":
try:
ratios.append(cache.compression_ratio_vs_bf16())
except Exception:
pass
times.append(dt)
bf_ok += res["bf16"][0]
iq_ok += res["int4"][0]
agree += (res["bf16"][0] == res["int4"][0])
if example is None and res["int4"][0] and res["bf16"][0]:
example = (ctx, kind, question, expected,
res["bf16"][1].strip()[:120],
res["int4"][1].strip()[:120])
n = args.trials
ctx_m = sum(ctxs) // len(ctxs)
ratio_m = (sum(ratios) / len(ratios)) if ratios else 0.0
t_m = (sum(times) / len(times)) if times else 0.0
print(f" {length:>7} {depth:>6.2f} {kind:>10} {ctx_m:>8} "
f"{bf_ok:>4}/{n:<5} {iq_ok:>4}/{n:<5} {agree:>3}/{n:<3} "
f"{ratio_m:>5.2f}x {t_m:>5.1f}s", flush=True)
rows.append((length, depth, kind, ctx_m, bf_ok, iq_ok, agree, n,
ratio_m))
print(f"{'':-<92}")
# Aggregate by length.
print("\n RETRIEVAL ACCURACY BY CONTEXT LENGTH (summed over depths/kinds):")
by_len: dict[int, list[int]] = {}
for (length, depth, kind, ctx, bf, iq, ag, n, ratio) in rows:
acc = by_len.setdefault(length, [0, 0, 0, 0])
acc[0] += bf; acc[1] += iq; acc[2] += ag; acc[3] += n
print(f" {'target_len':>11} {'BF16':>14} {'INT4-KIVI':>14} {'agreement':>12}")
for length in sorted(by_len):
bf, iq, ag, n = by_len[length]
print(f" {length:>11} {bf:>6}/{n:<4} ({100*bf/n:>3.0f}%) "
f"{iq:>6}/{n:<4} ({100*iq/n:>3.0f}%) {ag:>4}/{n:<4} "
f"({100*ag/n:>3.0f}%)")
if example:
ctx, kind, q, exp, bf_r, iq_r = example
print(f"\n{'':-<92}")
print(f" REPRESENTATIVE NEEDLE (ctx={ctx} tok, kind={kind})")
print(f" Q: {q}")
print(f" expected: {exp}")
print(f" BF16 ->: {bf_r!r}")
print(f" INT4 ->: {iq_r!r}")
print(f"{'':-<92}")
print("\n[done]", flush=True)
if __name__ == "__main__":
main()
|