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

581 lines
20 KiB
Python

"""End-to-end validation of fused-TurboQuant on a real Gemma model on the local 5080.
Three measurements on identical prompts, identical lengths:
1. Baseline: bf16 model + ``DynamicCache`` (the upstream HF default).
2. Pure-PyTorch turbokv 0.1.0: bf16 model + ``TurboQuantCache`` from the
``turboquant`` import (the slow path).
3. Fused-turboquant 0.1.0 (vendored): bf16 model + ``CompressedKVCache``
produced by
``quantization.fused_turboquant_vendored.hf.patch_model``. This
rewrites every full-attention ``forward`` to route through Triton
kernels for encode / Q@K^T / decode and includes the gated-attention
patch for Gemma 4.
Per path we record peak VRAM (``torch.cuda.max_memory_allocated``), tokens/sec
(wall clock), and decode the first generation as a sanity sample. The
assertions at the end verify the *whole point* of the Triton kernel:
- fused-turboquant peak VRAM ≤ pure-PyTorch peak VRAM, and
- fused-turboquant tokens/sec ≥ 1.5x pure-PyTorch tokens/sec.
Default model is ``google/gemma-4-E2B``. It is a hybrid linear
attention + Gated Attention multimodal checkpoint, so compatibility with
the fused path is a release requirement rather than an optional bonus.
Usage::
.venv/bin/python scripts/quantization/test_fused_turboquant.py
"""
from __future__ import annotations
import argparse
import gc
import json
import logging
import sys
import time
import traceback
from pathlib import Path
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.cache_utils import DynamicCache
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("test_fused_turboquant")
ROOT = Path(__file__).resolve().parents[2]
VAL_JSONL = ROOT / "data" / "final" / "val.jsonl"
# Make the vendored fused_turboquant importable as
# ``quantization.fused_turboquant_vendored`` regardless of the caller's CWD.
sys.path.insert(0, str(ROOT / "scripts"))
def load_payload_message_handler_prompts(n: int) -> list[dict]:
"""Pull n records whose expected response looks like a native JSON message_handler doc."""
if not VAL_JSONL.exists():
# Fall back to a synthetic prompt if the dataset isn't checked in. The
# test still runs; only the realism of the prompt distribution suffers.
log.warning("%s not found, falling back to synthetic prompts", VAL_JSONL)
return [
{
"currentMessage": {
"content": (
"Summarize the following operational native JSON document in "
"native JSON format. Keep the field order exact."
)
},
"memoryEntries": [],
"expectedResponse": "thought: ...\ntext: ...",
}
for _ in range(n)
]
out: list[dict] = []
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:
raise RuntimeError(f"Only found {len(out)} native JSON prompts in {VAL_JSONL}")
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 pad_prompt_to_length(
tokenizer, base_prompt: str, target_tokens: int, filler: str
) -> str:
"""Tile `filler` after `base_prompt` until the tokenized length hits
`target_tokens` (then truncate exactly).
The padding text is appended *before* the assistant generation marker so we
never break the chat template's open-assistant turn. We re-render through
the tokenizer and slice on token IDs to land precisely.
"""
ids = tokenizer(base_prompt, return_tensors="pt").input_ids[0]
if ids.shape[-1] >= target_tokens:
return tokenizer.decode(ids[:target_tokens], skip_special_tokens=False)
pad_text = (filler + "\n") * 200
while ids.shape[-1] < target_tokens:
base_prompt = base_prompt + "\n" + pad_text
ids = tokenizer(base_prompt, return_tensors="pt").input_ids[0]
truncated = ids[:target_tokens]
return tokenizer.decode(truncated, skip_special_tokens=False)
def _free():
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
def measure_path(
*,
label: str,
model,
tokenizer,
prompts: list[str],
max_new_tokens: int,
cache_factory,
pre_generate=None,
post_generate=None,
) -> dict:
"""Generate over `prompts` and return wall-clock + memory + decoded samples.
`pre_generate` / `post_generate` run once per prompt (e.g., to patch /
unpatch the model around each call when the kernel needs that).
"""
_free()
if torch.cuda.is_available():
torch.cuda.reset_peak_memory_stats()
decoded: list[str] = []
total_new = 0
t0 = time.perf_counter()
for p in prompts:
ids = tokenizer(p, return_tensors="pt").to(model.device)
if pre_generate is not None:
cache = pre_generate()
else:
cache = cache_factory()
with torch.inference_mode():
out = model.generate(
**ids,
past_key_values=cache,
max_new_tokens=max_new_tokens,
do_sample=False,
use_cache=True,
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))
if post_generate is not None:
post_generate()
else:
del cache
del ids, out
if torch.cuda.is_available():
torch.cuda.synchronize()
elapsed = time.perf_counter() - t0
peak = (
int(torch.cuda.max_memory_allocated())
if torch.cuda.is_available()
else 0
)
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": peak,
"decoded_first": decoded[0] if decoded else "",
}
def _try_fused(
*,
model,
bits: int,
):
"""Import-and-patch wrapper that surfaces Triton/JIT failures cleanly.
Returns ``(cache, error_str)`` — if the kernel can't compile we return
``(None, "error message")`` so the caller can log the blocker and skip
the fused path without crashing the whole test.
"""
try:
from quantization.fused_turboquant_vendored.hf import patch_model
except Exception as exc:
return None, (
"import quantization.fused_turboquant_vendored.hf failed: "
f"{exc!r}"
)
try:
cache = patch_model(model, bits=bits, compress_v=True, verify=True)
return cache, None
except Exception as exc:
return None, "".join(
traceback.format_exception_only(type(exc), exc)
).strip() + "\n" + traceback.format_exc(limit=3)
def run_one_model(
*,
model_id: str,
num_prompts: int,
max_new_tokens: int,
prompt_tokens: int,
bits: int,
) -> dict:
log.info("=" * 78)
log.info("MODEL: %s", model_id)
log.info("=" * 78)
log.info("loading tokenizer + model in bf16 on cuda")
tok = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
device_map="cuda",
trust_remote_code=True,
)
model.eval()
# Build prompts at the requested token length.
records = load_payload_message_handler_prompts(num_prompts)
base_prompts = [render_chat(tok, r) for r in records]
filler = (records[0].get("currentMessage") or {}).get(
"content", "Continue the operational notes."
)
prompts = [
pad_prompt_to_length(tok, p, target_tokens=prompt_tokens, filler=filler)
for p in base_prompts
]
real_lens = [tok(p, return_tensors="pt").input_ids.shape[-1] for p in prompts]
log.info(
"built %d prompts at target=%d tokens (actual range %d..%d)",
len(prompts),
prompt_tokens,
min(real_lens),
max(real_lens),
)
# 0. Compatibility check (always run; tells us whether the fused path
# even applies before we sink time into the runs).
from quantization.fused_turboquant_vendored.hf import (
check_model_compatibility,
)
compat = check_model_compatibility(model)
log.info(
"fused-turboquant compatibility: compatible=%s eligible=%d/%d "
"head_dim=%d known=%s issues=%s",
compat["compatible"],
compat["eligible_layers"],
compat["total_layers"],
compat["head_dim"],
compat["known_compatible"],
compat["issues"],
)
# 1. Baseline
log.info("--- path 1/3: baseline bf16 + DynamicCache ---")
base_res = measure_path(
label="baseline_bf16",
model=model,
tokenizer=tok,
prompts=prompts,
max_new_tokens=max_new_tokens,
cache_factory=lambda: DynamicCache(),
)
log.info(
"baseline: peak=%.3f GB toks/s=%.2f new=%d elapsed=%.2fs",
base_res["peak_vram_bytes"] / 1e9,
base_res["toks_per_s"],
base_res["tokens_new"],
base_res["elapsed_s"],
)
# 2. Pure-PyTorch turbokv (turboquant import name)
log.info("--- path 2/3: pure-PyTorch turbokv (TurboQuantCache) ---")
try:
from turboquant import TurboQuantCache
except Exception as exc:
log.warning("turbokv import failed: %r — skipping pure-PyTorch path", exc)
turbokv_res = {"label": "turbokv_pyt", "error": repr(exc)}
else:
turbokv_res = measure_path(
label=f"turbokv_pyt_{bits}bit",
model=model,
tokenizer=tok,
prompts=prompts,
max_new_tokens=max_new_tokens,
cache_factory=lambda: TurboQuantCache(
model.config, nbits=bits, base_seed=42, skip_layers=set() # noqa: F821
),
)
log.info(
"turbokv: peak=%.3f GB toks/s=%.2f new=%d elapsed=%.2fs",
turbokv_res["peak_vram_bytes"] / 1e9,
turbokv_res["toks_per_s"],
turbokv_res["tokens_new"],
turbokv_res["elapsed_s"],
)
# 3. Fused-turboquant
log.info("--- path 3/3: fused-turboquant (Triton kernels) ---")
if not compat["compatible"]:
fused_res = {
"label": "fused_skipped_incompatible",
"error": f"check_model_compatibility returned compatible=False: {compat['issues']}",
}
log.warning("skipping fused path: %s", fused_res["error"])
else:
from quantization.fused_turboquant_vendored.hf import (
patch_model,
unpatch_model,
)
# patch_model pre-flights via verify=True; failures bubble up.
try:
# Per-prompt patch+unpatch so the cache starts clean each call,
# mirroring the cache_factory pattern used for the other paths.
def factory():
return patch_model(model, bits=bits, compress_v=True, verify=False) # noqa: F821
def cleanup():
unpatch_model(model) # noqa: F821
# Sanity-check the patch once with verify=True before benchmarking.
verify_cache = patch_model(model, bits=bits, compress_v=True, verify=True)
unpatch_model(model)
del verify_cache
_free()
fused_res = measure_path(
label=f"fused_turboquant_{bits}bit",
model=model,
tokenizer=tok,
prompts=prompts,
max_new_tokens=max_new_tokens,
cache_factory=factory,
pre_generate=factory,
post_generate=cleanup,
)
log.info(
"fused: peak=%.3f GB toks/s=%.2f new=%d elapsed=%.2fs",
fused_res["peak_vram_bytes"] / 1e9,
fused_res["toks_per_s"],
fused_res["tokens_new"],
fused_res["elapsed_s"],
)
except Exception as exc:
tb = traceback.format_exc()
fused_res = {
"label": f"fused_turboquant_{bits}bit",
"error": "".join(traceback.format_exception_only(type(exc), exc)).strip(),
"traceback_tail": "\n".join(tb.splitlines()[-12:]),
}
log.error("fused path failed: %s", fused_res["error"])
log.error("tail:\n%s", fused_res["traceback_tail"])
# Free the model before the next one.
del model, tok
_free()
return {
"model_id": model_id,
"num_prompts": num_prompts,
"prompt_tokens_target": prompt_tokens,
"prompt_tokens_actual_range": [min(real_lens), max(real_lens)],
"max_new_tokens": max_new_tokens,
"bits": bits,
"compatibility": compat,
"baseline": base_res,
"turbokv_pyt": turbokv_res,
"fused_turboquant": fused_res,
}
def _print_table(result: dict) -> None:
print()
print("=" * 78)
print(f"fused-TurboQuant validation report: {result['model_id']}")
print("=" * 78)
print(
f"prompts: {result['num_prompts']} x ~{result['prompt_tokens_target']} tokens, "
f"{result['max_new_tokens']} new each, {result['bits']}-bit"
)
print()
rows = [
("baseline (bf16 DynamicCache)", result["baseline"]),
("pure-PyTorch turbokv 0.1.0", result["turbokv_pyt"]),
("fused-turboquant 0.1.0", result["fused_turboquant"]),
]
print(f"{'path':40s} {'peak VRAM':>12s} {'tokens/sec':>12s}")
print("-" * 70)
for name, r in rows:
if "error" in r:
print(f"{name:40s} {'SKIP':>12s} {'SKIP':>12s} ({r['error'][:80]})")
continue
print(
f"{name:40s} {r['peak_vram_bytes']/1e9:>9.3f} GB {r['toks_per_s']:>9.2f} tok/s"
)
print()
if "error" not in result["turbokv_pyt"] and "error" not in result["fused_turboquant"]:
speedup = (
result["fused_turboquant"]["toks_per_s"]
/ max(result["turbokv_pyt"]["toks_per_s"], 1e-9)
)
print(f"fused vs turbokv-pyt speedup: {speedup:.2f}x")
if "error" not in result["fused_turboquant"]:
sample = result["fused_turboquant"]["decoded_first"]
sample = sample[:240].replace("\n", " ")
print(f"fused sample[0]: {sample!r}")
print("=" * 78)
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(
"--bonus-model",
default="google/gemma-4-E2B",
help="Optional bonus model. Skipped if check_model_compatibility "
"returns compatible=False (e.g., dense attention).",
)
ap.add_argument("--num-prompts", type=int, default=5)
ap.add_argument("--max-new-tokens", type=int, default=128)
ap.add_argument("--prompt-tokens", type=int, default=4096)
ap.add_argument("--bits", type=int, default=4, choices=(3, 4))
ap.add_argument(
"--report",
default=str(
ROOT / "scripts" / "quantization" / "fused_turboquant_report.json"
),
)
ap.add_argument(
"--enforce-speedup",
type=float,
default=1.5,
help="Required fused tok/s / turbokv tok/s ratio. Set 0 to disable.",
)
args = ap.parse_args()
if not torch.cuda.is_available():
raise RuntimeError("CUDA required")
results: list[dict] = []
primary = run_one_model(
model_id=args.model,
num_prompts=args.num_prompts,
max_new_tokens=args.max_new_tokens,
prompt_tokens=args.prompt_tokens,
bits=args.bits,
)
results.append(primary)
_print_table(primary)
# Bonus model: only attempt if user named one and we can probe it.
if args.bonus_model:
log.info("attempting bonus model %s", args.bonus_model)
try:
bonus_tok = AutoTokenizer.from_pretrained(
args.bonus_model, trust_remote_code=True
)
bonus_model = AutoModelForCausalLM.from_pretrained(
args.bonus_model,
torch_dtype=torch.bfloat16,
device_map="cuda",
trust_remote_code=True,
)
from quantization.fused_turboquant_vendored.hf import (
check_model_compatibility,
)
compat = check_model_compatibility(bonus_model)
log.info(
"bonus compatibility: compatible=%s known=%s issues=%s",
compat["compatible"],
compat["known_compatible"],
compat["issues"],
)
del bonus_model, bonus_tok
_free()
if compat["compatible"]:
bonus = run_one_model(
model_id=args.bonus_model,
num_prompts=args.num_prompts,
max_new_tokens=args.max_new_tokens,
prompt_tokens=args.prompt_tokens,
bits=args.bits,
)
results.append(bonus)
_print_table(bonus)
else:
log.warning(
"bonus model %s skipped: not compatible (%s)",
args.bonus_model,
compat["issues"],
)
except Exception as exc:
log.warning(
"bonus model %s failed to load (%r) — skipping",
args.bonus_model,
exc,
)
Path(args.report).write_text(json.dumps(results, indent=2), encoding="utf-8")
log.info("wrote report to %s", args.report)
# Assertions (only on primary). Skip the speedup check entirely if the
# fused path was unable to run — the test still records the blocker.
failures: list[str] = []
fused = primary["fused_turboquant"]
turbokv = primary["turbokv_pyt"]
if "error" in fused:
failures.append(
f"fused-turboquant did not run on {primary['model_id']}: {fused['error']}"
)
elif "error" not in turbokv:
if fused["peak_vram_bytes"] > turbokv["peak_vram_bytes"]:
failures.append(
f"fused peak VRAM ({fused['peak_vram_bytes']/1e9:.3f} GB) > "
f"turbokv peak ({turbokv['peak_vram_bytes']/1e9:.3f} GB)"
)
if args.enforce_speedup > 0:
ratio = fused["toks_per_s"] / max(turbokv["toks_per_s"], 1e-9)
if ratio < args.enforce_speedup:
failures.append(
f"fused/turbokv tok/s ratio {ratio:.2f} < required "
f"{args.enforce_speedup}"
)
if failures:
print("\nFAILED ASSERTIONS:")
for f in failures:
print(f" - {f}")
return 1
print("\nAll assertions passed.")
return 0
if __name__ == "__main__":
sys.exit(main())