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
360 lines
11 KiB
Python
360 lines
11 KiB
Python
"""Apply PolarQuant to a HuggingFace causal-LM checkpoint.
|
|
|
|
PolarQuant
|
|
Vicentino, Caio. *PolarQuant: Optimal Gaussian Weight Quantization via
|
|
Hadamard Rotation for LLM Compression*. arXiv:2603.29078, March 2026.
|
|
Reference: https://github.com/caiovicentino/eoq-quantization @
|
|
15a12160245d7d3015290c6c5b6dbb7f22094d5e.
|
|
|
|
Walks every ``nn.Linear`` whose weight tensor has at least ``--min-numel``
|
|
elements and replaces its ``weight`` with the PolarQuant round-trip
|
|
reconstruction (per-block L2 normalize -> WHT -> Lloyd-Max -> optional
|
|
1-bit QJL residual -> inverse). Saves the resulting model + a sidecar
|
|
``polarquant_artifacts.safetensors`` carrying the int8 codes + fp16
|
|
norms (the format a downstream INT4 inference kernel consumes).
|
|
|
|
PolarQuant is data-free: ``--calibration`` is accepted only for CLI
|
|
parity with the other quantizers.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import gc
|
|
import json
|
|
import logging
|
|
import sys
|
|
import time
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
from safetensors.torch import save_file
|
|
from transformers import AutoConfig
|
|
|
|
_HERE = Path(__file__).resolve().parent
|
|
if str(_HERE) not in sys.path:
|
|
sys.path.insert(0, str(_HERE))
|
|
|
|
from _common import ( # noqa: E402
|
|
kernel_manifest_fragment,
|
|
load_model_and_tokenizer,
|
|
save_model,
|
|
)
|
|
from polarquant.polar_quant import ( # noqa: E402
|
|
PolarQuantResult,
|
|
polar_dequantize,
|
|
polar_quantize,
|
|
)
|
|
|
|
logger = logging.getLogger("polarquant_apply")
|
|
|
|
|
|
# Architectures we have explicitly verified against the standard
|
|
# ``self_attn.{q,k,v,o}_proj`` / ``mlp.{gate,up,down}_proj`` linear layout.
|
|
_KNOWN_GOOD_ARCH_SUBSTRINGS = ("gemma", "llama", "mistral", "phi3")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PolarQuantRecipe:
|
|
"""Knobs handed to PolarQuant for one model."""
|
|
|
|
bits: int = 4
|
|
block_size: int = 128
|
|
use_qjl: bool = True
|
|
min_numel: int = 4096
|
|
skip_lm_head: bool = True
|
|
skip_embedding: bool = True
|
|
dtype: str = "float16"
|
|
|
|
def to_json(self) -> dict[str, object]:
|
|
return {
|
|
**asdict(self),
|
|
"paper": "arXiv:2603.29078",
|
|
"upstream_commit": "15a12160245d7d3015290c6c5b6dbb7f22094d5e",
|
|
"upstream_repo": "https://github.com/caiovicentino/eoq-quantization",
|
|
}
|
|
|
|
|
|
def _iter_linears(
|
|
model: nn.Module,
|
|
*,
|
|
min_numel: int,
|
|
skip_lm_head: bool,
|
|
skip_embedding: bool,
|
|
) -> list[tuple[str, nn.Linear]]:
|
|
"""Linears we want to quantize, in deterministic order.
|
|
|
|
Skips small projections / MoE routers (``min_numel``), the LM head
|
|
(weight-tied on many causal-LM models), and embedding tables (lookups,
|
|
not multiplies).
|
|
"""
|
|
out: list[tuple[str, nn.Linear]] = []
|
|
for name, module in model.named_modules():
|
|
if not isinstance(module, nn.Linear):
|
|
continue
|
|
if skip_lm_head and name.endswith("lm_head"):
|
|
continue
|
|
if skip_embedding and "embed" in name:
|
|
continue
|
|
if module.weight.numel() < min_numel:
|
|
continue
|
|
out.append((name, module))
|
|
return out
|
|
|
|
|
|
def _quantize_linear_inplace(
|
|
layer: nn.Linear,
|
|
*,
|
|
bits: int,
|
|
block_size: int,
|
|
use_qjl: bool,
|
|
) -> tuple[PolarQuantResult, float]:
|
|
"""Run PolarQuant on one linear's weight, write the reconstruction back.
|
|
|
|
Returns ``(result, mse)`` where ``mse`` is the reconstruction MSE.
|
|
"""
|
|
weight = layer.weight.data
|
|
target_dtype = weight.dtype
|
|
target_device = weight.device
|
|
|
|
result = polar_quantize(
|
|
weight.detach().to(torch.float32),
|
|
bits=bits,
|
|
block_size=block_size,
|
|
use_qjl=use_qjl,
|
|
)
|
|
|
|
recon = polar_dequantize(result, device=target_device).to(target_dtype)
|
|
mse = (weight.float() - recon.float()).pow(2).mean().item()
|
|
|
|
with torch.no_grad():
|
|
layer.weight.data.copy_(recon)
|
|
del recon
|
|
return result, mse
|
|
|
|
|
|
def _save_artifacts(
|
|
artifacts: dict[str, PolarQuantResult],
|
|
*,
|
|
output_dir: Path,
|
|
) -> Path:
|
|
"""Persist int8 codes + fp16 norms (+ optional QJL bits) to a single
|
|
safetensors blob keyed by parameter name.
|
|
"""
|
|
flat: dict[str, torch.Tensor] = {}
|
|
for name, res in artifacts.items():
|
|
flat[f"{name}.codes"] = res.codes.detach().to(torch.int8).cpu().contiguous()
|
|
flat[f"{name}.norms"] = res.norms.detach().to(torch.float16).cpu().contiguous()
|
|
if res.use_qjl and res.qjl_signs is not None:
|
|
flat[f"{name}.qjl"] = res.qjl_signs.detach().to(torch.uint8).cpu().contiguous()
|
|
|
|
sidecar = output_dir / "polarquant_artifacts.safetensors"
|
|
sidecar.parent.mkdir(parents=True, exist_ok=True)
|
|
save_file(flat, str(sidecar))
|
|
return sidecar
|
|
|
|
|
|
def quantize_checkpoint(
|
|
*,
|
|
model_id_or_path: str,
|
|
output_dir: Path,
|
|
recipe: PolarQuantRecipe,
|
|
device: str = "auto",
|
|
save_artifacts: bool = True,
|
|
progress_every: int = 32,
|
|
) -> dict[str, object]:
|
|
"""Load -> PolarQuant every linear -> save. Returns a stats dict."""
|
|
output_dir = Path(output_dir)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
logger.info("Loading config for %s", model_id_or_path)
|
|
config = AutoConfig.from_pretrained(model_id_or_path, trust_remote_code=True)
|
|
arch_lc = (getattr(config, "model_type", "") or "").lower()
|
|
if not any(s in arch_lc for s in _KNOWN_GOOD_ARCH_SUBSTRINGS):
|
|
logger.warning(
|
|
"Architecture %r is not on the known-good list %s; PolarQuant "
|
|
"will proceed but the reconstruction quality has not been "
|
|
"verified for this model family in this repo.",
|
|
arch_lc,
|
|
_KNOWN_GOOD_ARCH_SUBSTRINGS,
|
|
)
|
|
|
|
dtype_map = {
|
|
"float16": torch.float16,
|
|
"bfloat16": torch.bfloat16,
|
|
"float32": torch.float32,
|
|
}
|
|
dtype = dtype_map[recipe.dtype]
|
|
device_map = device if device != "auto" else None
|
|
|
|
logger.info("Loading model weights (dtype=%s, device=%s)", recipe.dtype, device)
|
|
model, tokenizer = load_model_and_tokenizer(
|
|
model_id_or_path, device_map=device_map, dtype=dtype
|
|
)
|
|
if device == "auto" and torch.cuda.is_available():
|
|
model = model.to("cuda")
|
|
model.eval()
|
|
|
|
layers = _iter_linears(
|
|
model,
|
|
min_numel=recipe.min_numel,
|
|
skip_lm_head=recipe.skip_lm_head,
|
|
skip_embedding=recipe.skip_embedding,
|
|
)
|
|
logger.info("Found %d linear layers eligible for PolarQuant", len(layers))
|
|
|
|
artifacts: dict[str, PolarQuantResult] = {}
|
|
mses: list[float] = []
|
|
n_params_quantized = 0
|
|
t0 = time.perf_counter()
|
|
|
|
for i, (name, layer) in enumerate(layers, start=1):
|
|
n_params_quantized += layer.weight.numel()
|
|
result, mse = _quantize_linear_inplace(
|
|
layer,
|
|
bits=recipe.bits,
|
|
block_size=recipe.block_size,
|
|
use_qjl=recipe.use_qjl,
|
|
)
|
|
if save_artifacts:
|
|
artifacts[f"{name}.weight"] = result
|
|
else:
|
|
del result
|
|
mses.append(mse)
|
|
|
|
if i % progress_every == 0 or i == len(layers):
|
|
elapsed = time.perf_counter() - t0
|
|
avg_mse = sum(mses) / len(mses)
|
|
logger.info(
|
|
" [%4d/%d] %s numel=%d mse=%.3e avg_mse=%.3e elapsed=%.1fs",
|
|
i, len(layers), name, layer.weight.numel(), mse, avg_mse, elapsed,
|
|
)
|
|
|
|
# save_pretrained shards on CPU; move first so we don't allocate a
|
|
# multi-shard CUDA-resident state dict.
|
|
model.to("cpu")
|
|
gc.collect()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
logger.info("Saving quantized model to %s", output_dir)
|
|
save_model(model, tokenizer, output_dir)
|
|
|
|
sidecar_path: Path | None = None
|
|
if save_artifacts and artifacts:
|
|
sidecar_path = _save_artifacts(artifacts, output_dir=output_dir)
|
|
logger.info("Saved PolarQuant artifacts to %s", sidecar_path)
|
|
|
|
config_path = output_dir / "polarquant_config.json"
|
|
config_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"source_model": model_id_or_path,
|
|
"recipe": recipe.to_json(),
|
|
"n_layers_quantized": len(layers),
|
|
"n_params_quantized": n_params_quantized,
|
|
"average_block_mse": sum(mses) / max(1, len(mses)),
|
|
"max_block_mse": max(mses) if mses else 0.0,
|
|
"elapsed_seconds": time.perf_counter() - t0,
|
|
"kernel_manifest": kernel_manifest_fragment("polarquant"),
|
|
},
|
|
indent=2,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
return {
|
|
"output_dir": str(output_dir),
|
|
"n_layers": len(layers),
|
|
"n_params": n_params_quantized,
|
|
"average_mse": sum(mses) / max(1, len(mses)),
|
|
"sidecar": str(sidecar_path) if sidecar_path else None,
|
|
"config_path": str(config_path),
|
|
}
|
|
|
|
|
|
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser(
|
|
description=(
|
|
"Apply PolarQuant (arXiv:2603.29078) to a HF causal-LM checkpoint."
|
|
),
|
|
)
|
|
p.add_argument("--model", required=True, help="HF repo id or local path.")
|
|
p.add_argument("--output", required=True, type=Path)
|
|
p.add_argument(
|
|
"--calibration",
|
|
type=Path,
|
|
default=None,
|
|
help="Accepted but unused (PolarQuant is data-free); validated for existence.",
|
|
)
|
|
p.add_argument("--calibration-samples", type=int, default=128)
|
|
p.add_argument("--bits", type=int, default=4, choices=[2, 3, 4, 5, 6])
|
|
p.add_argument("--block-size", type=int, default=128)
|
|
p.add_argument("--no-qjl", action="store_true")
|
|
p.add_argument("--min-numel", type=int, default=4096)
|
|
p.add_argument("--include-lm-head", action="store_true")
|
|
p.add_argument("--include-embedding", action="store_true")
|
|
p.add_argument(
|
|
"--dtype", default="float16", choices=["float16", "bfloat16", "float32"]
|
|
)
|
|
p.add_argument("--device", default="auto")
|
|
p.add_argument("--no-artifacts", action="store_true")
|
|
p.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Validate inputs and print the recipe; don't load or save weights.",
|
|
)
|
|
p.add_argument(
|
|
"--log-level",
|
|
default="INFO",
|
|
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
)
|
|
return p
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = _build_arg_parser().parse_args(argv)
|
|
|
|
logging.basicConfig(
|
|
level=getattr(logging, args.log_level),
|
|
format="[%(asctime)s] %(name)s %(levelname)s: %(message)s",
|
|
datefmt="%H:%M:%S",
|
|
)
|
|
|
|
if args.calibration is not None and not args.calibration.exists():
|
|
raise FileNotFoundError(
|
|
f"--calibration path does not exist: {args.calibration}. "
|
|
"PolarQuant doesn't read it, but if you pass the flag it still "
|
|
"has to point at a real file."
|
|
)
|
|
|
|
recipe = PolarQuantRecipe(
|
|
bits=args.bits,
|
|
block_size=args.block_size,
|
|
use_qjl=not args.no_qjl,
|
|
min_numel=args.min_numel,
|
|
skip_lm_head=not args.include_lm_head,
|
|
skip_embedding=not args.include_embedding,
|
|
dtype=args.dtype,
|
|
)
|
|
|
|
if args.dry_run:
|
|
print(json.dumps({"recipe": recipe.to_json(), "model": args.model}, indent=2))
|
|
return 0
|
|
|
|
stats = quantize_checkpoint(
|
|
model_id_or_path=args.model,
|
|
output_dir=args.output,
|
|
recipe=recipe,
|
|
device=args.device,
|
|
save_artifacts=not args.no_artifacts,
|
|
)
|
|
print(json.dumps(stats, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|