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
219 lines
7.0 KiB
Python
219 lines
7.0 KiB
Python
"""PolarQuant: optimal Gaussian weight quantization via Hadamard rotation.
|
|
|
|
Pipeline:
|
|
1. Per-block L2 normalize (block_size must be a power of 2).
|
|
2. Walsh-Hadamard rotation -> coordinates ~ N(0, 1/d).
|
|
3. Lloyd-Max optimal scalar quantization for N(0, 1).
|
|
4. Optional 1-bit QJL residual sign correction.
|
|
|
|
Storage layout: int8 codes + fp16 per-block norms (+ 1-bit QJL signs).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import torch
|
|
import torch.nn.functional as F
|
|
from scipy.stats import norm
|
|
|
|
# Import the canonical xorshift32 helper (sibling of the polarquant/ package).
|
|
_HERE = Path(__file__).resolve().parent
|
|
_QUANT_DIR = _HERE.parent
|
|
if str(_QUANT_DIR) not in sys.path:
|
|
sys.path.insert(0, str(_QUANT_DIR))
|
|
from polar_xorshift32 import polar_xorshift32_signs # noqa: E402
|
|
|
|
# Pre-computed Lloyd-Max centroids for N(0,1). Keyed by bits; entries stay
|
|
# None until first use (computed on demand by ``_ensure_centroids``).
|
|
_GAUSSIAN_CENTROIDS: dict[int, torch.Tensor | None] = {
|
|
2: torch.tensor([-1.5104, -0.4528, 0.4528, 1.5104]),
|
|
3: torch.tensor(
|
|
[-2.1520, -1.3440, -0.7560, -0.2451, 0.2451, 0.7560, 1.3440, 2.1520]
|
|
),
|
|
4: None,
|
|
5: None,
|
|
6: None,
|
|
}
|
|
|
|
_HADAMARD_CACHE: dict[int, torch.Tensor] = {}
|
|
|
|
# Magnitude of the QJL residual correction along the random projection
|
|
# direction. Small enough to never overshoot the Lloyd-Max cell.
|
|
_QJL_CORRECTION_MAGNITUDE = 0.5
|
|
|
|
# Deterministic seed for the per-block random sign vector used by the QJL
|
|
# correction. The encoder and decoder must agree on it.
|
|
_QJL_SEED = 42
|
|
|
|
|
|
def _compute_lloyd_max_centroids(n_levels: int, n_iter: int = 100) -> torch.Tensor:
|
|
"""Iteratively solve for MSE-optimal scalar quantizer levels for N(0,1).
|
|
|
|
Conditional expectation in [a, b] for X ~ N(0,1):
|
|
E[X | a < X < b] = (phi(a) - phi(b)) / (Phi(b) - Phi(a))
|
|
"""
|
|
boundaries = np.linspace(-4, 4, n_levels + 1)
|
|
boundaries[0] = -np.inf
|
|
boundaries[-1] = np.inf
|
|
|
|
centroids = np.zeros(n_levels)
|
|
for _ in range(n_iter):
|
|
for i in range(n_levels):
|
|
a, b = boundaries[i], boundaries[i + 1]
|
|
prob = norm.cdf(b) - norm.cdf(a)
|
|
if prob > 1e-10:
|
|
centroids[i] = (norm.pdf(a) - norm.pdf(b)) / prob
|
|
else:
|
|
centroids[i] = (a + b) / 2
|
|
|
|
new_boundaries = np.zeros(n_levels + 1)
|
|
new_boundaries[0] = -np.inf
|
|
new_boundaries[-1] = np.inf
|
|
for i in range(1, n_levels):
|
|
new_boundaries[i] = (centroids[i - 1] + centroids[i]) / 2
|
|
boundaries = new_boundaries
|
|
|
|
return torch.tensor(centroids, dtype=torch.float32)
|
|
|
|
|
|
def _ensure_centroids(bits: int) -> torch.Tensor:
|
|
if bits not in _GAUSSIAN_CENTROIDS:
|
|
raise ValueError(
|
|
f"PolarQuant supports bits in {sorted(_GAUSSIAN_CENTROIDS)}; got {bits}"
|
|
)
|
|
cached = _GAUSSIAN_CENTROIDS[bits]
|
|
if cached is None:
|
|
cached = _compute_lloyd_max_centroids(1 << bits)
|
|
_GAUSSIAN_CENTROIDS[bits] = cached
|
|
return cached
|
|
|
|
|
|
def _hadamard_matrix(n: int, device: torch.device | None = None) -> torch.Tensor:
|
|
"""Walsh-Hadamard matrix of size n (power of 2), normalized so H @ H.T == I."""
|
|
if n & (n - 1) != 0:
|
|
raise ValueError(f"Hadamard size must be a power of 2; got {n}")
|
|
cached = _HADAMARD_CACHE.get(n)
|
|
if cached is None:
|
|
if n == 1:
|
|
cached = torch.tensor([[1.0]])
|
|
else:
|
|
h = _hadamard_matrix(n // 2)
|
|
cached = torch.cat(
|
|
[torch.cat([h, h], 1), torch.cat([h, -h], 1)], 0
|
|
) / math.sqrt(2)
|
|
_HADAMARD_CACHE[n] = cached
|
|
if device is not None:
|
|
return cached.to(device)
|
|
return cached
|
|
|
|
|
|
@dataclass
|
|
class PolarQuantResult:
|
|
"""Result of PolarQuant compression for one tensor."""
|
|
|
|
codes: torch.Tensor # int8, length n_elements
|
|
norms: torch.Tensor # fp16, length n_blocks
|
|
bits: int
|
|
block_size: int
|
|
shape: torch.Size
|
|
n_elements: int
|
|
use_qjl: bool
|
|
qjl_signs: torch.Tensor | None = None # uint8, length n_blocks
|
|
|
|
|
|
def polar_quantize(
|
|
weight: torch.Tensor,
|
|
bits: int = 4,
|
|
block_size: int = 128,
|
|
use_qjl: bool = True,
|
|
) -> PolarQuantResult:
|
|
"""Encode ``weight`` into PolarQuant codes."""
|
|
centroids = _ensure_centroids(bits).to(weight.device)
|
|
H = _hadamard_matrix(block_size, weight.device)
|
|
|
|
flat = weight.detach().float().flatten()
|
|
n = flat.numel()
|
|
pad = (block_size - n % block_size) % block_size
|
|
if pad > 0:
|
|
flat = F.pad(flat, (0, pad))
|
|
blocks = flat.view(-1, block_size)
|
|
|
|
norms = blocks.norm(dim=1, keepdim=True).clamp(min=1e-10)
|
|
blocks_norm = blocks / norms
|
|
blocks_rot = blocks_norm @ H
|
|
|
|
# After rotation each coord is ~ N(0, 1/sqrt(d)); scale to N(0, 1)
|
|
# so the Lloyd-Max centroids apply directly.
|
|
scale = math.sqrt(block_size)
|
|
blocks_scaled = blocks_rot * scale
|
|
|
|
diffs = blocks_scaled.unsqueeze(-1) - centroids.unsqueeze(0).unsqueeze(0)
|
|
codes = diffs.abs().argmin(dim=-1).to(torch.int8)
|
|
|
|
qjl_signs: torch.Tensor | None = None
|
|
if use_qjl:
|
|
recon_scaled = centroids[codes.long()]
|
|
residual = blocks_scaled - recon_scaled
|
|
random_signs = torch.from_numpy(
|
|
polar_xorshift32_signs(block_size, _QJL_SEED)
|
|
).to(dtype=torch.float32, device=weight.device)
|
|
projections = (residual * random_signs.unsqueeze(0)).sum(dim=1)
|
|
qjl_signs = (projections >= 0).to(torch.uint8)
|
|
|
|
return PolarQuantResult(
|
|
codes=codes.flatten()[:n],
|
|
norms=norms.squeeze(1).to(torch.float16),
|
|
bits=bits,
|
|
block_size=block_size,
|
|
shape=weight.shape,
|
|
n_elements=n,
|
|
use_qjl=use_qjl,
|
|
qjl_signs=qjl_signs,
|
|
)
|
|
|
|
|
|
def polar_dequantize(
|
|
result: PolarQuantResult, device: torch.device | None = None
|
|
) -> torch.Tensor:
|
|
"""Decode a PolarQuant payload back to fp16."""
|
|
if device is None:
|
|
device = result.codes.device
|
|
|
|
centroids = _ensure_centroids(result.bits).to(device)
|
|
H = _hadamard_matrix(result.block_size, device)
|
|
|
|
bs = result.block_size
|
|
n = result.n_elements
|
|
pad = (bs - n % bs) % bs
|
|
codes = result.codes.to(device).long()
|
|
if pad > 0:
|
|
codes = F.pad(codes, (0, pad))
|
|
blocks_codes = codes.view(-1, bs)
|
|
|
|
recon_scaled = centroids[blocks_codes]
|
|
|
|
if result.use_qjl and result.qjl_signs is not None:
|
|
random_signs = torch.from_numpy(
|
|
polar_xorshift32_signs(bs, _QJL_SEED)
|
|
).to(dtype=torch.float32, device=device)
|
|
correction_dir = random_signs.unsqueeze(0) / math.sqrt(bs)
|
|
correction_sign = result.qjl_signs.float().to(device) * 2 - 1
|
|
recon_scaled = recon_scaled + (
|
|
_QJL_CORRECTION_MAGNITUDE
|
|
* correction_sign.unsqueeze(1)
|
|
* correction_dir
|
|
)
|
|
|
|
scale = math.sqrt(bs)
|
|
recon_rot = recon_scaled / scale
|
|
recon_norm = recon_rot @ H
|
|
norms = result.norms.float().to(device)
|
|
recon = recon_norm * norms.unsqueeze(1)
|
|
|
|
return recon.flatten()[:n].view(result.shape).half()
|