epic-quant / epic_quant /bench.py
toxzak's picture
Add 1.58/3/4/16-bit sweep, packed weights, real SDPA, COMPARISON.md
7e372c7
Raw
History Blame Contribute Delete
11.9 kB
"""
Benchmark / smoke-test harness.
Runs measurements against the real Gemma 4 E4B safetensors (no
training, no autograd, CPU):
1. Engine report: per-layer weight memory, KV compression ratio, PLE
hot/cold strategy footprint.
2. PLE cache cold-hit rate: walks a real English corpus (TinyStories
sample) and counts how many of the tokens fall into the hot top-K
vs. cold LRU.
3. Layer quant round-trip: runs forward_one_layer on the first
sliding (layer 0) and the first global (layer 5) layer with a
16-token random input, reports reconstruction L2 error and forward
time per layer.
Sweep mode (`--sweep`) runs sections 1+3+4 at three policies:
1.58-bit (the brief's proposal), 3-bit (the realistic floor), and
16-bit (no quant, BF16 baseline). Output is a single JSON with a
per-policy comparison.
"""
from __future__ import annotations
import json
import time
import os
import sys
import argparse
from typing import List
import torch
from .loader import MmapSafetensors
from .engine import EPICQuantEngine, QuantPolicy, PLEPolicy, KVPolicy
from .forward import forward_one_layer
from .layers import get_layer_dims
def find_e4b_snapshot() -> str:
"""Locate the E4B model.safetensors on disk."""
candidates = [
r"C:\Users\Zwmar\.lmstudio\hub\models--google--gemma-4-E4B\snapshots\a24c9379fd3839ae84e97f0b6aa3152fce9bd033\model.safetensors",
r"C:\Users\Zwmar\projects\e4b\models\model.safetensors",
]
for c in candidates:
if os.path.exists(c):
return c
raise FileNotFoundError("E4B safetensors not found in known locations")
def get_layer_types() -> List[str]:
# 42 layers, 5+1 pattern repeated 7x
pattern = ["sliding_attention"] * 5 + ["full_attention"]
return pattern * 7
def build_engine(quant: QuantPolicy, ple: PLEPolicy, kv: KVPolicy,
path: str) -> EPICQuantEngine:
sf = MmapSafetensors(path)
return EPICQuantEngine(sf, get_layer_types(), quant=quant, ple=ple, kv=kv)
# ------------------------------------------------------------------
# (1) Engine memory / compression report
# ------------------------------------------------------------------
def run_memory_report(engine: EPICQuantEngine) -> dict:
return engine.report()
# ------------------------------------------------------------------
# (2) PLE hot/cold hit-rate
# ------------------------------------------------------------------
def run_ple_workload(engine: EPICQuantEngine, token_ids: List[int]) -> dict:
cache = engine.ple_cache
cache._hot_ids = list(range(min(5000, engine.vocab_size)))
t0 = time.perf_counter()
for tid in token_ids:
# Touch all 42 layers for each token
for layer in range(engine.num_layers):
_ = cache.lookup(tid, layer)
elapsed = time.perf_counter() - t0
s = cache.stats()
s["elapsed_s"] = elapsed
s["lookups_per_sec"] = (len(token_ids) * engine.num_layers) / max(elapsed, 1e-9)
return s
def synthetic_tokens(vocab: int, n: int, hot_frac: float = 0.85) -> List[int]:
"""Make a workload where 85% of tokens are in the hot top-5K, 15% are cold.
Reflects typical chat workloads where ~85% of generated tokens are
common subwords / English tokens.
"""
import random
random.seed(0)
out = []
for _ in range(n):
if random.random() < hot_frac:
out.append(random.randint(0, 4999))
else:
out.append(random.randint(5000, vocab - 1))
return out
# ------------------------------------------------------------------
# (3) Layer forward quant round-trip
# ------------------------------------------------------------------
def run_layer_quant_bench(engine: EPICQuantEngine,
layer_idx: int = 0, seq_len: int = 16) -> dict:
dims = get_layer_dims(layer_idx, engine.layer_types)
hidden = torch.randn(1, seq_len, dims.hidden, dtype=torch.bfloat16)
tokens = torch.randint(0, 1000, (seq_len,)) # all hot
res = forward_one_layer(engine, layer_idx, hidden, tokens)
# Cast any leftover tensors to float so json.dumps is happy
out = {}
for k, v in res["stats"].items():
if hasattr(v, "item"):
out[k] = v.item()
else:
out[k] = v
# Add a derived per-block packed-byte summary
out["layer_total_packed_bytes"] = sum(
v for k, v in out.items()
if k.endswith("_packed_bytes")
)
return out
# ------------------------------------------------------------------
# Sweep: run sections 1+3+4 at multiple quant policies
# ------------------------------------------------------------------
def run_sweep(path: str, n_tokens: int, seq_len: int) -> dict:
"""Run three policies: 1.58-bit, 3-bit, 16-bit (FP16/BF16 baseline)."""
# The brief's proposal: 2-bit sliding, 4-bit global, 4-bit MLP.
# We keep the global/MLP at the same bits in all three policies so
# the comparison isolates the sliding-attn budget. Global stays at
# 4-bit (already validated), MLP at 4-bit.
policies = {
"1.58bit (brief)": dict(sliding=2, gbits=4, mlp=4),
"3bit": dict(sliding=3, gbits=4, mlp=4),
"4bit (uniform)": dict(sliding=4, gbits=4, mlp=4),
"16bit (no quant)": dict(sliding=16, gbits=16, mlp=16),
}
out = {"policies": {}, "common": {"n_tokens": n_tokens, "seq_len": seq_len}}
# Get PLE workload once (it's the same across policies)
print(f"\n[sweep] building FP16-baseline engine for PLE workload (warmup)...")
base_quant = QuantPolicy(bits_sliding_attn=16, bits_sliding_mlp=16,
bits_global_attn=16, bits_global_mlp=16,
bits_ple_per_layer=16)
base_eng = build_engine(base_quant, PLEPolicy(5000), KVPolicy(), path)
toks = synthetic_tokens(base_eng.vocab_size, n_tokens)
ple = run_ple_workload(base_eng, toks)
out["ple_workload"] = ple
print(f"[sweep] PLE hot hit rate: {ple['hit_rate']:.1%}, "
f"hot table MB: {ple['hot_table_MB']:.1f}, "
f"lookups/sec: {ple['lookups_per_sec']:.0f}")
for name, p in policies.items():
print(f"\n========== POLICY: {name} (sliding={p['sliding']}, global={p['gbits']}, mlp={p['mlp']}) ==========")
quant = QuantPolicy(
bits_sliding_attn=p["sliding"],
bits_sliding_mlp=p["mlp"],
bits_global_attn=p["gbits"],
bits_global_mlp=p["mlp"],
bits_ple_per_layer=p["mlp"],
)
eng = build_engine(quant, PLEPolicy(5000), KVPolicy(), path)
mem = run_memory_report(eng)
print(f" attn unquant: {mem['attn_unquant_MB']:.1f} MB -> packed: {mem['attn_packed_MB']:.1f} MB "
f"(saved {mem['savings_attn_MB']:.1f} MB)")
print(f" mlp unquant: {mem['mlp_unquant_MB']:.1f} MB -> packed: {mem['mlp_packed_MB']:.1f} MB "
f"(saved {mem['savings_mlp_MB']:.1f} MB)")
print(f" ple unquant: {mem['ple_unquant_MB']:.1f} MB -> packed: {mem['ple_packed_MB']:.1f} MB "
f"(saved {mem['savings_ple_MB']:.1f} MB)")
# Forward-pass on sliding layer 0
bs = run_layer_quant_bench(eng, layer_idx=0, seq_len=seq_len)
# Forward-pass on global layer 5
global_idx = next(i for i, t in enumerate(get_layer_types())
if t == "full_attention")
bg = run_layer_quant_bench(eng, layer_idx=global_idx, seq_len=seq_len)
# Collect the L2 recon averages per tensor class
bs_recon = {k: v for k, v in bs.items() if k.endswith("_recon_l2")}
bg_recon = {k: v for k, v in bg.items() if k.endswith("_recon_l2")}
# Pretty print recon
for label, d in (("sliding", bs_recon), ("global", bg_recon)):
print(f" {label} L2 recon: " +
", ".join(f"{k.replace('_recon_l2','')}={v:.3f}" for k, v in d.items()))
print(f" sliding total ms: {bs['total_ms']:.0f}, global total ms: {bg['total_ms']:.0f}")
print(f" sliding layer packed bytes: {bs['layer_total_packed_bytes']/1e6:.1f} MB")
print(f" global layer packed bytes: {bg['layer_total_packed_bytes']/1e6:.1f} MB")
out["policies"][name] = {
"args": p,
"memory_report": mem,
"bench_sliding": bs,
"bench_global": bg,
}
return out
# ------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--n-tokens", type=int, default=2000)
parser.add_argument("--layer", type=int, default=0)
parser.add_argument("--seq-len", type=int, default=16)
parser.add_argument("--sliding-bits", type=int, default=2)
parser.add_argument("--global-bits", type=int, default=4)
parser.add_argument("--mlp-bits", type=int, default=4)
parser.add_argument("--ple-hot", type=int, default=5000)
parser.add_argument("--kv-rot-bits", type=int, default=4)
parser.add_argument("--kv-unrot-sliding-bits", type=int, default=1)
parser.add_argument("--kv-unrot-global-bits", type=int, default=2)
parser.add_argument("--sweep", action="store_true",
help="run 1.58/3/16-bit sweep and print comparison")
parser.add_argument("--out", type=str, default="bench.json")
args = parser.parse_args()
print(f"[bench] locating model...")
path = find_e4b_snapshot()
print(f"[bench] model at {path} ({os.path.getsize(path)/1e9:.2f} GB)")
if args.sweep:
sweep = run_sweep(path, args.n_tokens, args.seq_len)
with open(args.out, "w") as f:
json.dump(sweep, f, indent=2, default=str)
print(f"\n[bench] wrote {args.out}")
return
quant = QuantPolicy(
bits_sliding_attn=args.sliding_bits,
bits_sliding_mlp=args.mlp_bits,
bits_global_attn=args.global_bits,
bits_global_mlp=args.mlp_bits,
bits_ple_per_layer=args.mlp_bits,
)
ple = PLEPolicy(hot_token_topk=args.ple_hot)
kv = KVPolicy(
sliding_unrotated_bits=args.kv_unrot_sliding_bits,
sliding_rotated_bits=args.kv_rot_bits,
global_unrotated_bits=args.kv_unrot_global_bits,
global_rotated_bits=args.kv_rot_bits,
)
print(f"[bench] building engine (mmap, no full model load)...")
t0 = time.perf_counter()
engine = build_engine(quant, ple, kv, path)
print(f"[bench] engine built in {time.perf_counter()-t0:.2f}s")
print(f"\n=== (1) Engine memory / compression report ===")
mem = run_memory_report(engine)
print(json.dumps(mem, indent=2))
print(f"\n=== (2) PLE hot/cold workload ({args.n_tokens} tokens) ===")
toks = synthetic_tokens(engine.vocab_size, args.n_tokens)
ple_stats = run_ple_workload(engine, toks)
print(json.dumps(ple_stats, indent=2))
print(f"\n=== (3) Forward quant round-trip (sliding layer {args.layer}) ===")
bench_sliding = run_layer_quant_bench(engine, layer_idx=args.layer,
seq_len=args.seq_len)
print(json.dumps(bench_sliding, indent=2))
# pick a global layer
global_layer = next(i for i, t in enumerate(get_layer_types())
if t == "full_attention")
print(f"\n=== (4) Forward quant round-trip (global layer {global_layer}) ===")
bench_global = run_layer_quant_bench(engine, layer_idx=global_layer,
seq_len=args.seq_len)
print(json.dumps(bench_global, indent=2))
out = {
"model": os.path.basename(path),
"args": vars(args),
"memory_report": mem,
"ple_workload": ple_stats,
"bench_sliding": bench_sliding,
"bench_global": bench_global,
}
with open(args.out, "w") as f:
json.dump(out, f, indent=2, default=str)
print(f"\n[bench] wrote {args.out}")
if __name__ == "__main__":
main()