Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

575 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""End-to-end validation of QJL on a real Gemma model on the local 5080.
Honest about what QJL is: a *runtime KV-cache* quantizer for the **K
(keys) side** of attention. It does NOT shrink ``model.safetensors`` on
disk -- the weights are unchanged. Together with TurboQuant on the V
side, the KV cache shrinks ~10x at long context (1-bit K + 4-bit V).
This test measures:
1. Whether the vendored CUDA extension at ``scripts/quantization/qjl/``
builds. If ``nvcc`` is missing the build aborts and we record the
exact command the user must run; downstream measurements that need
the C++ extension are skipped with that note. The pure-PyTorch
reference path (upstream ``QJLSketch.qjl_qunatize``) still runs.
2. Baseline generation on ``google/gemma-4-E2B`` with bf16 ``DynamicCache``:
peak VRAM, tok/sec, decoded sample.
3. **Pure-PyTorch QJL simulation** on real K activations from a forward
pass: project the per-token (head_dim,) K vector through a JL
matrix Π ∈ R^{head_dim × s}, sign-quantize to 1 bit, and measure
the actual byte footprint vs the bf16 baseline. This isolates the
compression ratio that QJL achieves on the target distribution.
4. Analytic KV bytes/token across the whole model from
``qjl_apply.kv_bytes_per_token_analytic``.
5. Asserts:
- K-cache bytes per token drop by ≥ 7x at the canonical 1-bit
setting with projection_dim=256 and per-token bf16 norms (which
is what upstream's ``QJLKeyQuantizer.build_sketch`` actually
stores: ``key_states_norm = norm(key, dim=-1)`` shape (B,H,T)).
The paper's headline ~16x figure is the *inlier-only* ratio; the
norm-per-token overhead drags the realized ratio to
``head_dim * 2 / (projection_dim/8 + 2)`` -- e.g.
``128*2 / (256/8 + 2) = 7.53x`` for gemma-4-E2B at projection_dim=256.
At projection_dim=128 the same formula gives 14.2x; the smaller
projection trades quality for ratio. The ratio asserted here
(≥7x) is the honest end-to-end number for the canonical
projection_dim=256 setting.
- Baseline outputs are non-empty / non-degenerate.
Why we do NOT measure the runtime peak VRAM of QJL itself
---------------------------------------------------------
The upstream ``LlamaAttention_QJL`` wrapper (``models/llama3_qjl.py``)
imports ``qjl_kernel.cuda_qjl_quant`` at module load time. Without the
CUDA extension built, that import path can't be exercised end-to-end
on this box. We measure what we *can* measure (the analytic and
pure-PyTorch ratios on real activations) and emit the exact build
command the user needs. See ``scripts/quantization/README.md`` for the
full build prereqs (``sudo apt install nvidia-cuda-toolkit
python3.12-dev``) and the Blackwell ``TORCH_CUDA_ARCH_LIST`` workaround.
"""
from __future__ import annotations
import argparse
import gc
import json
import logging
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
import pytest
torch = pytest.importorskip("torch")
transformers = pytest.importorskip("transformers")
AutoModelForCausalLM = transformers.AutoModelForCausalLM
AutoTokenizer = transformers.AutoTokenizer
DynamicCache = pytest.importorskip("transformers.cache_utils").DynamicCache
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("test_qjl")
ROOT = Path(__file__).resolve().parents[2]
QJL_DIR = ROOT / "scripts" / "quantization" / "qjl"
VAL_JSONL = ROOT / "data" / "final" / "val.jsonl"
# ---------------------------------------------------------------------------
# Build attempt for the vendored CUDA extension. The build is best-effort:
# success unlocks the runtime QJL kernel; failure is recorded with the exact
# remediation command and we proceed with the analytic / pure-pytorch path.
# ---------------------------------------------------------------------------
def attempt_qjl_kernel_build() -> dict:
"""Try to compile ``scripts/quantization/qjl/csrc/*.cu``. Returns a dict
describing the outcome plus any actionable error.
"""
nvcc = shutil.which("nvcc")
python_h_paths = [
Path("/usr/include/python3.12/Python.h"),
Path("/usr/include/python3.11/Python.h"),
Path(sys.prefix) / "include" / f"python{sys.version_info.major}.{sys.version_info.minor}" / "Python.h",
]
python_h = next((p for p in python_h_paths if p.exists()), None)
info: dict = {
"nvcc_present": bool(nvcc),
"nvcc_path": nvcc,
"python_h_present": bool(python_h),
"python_h_path": str(python_h) if python_h else None,
}
if not nvcc or not python_h:
info["built"] = False
info["error"] = (
f"missing prerequisites (nvcc={'OK' if nvcc else 'MISSING'},"
f" python.h={'OK' if python_h else 'MISSING'})"
)
info["remediation"] = (
"sudo apt install nvidia-cuda-toolkit python3.12-dev # then:\n"
"cd scripts/quantization/qjl && "
"TORCH_CUDA_ARCH_LIST='12.0+PTX' "
"python setup.py build_ext --inplace"
)
return info
log.info("nvcc and python.h are present; attempting kernel build")
env = dict(os.environ)
env["TORCH_CUDA_ARCH_LIST"] = env.get("TORCH_CUDA_ARCH_LIST", "12.0+PTX")
proc = subprocess.run(
[sys.executable, "setup.py", "build_ext", "--inplace"],
cwd=str(QJL_DIR),
env=env,
capture_output=True,
text=True,
timeout=600,
)
info["build_returncode"] = proc.returncode
info["build_stdout_tail"] = "\n".join(proc.stdout.splitlines()[-20:])
info["build_stderr_tail"] = "\n".join(proc.stderr.splitlines()[-30:])
info["built"] = proc.returncode == 0
if not info["built"]:
info["error"] = (
f"build_ext failed (rc={proc.returncode}); see build_stderr_tail"
)
info["remediation"] = (
"If the failure is sm_120-related, rebuild with"
" TORCH_CUDA_ARCH_LIST='12.0+PTX'."
" Otherwise inspect the stderr tail above."
)
return info
# ---------------------------------------------------------------------------
# Pure-PyTorch QJL reference (no CUDA extension required).
# Mirrors upstream ``QJLSketch.qjl_qunatize`` (note the upstream typo) from
# qjl/qjl_kernel.py's parent ``models/llama3_utils_qjl.py``. We re-derive
# the inlier branch only because the outlier branch needs the runtime hook
# in attention forward to know which head_dim coords are outliers per row.
# ---------------------------------------------------------------------------
def qjl_pure_pytorch_quantize(
keys: torch.Tensor, *, projection_dim: int, seed: int = 42
) -> tuple[torch.Tensor, int, int]:
"""1-bit JL projection of K activations.
Args:
keys: (B, num_kv_heads, T, head_dim) bf16/fp16/fp32.
projection_dim: JL output dimension (the QJL paper calls this
``key_quantization_bits`` -- a misnomer; it's the projected
dimension count, not bits per coord). Must be a multiple of 8.
seed: PRNG seed for the JL matrix.
Returns:
(packed_signs, baseline_bytes, qjl_bytes)
"""
assert projection_dim % 8 == 0, "projection_dim must be byte-aligned"
B, H, T, D = keys.shape
g = torch.Generator(device=keys.device).manual_seed(seed)
# Canonical layout: (head_dim, proj_dim) row-major — matches the
# qjl-cpu / verify references and the recipe's _build_jl_projections.
proj = torch.randn(D, projection_dim, generator=g, device=keys.device, dtype=torch.float32)
# x @ proj -> (B, H, T, projection_dim) fp32. No transpose: Π is
# stored kernel-canonical, so this is the direct sketch math.
sk = (keys.float() @ proj)
bits = (sk > 0).to(torch.uint8) # (B, H, T, projection_dim), uint8 0/1
# Pack 8 bits per byte along the last dim.
bits = bits.view(B, H, T, projection_dim // 8, 8)
enc = (1 << torch.arange(8, device=keys.device, dtype=torch.uint8)).view(1, 1, 1, 1, 8)
packed = (bits * enc).sum(dim=-1).to(torch.uint8) # (B, H, T, projection_dim/8)
# Per-token, per-head L2 norm (bf16) -- matches upstream's key_states_norm.
# 2 bytes per (head, token) for the norm, plus the packed signs.
baseline_bytes = B * H * T * D * 2 # bf16 K cache
qjl_bytes = (
B * H * T * (projection_dim // 8) # packed JL signs
+ B * H * T * 2 # bf16 norm per (head, token)
)
return packed, baseline_bytes, qjl_bytes
# ---------------------------------------------------------------------------
# Generation harness (mirrors test_turboquant.py for apples-to-apples).
# ---------------------------------------------------------------------------
def load_payload_prompts(n: int) -> list[dict]:
out: list[dict] = []
if not VAL_JSONL.exists():
# Fall back to synthetic prompts if val.jsonl is unavailable.
return [
{
"currentMessage": {"content": f"Summarize prime numbers under 100. (run {i})"},
"memoryEntries": [],
}
for i in range(n)
]
with VAL_JSONL.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except json.JSONDecodeError:
continue
er = rec.get("expectedResponse") or ""
if er.lstrip().startswith("thought:"):
out.append(rec)
if len(out) >= n:
break
if len(out) < n:
for i in range(len(out), n):
out.append(
{
"currentMessage": {"content": f"Describe the cause of tides briefly. (#{i})"},
"memoryEntries": [],
}
)
return out[:n]
def render_chat(tokenizer, record: dict) -> str:
sys_prompt = (
"You are an autonomous elizaOS agent. Decide which action to take "
"from `availableActions` and respond with ONE native JSON document. "
"Always native JSON. No fences, no <think>, no prose before or after."
)
msgs = [{"role": "system", "content": sys_prompt}]
for m in record.get("memoryEntries") or []:
role = m.get("role") or "user"
if role not in ("user", "assistant"):
continue
content = m.get("content") or ""
if content:
msgs.append({"role": role, "content": content})
cm = record.get("currentMessage") or {}
msgs.append({"role": "user", "content": cm.get("content") or ""})
return tokenizer.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
def measure_generation(
model,
tokenizer,
prompts: list[str],
*,
cache_factory,
max_new_tokens: int,
label: str,
) -> dict:
torch.cuda.empty_cache()
gc.collect()
torch.cuda.reset_peak_memory_stats()
decoded: list[str] = []
total_new = 0
t0 = time.perf_counter()
for p in prompts:
cache = cache_factory()
ids = tokenizer(p, return_tensors="pt").to(model.device)
with torch.no_grad():
out = model.generate(
**ids,
past_key_values=cache,
max_new_tokens=max_new_tokens,
do_sample=False,
pad_token_id=tokenizer.eos_token_id,
)
new = out[0, ids.input_ids.shape[-1]:]
total_new += int(new.shape[-1])
decoded.append(tokenizer.decode(new, skip_special_tokens=True))
del cache, out, ids
torch.cuda.synchronize()
elapsed = time.perf_counter() - t0
peak = torch.cuda.max_memory_allocated()
return {
"label": label,
"elapsed_s": elapsed,
"tokens_new": total_new,
"toks_per_s": total_new / elapsed if elapsed > 0 else 0.0,
"peak_vram_bytes": int(peak),
"decoded": decoded,
}
# ---------------------------------------------------------------------------
# K-projection capture: hook every full-attention layer's k_proj so we can
# feed real K activations into the pure-pytorch QJL ratio measurement.
# ---------------------------------------------------------------------------
def capture_real_k_activations(
model, tokenizer, prompt: str, *, max_layers: int = 4
) -> list[torch.Tensor]:
"""Run one forward pass and return up to ``max_layers`` of per-layer
K activations shaped (1, num_kv_heads, T, head_dim).
"""
text_cfg = (
model.config.get_text_config(decoder=True)
if hasattr(model.config, "get_text_config")
else model.config
)
head_dim = getattr(text_cfg, "head_dim", None) or (
text_cfg.hidden_size // text_cfg.num_attention_heads
)
num_kv_heads = (
getattr(text_cfg, "num_key_value_heads", None) or text_cfg.num_attention_heads
)
captured: list[torch.Tensor] = []
handles = []
def _hook(_m, _i, output):
if len(captured) >= max_layers:
return
t = output[0] if isinstance(output, tuple) else output
if t.dim() == 3:
B, T, _D = t.shape
t = t.view(B, T, num_kv_heads, head_dim).transpose(1, 2).contiguous()
captured.append(t.detach())
for i, layer in enumerate(model.model.layers):
if len(handles) >= max_layers:
break
attn = getattr(layer, "self_attn", None)
if attn is None:
continue
k_proj = getattr(attn, "k_proj", None)
if k_proj is None:
continue
handles.append(k_proj.register_forward_hook(_hook))
try:
ids = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
model(**ids, use_cache=False)
finally:
for h in handles:
h.remove()
return captured
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
ap.add_argument("--model", default="google/gemma-4-E2B")
ap.add_argument("--num-prompts", type=int, default=5)
ap.add_argument("--max-new-tokens", type=int, default=128)
ap.add_argument("--projection-dim-per-head", type=int, default=256)
ap.add_argument("--projection-seed", type=int, default=42)
ap.add_argument(
"--report",
default=str(ROOT / "scripts" / "quantization" / "qjl_report.json"),
)
args = ap.parse_args()
if not torch.cuda.is_available():
raise RuntimeError("CUDA required")
log.info("attempting to build vendored QJL CUDA extension")
build_info = attempt_qjl_kernel_build()
log.info(
"build status: built=%s nvcc=%s python_h=%s",
build_info["built"],
build_info["nvcc_present"],
build_info["python_h_present"],
)
if not build_info["built"]:
log.warning("QJL CUDA kernel could NOT be built. Reason: %s", build_info["error"])
log.warning(
"Remediation:\n%s",
build_info["remediation"],
)
log.info("loading %s", args.model)
tok = AutoTokenizer.from_pretrained(args.model, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
args.model,
torch_dtype=torch.bfloat16,
device_map="cuda",
trust_remote_code=True,
)
model.eval()
# Baseline generation (bf16 DynamicCache).
records = load_payload_prompts(args.num_prompts)
rendered = [render_chat(tok, r) for r in records]
log.info("running baseline bf16 generation on %d prompts", len(rendered))
base_res = measure_generation(
model, tok, rendered,
cache_factory=lambda: DynamicCache(),
max_new_tokens=args.max_new_tokens,
label="baseline_bf16",
)
# Pure-PyTorch QJL on captured K activations.
log.info("capturing real K activations for pure-pytorch QJL probe")
keys_per_layer = capture_real_k_activations(
model, tok, rendered[0], max_layers=4
)
layer_ratios = []
for i, K in enumerate(keys_per_layer):
_packed, base_b, qjl_b = qjl_pure_pytorch_quantize(
K,
projection_dim=args.projection_dim_per_head,
seed=args.projection_seed,
)
ratio = base_b / max(qjl_b, 1)
layer_ratios.append({"layer": i, "shape": list(K.shape), "baseline_bytes": base_b, "qjl_bytes": qjl_b, "ratio": ratio})
log.info(
" layer %d K shape=%s baseline=%d B qjl=%d B ratio=%.2fx",
i, tuple(K.shape), base_b, qjl_b, ratio,
)
# Sensitivity probe: same K activations, sweep projection_dim ∈ {128, 256, 512}
# so the report shows the full ratio vs quality tradeoff.
sweep = []
for pdim in (128, 256, 512):
if not keys_per_layer:
break
K = keys_per_layer[0]
_, base_b, qjl_b = qjl_pure_pytorch_quantize(
K, projection_dim=pdim, seed=args.projection_seed
)
sweep.append({"projection_dim": pdim, "baseline_bytes": base_b, "qjl_bytes": qjl_b, "ratio": base_b / max(qjl_b, 1)})
log.info(
" sweep projection_dim=%d baseline=%d B qjl=%d B ratio=%.2fx",
pdim, base_b, qjl_b, base_b / max(qjl_b, 1),
)
# Analytic KV bytes per token across the whole model.
sys.path.insert(0, str(ROOT / "scripts" / "quantization"))
from qjl_apply import kv_bytes_per_token_analytic # type: ignore # noqa: E402
base_bpt, quant_bpt = kv_bytes_per_token_analytic(
model.config,
key_quantization_bits=args.projection_dim_per_head,
key_quantization_bits_initial_layers=args.projection_dim_per_head * 2,
initial_layers_count=15,
outlier_count_general=8,
outlier_count_initial_layers=8,
value_bits=4, # paired TurboQuant V side
)
# Reporting
print()
print("=" * 78)
print("QJL validation report")
print("=" * 78)
print(f"model: {args.model}")
print(f"projection_dim_per_head: {args.projection_dim_per_head}")
print(f"projection_seed: {args.projection_seed}")
print()
print("CUDA extension build status:")
print(f" nvcc present: {build_info['nvcc_present']} ({build_info['nvcc_path'] or 'not found'})")
print(f" python.h present: {build_info['python_h_present']} ({build_info['python_h_path'] or 'not found'})")
print(f" built: {build_info['built']}")
if not build_info["built"]:
print(f" error: {build_info['error']}")
print(f" remediation: {build_info['remediation']}")
print()
print("Pure-PyTorch QJL on real K activations (K-side only):")
for r in layer_ratios:
print(
f" layer {r['layer']:>2} shape={tuple(r['shape'])} "
f"baseline={r['baseline_bytes'] / 1024:.1f} KiB "
f"qjl={r['qjl_bytes'] / 1024:.1f} KiB "
f"ratio={r['ratio']:.2f}x"
)
print()
print("projection_dim sweep (layer 0 K activations):")
for s in sweep:
print(
f" projection_dim={s['projection_dim']:>3} "
f"baseline={s['baseline_bytes'] / 1024:.1f} KiB "
f"qjl={s['qjl_bytes'] / 1024:.1f} KiB "
f"ratio={s['ratio']:.2f}x"
)
print()
print("Analytic KV bytes / token (whole model, full-attention layers):")
print(f" baseline (bf16 K + bf16 V): {base_bpt:>10,} bytes")
print(f" qjl K (1-bit) + turboquant V (4-bit): {quant_bpt:>10,} bytes")
print(f" reduction: {base_bpt / max(quant_bpt, 1):.2f}x ({100 * (1 - quant_bpt / max(base_bpt, 1)):.1f}% smaller)")
print()
print(f"Baseline generation ({args.num_prompts} prompts, {args.max_new_tokens} new tokens each):")
print(f" tok/s: {base_res['toks_per_s']:.2f} ({base_res['tokens_new']} tok in {base_res['elapsed_s']:.2f}s)")
print(f" peak VRAM: {base_res['peak_vram_bytes'] / 1e9:.3f} GB")
print()
print("Sample baseline outputs:")
for i, txt in enumerate(base_res["decoded"][:3]):
snippet = txt[:240].replace("\n", " ")
print(f" [{i + 1}] {snippet!r}")
print("=" * 78)
# Assertions
failures: list[str] = []
# K-only ratio at projection_dim=256 with per-token bf16 norm (the
# exact geometry upstream's QJLKeyQuantizer uses). Closed-form ratio
# is head_dim*2 / (projection_dim/8 + 2) = 256/34 = 7.53x for
# head_dim=128, projection_dim=256.
avg_ratio = sum(r["ratio"] for r in layer_ratios) / max(len(layer_ratios), 1)
expected_min = 7.0
if avg_ratio < expected_min:
failures.append(
f"K-side compression ratio insufficient: avg {avg_ratio:.2f}x "
f"(>={expected_min:.1f}x required at projection_dim=256, 1-bit, "
f"per-token norm; closed-form for head_dim=128 is 7.53x)"
)
# Baseline outputs must be non-degenerate.
for i, txt in enumerate(base_res["decoded"]):
s = (txt or "").strip()
if len(s) < 8:
failures.append(f"baseline prompt {i}: output too short: {s!r}")
elif len(set(s)) < 3:
failures.append(f"baseline prompt {i}: output looks degenerate: {s[:80]!r}")
report = {
"model": args.model,
"projection_dim_per_head": args.projection_dim_per_head,
"build_info": build_info,
"baseline": {k: v for k, v in base_res.items() if k != "decoded"},
"baseline_decoded": base_res["decoded"],
"pure_pytorch_qjl_per_layer": layer_ratios,
"pure_pytorch_qjl_avg_ratio": avg_ratio,
"projection_dim_sweep": sweep,
"analytic_kv_bytes_per_token_baseline": base_bpt,
"analytic_kv_bytes_per_token_qjl_plus_turboquant": quant_bpt,
"analytic_kv_reduction_factor": base_bpt / max(quant_bpt, 1),
"assertions_failed": failures,
}
Path(args.report).write_text(json.dumps(report, indent=2), encoding="utf-8")
log.info("wrote report to %s", args.report)
if failures:
print("\nFAILED ASSERTIONS:")
for f in failures:
print(f" - {f}")
return 1
print("\nAll assertions passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())