Skip to content

Commit b43dca1

Browse files
bench: gemma-4 native bounded-decode (small sliding window) concurrency ceiling
Probe established gemma-4 keeps recall 1.0 at sliding_window=68 with NO restoration (5 full-attn layers carry recall). This bench measures the resulting long-context concurrency ceiling (native HybridCache, shrunk window) to compare vs vLLM. Honest: this is native window tuning vLLM can match, not a Kakeya moat -- that needs a full-attention model. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent caae4cd commit b43dca1

1 file changed

Lines changed: 153 additions & 0 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""gemma-4 bounded-decode concurrency ceiling (native hybrid cache, small window).
2+
3+
ADR 0015 item #2 on gemma-4. KEY FINDING (probed separately): gemma-4 keeps
4+
recall 1.0 with the sliding window shrunk to ~68 *natively* — no Kakeya
5+
restoration needed — because its 5 full-attention layers (of 30) carry recall.
6+
So on gemma-4 "bounded decode" reduces to `sliding_window=W` on the native
7+
HybridCache; this bench measures the resulting concurrency ceiling at long
8+
context to compare against vLLM.
9+
10+
(Honest caveat: this is native-window tuning that vLLM can also apply, so it is
11+
NOT a Kakeya algorithmic advantage on gemma-4 — that requires a full-attention
12+
model where shrinking the window kills recall and only restoration recovers it.)
13+
14+
Sweeps N (concurrent sessions) at a fixed context, greedy, reporting per-N peak
15+
GPU memory, per-session recall, and aggregate decode tok/s; the ceiling is the
16+
largest N that fits.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import argparse
22+
import json
23+
import sys
24+
import time
25+
from collections import Counter
26+
from pathlib import Path
27+
from typing import Any, Dict, List
28+
29+
30+
def main() -> int:
31+
ap = argparse.ArgumentParser(description=__doc__)
32+
ap.add_argument("--verifier-id", default="google/gemma-4-26B-A4B-it")
33+
ap.add_argument("--sliding-window", type=int, default=68)
34+
ap.add_argument("--haystack-lines", type=int, default=3100)
35+
ap.add_argument("--batch-sizes", default="1,2,4,8,16,24,32")
36+
ap.add_argument("--gen-tokens", type=int, default=64)
37+
ap.add_argument("--pool", type=int, default=40)
38+
ap.add_argument("--seed", type=int, default=0)
39+
ap.add_argument("--output", default=None)
40+
args = ap.parse_args()
41+
42+
import torch
43+
from transformers import AutoModelForCausalLM, AutoTokenizer
44+
sys.path.insert(0, "."); sys.path.insert(0, "sdks/python")
45+
from inference_engine.v04.niah_eval import make_niah_dataset
46+
47+
device = torch.device("cuda")
48+
tok = AutoTokenizer.from_pretrained(args.verifier_id)
49+
print(f"[gb] loading {args.verifier_id} sdpa bf16", file=sys.stderr, flush=True)
50+
model = AutoModelForCausalLM.from_pretrained(
51+
args.verifier_id, dtype=torch.bfloat16, attn_implementation="sdpa",
52+
).to(device).eval()
53+
tc = model.config.get_text_config()
54+
native_sw = tc.sliding_window
55+
tc.sliding_window = args.sliding_window
56+
if hasattr(model.config, "sliding_window"):
57+
model.config.sliding_window = args.sliding_window
58+
print(f"[gb] sliding_window {native_sw} -> {args.sliding_window}", file=sys.stderr, flush=True)
59+
60+
def enc(t):
61+
ids = tok.apply_chat_template([{"role": "user", "content": t}],
62+
add_generation_prompt=True, tokenize=True,
63+
return_tensors="pt")
64+
return (ids["input_ids"] if hasattr(ids, "keys") else ids)[0].tolist()
65+
66+
pool = make_niah_dataset(n_samples=args.pool, haystack_min_lines=args.haystack_lines,
67+
haystack_max_lines=args.haystack_lines, seed=args.seed)
68+
encs = [(enc(s.prompt_text), s.answer_text) for s in pool]
69+
modal = Counter(len(e[0]) for e in encs).most_common(1)[0][0]
70+
bucket = [(i, a) for i, a in encs if len(i) == modal]
71+
print(f"[gb] modal prompt len={modal}, {len(bucket)} equal-length", file=sys.stderr, flush=True)
72+
batch_sizes = [int(x) for x in args.batch_sizes.split(",") if x.strip()]
73+
need = max(batch_sizes)
74+
while len(bucket) < need:
75+
bucket += bucket[: need - len(bucket)]
76+
77+
def recall(ids_out, ans):
78+
return ans in tok.decode(ids_out, skip_special_tokens=True)
79+
80+
@torch.no_grad()
81+
def run(N):
82+
sel = bucket[:N]
83+
ids = torch.tensor([s[0] for s in sel], device=device)
84+
ans = [s[1] for s in sel]
85+
torch.cuda.reset_peak_memory_stats(device)
86+
# prefill
87+
out = model(input_ids=ids, use_cache=True, logits_to_keep=1)
88+
cache = out.past_key_values
89+
nxt = out.logits[:, -1, :].argmax(-1)
90+
gen = [[int(nxt[i])] for i in range(N)]
91+
T = ids.size(1)
92+
torch.cuda.synchronize(device); t0 = time.perf_counter()
93+
for step in range(args.gen_tokens - 1):
94+
cur = nxt.view(N, 1)
95+
cpos = torch.tensor([T + step], device=device)
96+
out = model(input_ids=cur, past_key_values=cache, use_cache=True,
97+
cache_position=cpos, logits_to_keep=1)
98+
cache = out.past_key_values
99+
nxt = out.logits[:, -1, :].argmax(-1)
100+
for i in range(N):
101+
gen[i].append(int(nxt[i]))
102+
torch.cuda.synchronize(device); dt = time.perf_counter() - t0
103+
tps = (N * args.gen_tokens) / dt
104+
rec = sum(recall(gen[i], ans[i]) for i in range(N)) / N
105+
peak = torch.cuda.max_memory_allocated(device) / 1e9
106+
return tps, rec, peak
107+
108+
# warmup
109+
try:
110+
run(1)
111+
except Exception as e: # noqa: BLE001
112+
print(f"[gb] warmup note: {e}", file=sys.stderr)
113+
114+
rows: List[Dict[str, Any]] = []
115+
single_tps = None
116+
for N in batch_sizes:
117+
try:
118+
tps, rec, peak = run(N)
119+
except torch.OutOfMemoryError as e: # noqa: BLE001
120+
print(f"[gb] N={N}: OOM ({e})", file=sys.stderr, flush=True)
121+
rows.append({"agents": N, "oom": True})
122+
break
123+
if single_tps is None:
124+
single_tps = tps
125+
row = {"agents": N, "decode_aggregate_tps": round(tps, 2),
126+
"recall": round(rec, 3),
127+
"parallel_speedup_vs_n1": round(tps / single_tps, 2),
128+
"peak_gpu_gb": round(peak, 2)}
129+
rows.append(row)
130+
print(f"[gb] N={N:3d} | {row['decode_aggregate_tps']} tok/s "
131+
f"(x{row['parallel_speedup_vs_n1']}) | recall {row['recall']} | "
132+
f"peak {row['peak_gpu_gb']} GB", file=sys.stderr, flush=True)
133+
134+
report = {
135+
"kind": "gemma_bounded_decode",
136+
"config": {"verifier_id": args.verifier_id, "sliding_window": args.sliding_window,
137+
"native_sliding_window": native_sw, "modal_prompt_len": modal,
138+
"gen_tokens": args.gen_tokens, "batch_sizes": batch_sizes,
139+
"note": ("native gemma-4 hybrid cache with shrunk sliding window; "
140+
"no Kakeya restoration (recall comes from the 5 full-attn "
141+
"layers). vLLM can apply the same window — not a Kakeya moat.")},
142+
"env": {"gpu": torch.cuda.get_device_name(0), "torch": torch.__version__},
143+
"results": rows,
144+
}
145+
if args.output:
146+
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
147+
Path(args.output).write_text(json.dumps(report, indent=2))
148+
print(f"[gb] wrote {args.output}", file=sys.stderr)
149+
return 0
150+
151+
152+
if __name__ == "__main__":
153+
raise SystemExit(main())

0 commit comments

Comments
 (0)