Upload 11 files
Browse files- __init__.py +35 -0
- __main__.py +67 -0
- config.py +92 -0
- factored.py +547 -0
- fp16.py +55 -0
- pipeline.py +559 -0
- q2k.py +223 -0
- seeds.py +190 -0
- sieve.py +106 -0
- spectral.py +226 -0
- viterbi.py +164 -0
__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HPC-Quantize V2: factored Q2_K quantization in Python.
|
| 2 |
+
|
| 3 |
+
The package keeps the full HPC decision stack (batched GGML seeds, WLS
|
| 4 |
+
superblock solve, factored candidate field, sequential Sieve, 36-state
|
| 5 |
+
Viterbi, spectral/DC shaping, local-safety floors) while replacing the
|
| 6 |
+
original factored solver with a monotone, multi-start, fp16-exact version.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from .config import HPCConfig
|
| 10 |
+
from .factored import (
|
| 11 |
+
FactorResult,
|
| 12 |
+
factor_1d,
|
| 13 |
+
factor_candidates,
|
| 14 |
+
factor_only_solve,
|
| 15 |
+
factor_q2k_block,
|
| 16 |
+
)
|
| 17 |
+
from .pipeline import Q2KQuantizer, QuantizedTensor, TensorStats
|
| 18 |
+
from .q2k import Q2KBlock, QK_K, dequantize_params, pack_codes, unpack_codes
|
| 19 |
+
|
| 20 |
+
__all__ = [
|
| 21 |
+
"HPCConfig",
|
| 22 |
+
"FactorResult",
|
| 23 |
+
"Q2KBlock",
|
| 24 |
+
"Q2KQuantizer",
|
| 25 |
+
"QuantizedTensor",
|
| 26 |
+
"TensorStats",
|
| 27 |
+
"QK_K",
|
| 28 |
+
"dequantize_params",
|
| 29 |
+
"factor_1d",
|
| 30 |
+
"factor_candidates",
|
| 31 |
+
"factor_only_solve",
|
| 32 |
+
"factor_q2k_block",
|
| 33 |
+
"pack_codes",
|
| 34 |
+
"unpack_codes",
|
| 35 |
+
]
|
__main__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command-line entry point for the V2 factored Q2_K quantizer.
|
| 2 |
+
|
| 3 |
+
Examples::
|
| 4 |
+
|
| 5 |
+
python -m hpc_quantize_v2 --demo
|
| 6 |
+
python -m hpc_quantize_v2 --input tensor.npy --output tensor_q2k.npz
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
from .config import HPCConfig
|
| 16 |
+
from .pipeline import Q2KQuantizer
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def main() -> None:
|
| 20 |
+
parser = argparse.ArgumentParser(description="HPC-Quantize V2 factored Q2_K")
|
| 21 |
+
parser.add_argument("--input", help=".npy file containing float weights")
|
| 22 |
+
parser.add_argument("--importance", help="optional .npy importance matrix")
|
| 23 |
+
parser.add_argument("--output", help="output .npz with scales/qs/dm")
|
| 24 |
+
parser.add_argument("--demo", action="store_true", help="quantize synthetic weights")
|
| 25 |
+
parser.add_argument("--blocks", type=int, default=64, help="demo block count")
|
| 26 |
+
parser.add_argument("--std", type=float, default=0.02, help="demo weight std")
|
| 27 |
+
parser.add_argument("--seed", type=int, default=1234, help="demo RNG seed")
|
| 28 |
+
parser.add_argument("--cand-d", type=int, default=None, help="candidate d count")
|
| 29 |
+
parser.add_argument("--cand-m", type=int, default=None, help="candidate min count")
|
| 30 |
+
parser.add_argument("--mse", action="store_true", help="disable HPC sequence search")
|
| 31 |
+
parser.add_argument("--verbose", action="store_true")
|
| 32 |
+
args = parser.parse_args()
|
| 33 |
+
|
| 34 |
+
if args.demo:
|
| 35 |
+
rng = np.random.default_rng(args.seed)
|
| 36 |
+
weights = rng.normal(0.0, args.std, size=(args.blocks, 256)).astype(np.float32)
|
| 37 |
+
importance = None
|
| 38 |
+
elif args.input:
|
| 39 |
+
weights = np.load(args.input)
|
| 40 |
+
importance = np.load(args.importance) if args.importance else None
|
| 41 |
+
else:
|
| 42 |
+
parser.error("pass --demo or --input")
|
| 43 |
+
|
| 44 |
+
cfg = HPCConfig(verbose=True)
|
| 45 |
+
if args.cand_d is not None:
|
| 46 |
+
cfg.n_cand_d = args.cand_d
|
| 47 |
+
if args.cand_m is not None:
|
| 48 |
+
cfg.n_cand_m = args.cand_m
|
| 49 |
+
if args.mse:
|
| 50 |
+
cfg.optimizer = "mse"
|
| 51 |
+
|
| 52 |
+
result = Q2KQuantizer(cfg).quantize(weights, importance=importance)
|
| 53 |
+
if args.output:
|
| 54 |
+
np.savez_compressed(
|
| 55 |
+
args.output,
|
| 56 |
+
scales=result.scales,
|
| 57 |
+
qs=result.qs,
|
| 58 |
+
dm=result.dm,
|
| 59 |
+
dequantized=result.dequantize(),
|
| 60 |
+
)
|
| 61 |
+
print(f"wrote {args.output}")
|
| 62 |
+
else:
|
| 63 |
+
_ = result.dequantize()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
main()
|
config.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for the HPC-Quantize V2 Q2_K engine.
|
| 2 |
+
|
| 3 |
+
Every tunable that was a compile-time macro, environment variable or hidden
|
| 4 |
+
global in the C engine is collected here as an explicit, validated field.
|
| 5 |
+
The defaults are the C engine's production defaults unless marked
|
| 6 |
+
``V2`` in the docstring, in which case they are deliberate improvements.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass(slots=True)
|
| 15 |
+
class HPCConfig:
|
| 16 |
+
# ---- block geometry -------------------------------------------------
|
| 17 |
+
qk_k: int = 256
|
| 18 |
+
|
| 19 |
+
# ---- candidate field ------------------------------------------------
|
| 20 |
+
# V2 improvement: the default is the 6x6 symbolic lattice (36 exact
|
| 21 |
+
# factored solves) instead of the 24x24 physical grid (576 solves).
|
| 22 |
+
# Set to 24/24 to reproduce the full C candidate field.
|
| 23 |
+
n_cand_d: int = 6
|
| 24 |
+
n_cand_m: int = 6
|
| 25 |
+
factor_pin: int = 3
|
| 26 |
+
factor_iters: int = 1
|
| 27 |
+
factor_standalone_iters: int = 16
|
| 28 |
+
factor_starts: int = 3
|
| 29 |
+
factor_ulp_radius: int = 1
|
| 30 |
+
factor_refine_1d: bool = True
|
| 31 |
+
factor_spectral_refine: bool = True
|
| 32 |
+
factor_exact_emit: bool = True
|
| 33 |
+
|
| 34 |
+
# ---- objective ------------------------------------------------------
|
| 35 |
+
use_spectral: bool = True
|
| 36 |
+
dc_lambda: float = 1.0
|
| 37 |
+
vw_lambda: float = 1.0
|
| 38 |
+
fold_depth: int = 1
|
| 39 |
+
fold_gamma: float = 2.0
|
| 40 |
+
dc_decay: float = 0.85
|
| 41 |
+
carry_cumulative: bool = True
|
| 42 |
+
sse_budget: float = 5.0e-4
|
| 43 |
+
greedy_override_ratio: float = 0.995
|
| 44 |
+
|
| 45 |
+
# ---- sieve ----------------------------------------------------------
|
| 46 |
+
same_penalty: float = 0.85
|
| 47 |
+
log_slack: float = 1.5
|
| 48 |
+
quhits_per_block: int = 2
|
| 49 |
+
graph_max_blocks: int = 8192
|
| 50 |
+
|
| 51 |
+
# ---- Viterbi --------------------------------------------------------
|
| 52 |
+
optimizer: str = "hpc"
|
| 53 |
+
viterbi_beta: float = 0.25
|
| 54 |
+
viterbi_alpha: float = 0.08
|
| 55 |
+
temperature_sample_blocks: int = 128
|
| 56 |
+
|
| 57 |
+
# ---- assembly -------------------------------------------------------
|
| 58 |
+
refine_passes: int = 3
|
| 59 |
+
ulp_radius: int = 4
|
| 60 |
+
subblock_states: int = 6
|
| 61 |
+
subblock_sieve: bool = True
|
| 62 |
+
use_factor_floor: bool = True
|
| 63 |
+
use_candidate_floor: bool = True
|
| 64 |
+
apply_dc_carry: bool = True
|
| 65 |
+
|
| 66 |
+
# ---- seed clamp -----------------------------------------------------
|
| 67 |
+
clamp_kurtosis_off: float = 6.0
|
| 68 |
+
clamp_kurtosis_soft: float = 20.0
|
| 69 |
+
clamp_sigma_soft: float = 6.0
|
| 70 |
+
clamp_sigma_hard: float = 4.0
|
| 71 |
+
|
| 72 |
+
# ---- reporting ------------------------------------------------------
|
| 73 |
+
verbose: bool = False
|
| 74 |
+
|
| 75 |
+
def validate(self) -> "HPCConfig":
|
| 76 |
+
if self.qk_k != 256:
|
| 77 |
+
raise ValueError("Q2_K requires qk_k == 256")
|
| 78 |
+
if not (1 <= self.n_cand_d <= 24 and 1 <= self.n_cand_m <= 24):
|
| 79 |
+
raise ValueError("candidate counts must lie in [1, 24]")
|
| 80 |
+
if self.optimizer not in ("hpc", "mse"):
|
| 81 |
+
raise ValueError("optimizer must be 'hpc' or 'mse'")
|
| 82 |
+
if not 0 <= self.factor_pin <= 3:
|
| 83 |
+
raise ValueError("factor_pin must be a 2-bit mask")
|
| 84 |
+
if self.fold_depth < 0:
|
| 85 |
+
raise ValueError("fold_depth must be non-negative")
|
| 86 |
+
if not 0.0 <= self.dc_decay <= 1.0:
|
| 87 |
+
raise ValueError("dc_decay must lie in [0, 1]")
|
| 88 |
+
if self.ulp_radius < 0 or self.factor_ulp_radius < 0:
|
| 89 |
+
raise ValueError("ULP radii must be non-negative")
|
| 90 |
+
if self.refine_passes < 1:
|
| 91 |
+
raise ValueError("refine_passes must be >= 1")
|
| 92 |
+
return self
|
factored.py
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Grid-free factored Q2_K solver (V2).
|
| 2 |
+
|
| 3 |
+
The reference solver alternates four exact coordinate solves::
|
| 4 |
+
|
| 5 |
+
q | (d, Ls, dmin, Lm)
|
| 6 |
+
s_j,t_j | q
|
| 7 |
+
d,Ls | s
|
| 8 |
+
dmin,Lm | t
|
| 9 |
+
|
| 10 |
+
V2 keeps that decomposition but fixes three weaknesses:
|
| 11 |
+
|
| 12 |
+
1. ``factor_1d`` is no longer a fixed six-iteration Lloyd heuristic. It
|
| 13 |
+
starts from several deterministic candidates and finishes with an exact
|
| 14 |
+
integer-code coordinate refinement, so its weighted error is monotone.
|
| 15 |
+
2. The block solve is multi-start, accepts only objective-decreasing states,
|
| 16 |
+
and returns the best snapshot rather than the last iterate.
|
| 17 |
+
3. The emitted fp16 ``(d, dmin)`` pair is polished in its own domain, and the
|
| 18 |
+
spectral/DC shaping stage is applied inside the solver. The returned
|
| 19 |
+
objective therefore equals the objective of the bytes that are written.
|
| 20 |
+
|
| 21 |
+
The result object is immutable and self-describing, replacing the C
|
| 22 |
+
engine's many out-parameters.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
from dataclasses import dataclass
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
|
| 31 |
+
from .config import HPCConfig
|
| 32 |
+
from .fp16 import from_fp16_bits, fp16_ulp_neighbors, nearest_int, round_fp16, to_fp16_bits
|
| 33 |
+
from .q2k import NS, QK_K, codes_from_params, derive_subscales
|
| 34 |
+
from .spectral import objective, refine_codes_hadamard
|
| 35 |
+
|
| 36 |
+
N_CAND_D = 24
|
| 37 |
+
N_CAND_M = 24
|
| 38 |
+
TOTAL_CANDIDATES = N_CAND_D * N_CAND_M
|
| 39 |
+
|
| 40 |
+
MULTS_D = np.array(
|
| 41 |
+
[
|
| 42 |
+
0.780,
|
| 43 |
+
0.835,
|
| 44 |
+
0.880,
|
| 45 |
+
0.915,
|
| 46 |
+
0.943,
|
| 47 |
+
0.963,
|
| 48 |
+
0.978,
|
| 49 |
+
0.988,
|
| 50 |
+
0.994,
|
| 51 |
+
0.997,
|
| 52 |
+
0.999,
|
| 53 |
+
1.000,
|
| 54 |
+
1.002,
|
| 55 |
+
1.005,
|
| 56 |
+
1.011,
|
| 57 |
+
1.021,
|
| 58 |
+
1.035,
|
| 59 |
+
1.054,
|
| 60 |
+
1.080,
|
| 61 |
+
1.115,
|
| 62 |
+
1.160,
|
| 63 |
+
1.215,
|
| 64 |
+
1.275,
|
| 65 |
+
1.340,
|
| 66 |
+
],
|
| 67 |
+
dtype=np.float32,
|
| 68 |
+
)
|
| 69 |
+
MULTS_M = np.array(
|
| 70 |
+
[
|
| 71 |
+
0.750,
|
| 72 |
+
0.800,
|
| 73 |
+
0.840,
|
| 74 |
+
0.870,
|
| 75 |
+
0.900,
|
| 76 |
+
0.920,
|
| 77 |
+
0.940,
|
| 78 |
+
0.955,
|
| 79 |
+
0.970,
|
| 80 |
+
0.985,
|
| 81 |
+
0.995,
|
| 82 |
+
1.000,
|
| 83 |
+
1.005,
|
| 84 |
+
1.015,
|
| 85 |
+
1.030,
|
| 86 |
+
1.045,
|
| 87 |
+
1.060,
|
| 88 |
+
1.080,
|
| 89 |
+
1.100,
|
| 90 |
+
1.130,
|
| 91 |
+
1.160,
|
| 92 |
+
1.200,
|
| 93 |
+
1.250,
|
| 94 |
+
1.300,
|
| 95 |
+
],
|
| 96 |
+
dtype=np.float32,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
CAND_TO_QUHIT = np.array(
|
| 100 |
+
[0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5],
|
| 101 |
+
dtype=np.int64,
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@dataclass(slots=True)
|
| 106 |
+
class FactorResult:
|
| 107 |
+
d: np.float32
|
| 108 |
+
dmin: np.float32
|
| 109 |
+
d16: np.uint16
|
| 110 |
+
m16: np.uint16
|
| 111 |
+
ls: np.ndarray
|
| 112 |
+
lm: np.ndarray
|
| 113 |
+
codes: np.ndarray
|
| 114 |
+
sse: float
|
| 115 |
+
objective: float
|
| 116 |
+
iterations: int
|
| 117 |
+
starts: int
|
| 118 |
+
|
| 119 |
+
def subscales(self) -> np.ndarray:
|
| 120 |
+
return (self.ls & 0x0F) | ((self.lm & 0x0F) << 4)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _factor_1d_lloyd(v: np.ndarray, w: np.ndarray, nmax: int, iters: int):
|
| 124 |
+
vmax = float(v.max())
|
| 125 |
+
if vmax < 1.0e-30:
|
| 126 |
+
return 0.0, np.zeros(v.size, dtype=np.uint8)
|
| 127 |
+
best_d = vmax / nmax
|
| 128 |
+
best_codes = np.clip(nearest_int(v / best_d), 0, nmax)
|
| 129 |
+
best_obj = float(np.sum(w * (v - best_d * best_codes) ** 2))
|
| 130 |
+
|
| 131 |
+
starts = [vmax / nmax]
|
| 132 |
+
init_codes = np.clip(nearest_int(v / starts[0]), 0, nmax)
|
| 133 |
+
den0 = float(np.sum(w * init_codes * init_codes))
|
| 134 |
+
if den0 > 1.0e-30:
|
| 135 |
+
starts.append(float(np.sum(w * v * init_codes) / den0))
|
| 136 |
+
|
| 137 |
+
seen: set[float] = set()
|
| 138 |
+
for d0 in starts:
|
| 139 |
+
if not np.isfinite(d0) or d0 <= 0.0:
|
| 140 |
+
continue
|
| 141 |
+
key = float(np.float32(d0))
|
| 142 |
+
if key in seen:
|
| 143 |
+
continue
|
| 144 |
+
seen.add(key)
|
| 145 |
+
d = float(d0)
|
| 146 |
+
codes = np.clip(nearest_int(v / d), 0, nmax)
|
| 147 |
+
for _ in range(iters):
|
| 148 |
+
den = float(np.sum(w * codes * codes))
|
| 149 |
+
if den < 1.0e-30:
|
| 150 |
+
break
|
| 151 |
+
dn = float(np.sum(w * v * codes) / den)
|
| 152 |
+
if dn <= 0.0:
|
| 153 |
+
break
|
| 154 |
+
conv = abs(dn - d) <= 1.0e-7 * abs(d) + 1.0e-30
|
| 155 |
+
d = dn
|
| 156 |
+
codes = np.clip(nearest_int(v / d), 0, nmax)
|
| 157 |
+
if conv:
|
| 158 |
+
break
|
| 159 |
+
obj = float(np.sum(w * (v - d * codes) ** 2))
|
| 160 |
+
if obj < best_obj:
|
| 161 |
+
best_obj = obj
|
| 162 |
+
best_d = d
|
| 163 |
+
best_codes = codes.copy()
|
| 164 |
+
return best_d, best_codes
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _refine_integer_codes(v, w, d, codes, nmax, sweeps=2):
|
| 168 |
+
v = np.asarray(v, dtype=np.float64)
|
| 169 |
+
w = np.asarray(w, dtype=np.float64)
|
| 170 |
+
codes = np.asarray(codes, dtype=np.int64).copy()
|
| 171 |
+
d = float(d)
|
| 172 |
+
if d <= 0.0:
|
| 173 |
+
return d, codes.astype(np.uint8)
|
| 174 |
+
obj = float(np.sum(w * (v - d * codes) ** 2))
|
| 175 |
+
options = np.arange(nmax + 1, dtype=np.int64)
|
| 176 |
+
for _ in range(sweeps):
|
| 177 |
+
improved = False
|
| 178 |
+
for i in range(v.size):
|
| 179 |
+
wi, vi, li = w[i], v[i], codes[i]
|
| 180 |
+
sum_lv = float(np.sum(w * v * codes))
|
| 181 |
+
sum_l2 = float(np.sum(w * codes * codes))
|
| 182 |
+
slv = sum_lv - wi * vi * li + wi * vi * options
|
| 183 |
+
sl2 = sum_l2 - wi * li * li + wi * options * options
|
| 184 |
+
valid = sl2 > 1.0e-30
|
| 185 |
+
if not np.any(valid):
|
| 186 |
+
continue
|
| 187 |
+
dd = np.where(valid, slv / np.where(valid, sl2, 1.0), 0.0)
|
| 188 |
+
trial = codes[None, :].repeat(options.size, axis=0)
|
| 189 |
+
trial[:, i] = options
|
| 190 |
+
e = v[None, :] - dd[:, None] * trial.astype(np.float64)
|
| 191 |
+
scores = np.sum(w[None, :] * e * e, axis=1)
|
| 192 |
+
scores = np.where(valid & (dd > 0.0), scores, np.inf)
|
| 193 |
+
k = int(np.argmin(scores))
|
| 194 |
+
if scores[k] < obj - 1.0e-18 and options[k] != li:
|
| 195 |
+
obj = float(scores[k])
|
| 196 |
+
d = float(dd[k])
|
| 197 |
+
codes[i] = options[k]
|
| 198 |
+
improved = True
|
| 199 |
+
if not improved:
|
| 200 |
+
break
|
| 201 |
+
return d, codes.astype(np.uint8)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def factor_1d(v, w, nmax: int = 15, iters: int = 8, refine: bool = True):
|
| 205 |
+
"""Weighted 1-D factorisation ``v ~= d * L`` with ``L in [0, nmax]``."""
|
| 206 |
+
v = np.maximum(np.asarray(v, dtype=np.float64), 0.0)
|
| 207 |
+
w = np.maximum(np.asarray(w, dtype=np.float64), 0.0)
|
| 208 |
+
d, codes = _factor_1d_lloyd(v, w, nmax, iters)
|
| 209 |
+
if refine and d > 0.0:
|
| 210 |
+
d, codes = _refine_integer_codes(v, w, d, codes, nmax)
|
| 211 |
+
return d, codes
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _range_init(x, w, cfg: HPCConfig):
|
| 215 |
+
x = np.asarray(x, dtype=np.float64)
|
| 216 |
+
w = np.asarray(w, dtype=np.float64)
|
| 217 |
+
blocks = x.reshape(NS, 16)
|
| 218 |
+
wblocks = w.reshape(NS, 16)
|
| 219 |
+
s = np.zeros(NS)
|
| 220 |
+
t = np.zeros(NS)
|
| 221 |
+
codes = np.zeros(QK_K, dtype=np.uint8)
|
| 222 |
+
for j in range(NS):
|
| 223 |
+
sub = blocks[j]
|
| 224 |
+
xmin = min(float(sub.min()), 0.0)
|
| 225 |
+
xmax = float(sub.max())
|
| 226 |
+
span = xmax - xmin
|
| 227 |
+
sj = span / 3.0 if span > 1.0e-30 else 0.0
|
| 228 |
+
tj = -xmin
|
| 229 |
+
s[j] = sj
|
| 230 |
+
t[j] = tj
|
| 231 |
+
if sj > 1.0e-30:
|
| 232 |
+
codes[16 * j : 16 * (j + 1)] = np.clip(
|
| 233 |
+
nearest_int((sub + tj) / sj), 0, 3
|
| 234 |
+
).astype(np.uint8)
|
| 235 |
+
sw = wblocks.sum(axis=1) if w is not None else np.full(NS, 16.0)
|
| 236 |
+
d, ls = factor_1d(s, sw, 15, cfg.factor_iters, cfg.factor_refine_1d)
|
| 237 |
+
m, lm = factor_1d(t, sw, 15, cfg.factor_iters, cfg.factor_refine_1d)
|
| 238 |
+
return d, m, ls, lm, codes
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _seeded_init(x, scales, mins, d_seed, m_seed):
|
| 242 |
+
d = float(d_seed)
|
| 243 |
+
m = float(m_seed)
|
| 244 |
+
ls, lm = derive_subscales(scales, mins, d, m)
|
| 245 |
+
codes = codes_from_params(x, d, m, ls, lm)
|
| 246 |
+
return d, m, ls, lm, codes
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def _ls_init(x, w, cfg: HPCConfig):
|
| 250 |
+
x = np.asarray(x, dtype=np.float64)
|
| 251 |
+
w = np.asarray(w, dtype=np.float64)
|
| 252 |
+
blocks = x.reshape(NS, 16)
|
| 253 |
+
codes = np.zeros(QK_K, dtype=np.uint8)
|
| 254 |
+
for j in range(NS):
|
| 255 |
+
sub = blocks[j]
|
| 256 |
+
xmin = min(float(sub.min()), 0.0)
|
| 257 |
+
xmax = float(sub.max())
|
| 258 |
+
span = xmax - xmin
|
| 259 |
+
if span > 1.0e-30:
|
| 260 |
+
codes[16 * j : 16 * (j + 1)] = np.clip(
|
| 261 |
+
nearest_int((sub - xmin) * 3.0 / span), 0, 3
|
| 262 |
+
).astype(np.uint8)
|
| 263 |
+
s, t = _sub_ls(x, w, codes)
|
| 264 |
+
sw = w.reshape(NS, 16).sum(axis=1)
|
| 265 |
+
d, ls = factor_1d(s, sw, 15, cfg.factor_iters, cfg.factor_refine_1d)
|
| 266 |
+
m, lm = factor_1d(t, sw, 15, cfg.factor_iters, cfg.factor_refine_1d)
|
| 267 |
+
codes = codes_from_params(x, d, m, ls, lm)
|
| 268 |
+
return d, m, ls, lm, codes
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def _sub_ls(x, w, codes):
|
| 272 |
+
x = np.asarray(x, dtype=np.float64)
|
| 273 |
+
w = np.asarray(w, dtype=np.float64)
|
| 274 |
+
q = np.asarray(codes, dtype=np.float64).reshape(NS, 16)
|
| 275 |
+
xb = x.reshape(NS, 16)
|
| 276 |
+
wb = w.reshape(NS, 16)
|
| 277 |
+
s = np.zeros(NS)
|
| 278 |
+
t = np.zeros(NS)
|
| 279 |
+
for j in range(NS):
|
| 280 |
+
ww = wb[j]
|
| 281 |
+
qq = q[j]
|
| 282 |
+
xx = xb[j]
|
| 283 |
+
Sqq = float(np.sum(ww * qq * qq))
|
| 284 |
+
Swq = float(np.sum(ww * qq))
|
| 285 |
+
Sw = float(np.sum(ww))
|
| 286 |
+
Sqx = float(np.sum(ww * qq * xx))
|
| 287 |
+
Sx = float(np.sum(ww * xx))
|
| 288 |
+
det = Sqq * Sw - Swq * Swq
|
| 289 |
+
if det > 1.0e-30:
|
| 290 |
+
sj = (Sqx * Sw - Swq * Sx) / det
|
| 291 |
+
tj = (sj * Swq - Sx) / Sw
|
| 292 |
+
else:
|
| 293 |
+
sj = 0.0
|
| 294 |
+
tj = 0.0
|
| 295 |
+
if sj < 0.0:
|
| 296 |
+
sj = 0.0
|
| 297 |
+
if tj < 0.0:
|
| 298 |
+
tj = 0.0
|
| 299 |
+
sj = Sqx / Sqq if Sqq > 1.0e-30 else 0.0
|
| 300 |
+
if sj < 0.0:
|
| 301 |
+
sj = 0.0
|
| 302 |
+
s[j] = sj
|
| 303 |
+
t[j] = tj
|
| 304 |
+
return s, t
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def _solve(
|
| 308 |
+
x,
|
| 309 |
+
w,
|
| 310 |
+
cfg: HPCConfig,
|
| 311 |
+
d,
|
| 312 |
+
m,
|
| 313 |
+
ls,
|
| 314 |
+
lm,
|
| 315 |
+
codes,
|
| 316 |
+
pin: int,
|
| 317 |
+
use_spectral: bool,
|
| 318 |
+
max_iter: int,
|
| 319 |
+
dc_carry: float,
|
| 320 |
+
):
|
| 321 |
+
x = np.asarray(x, dtype=np.float32)
|
| 322 |
+
w = np.asarray(w, dtype=np.float32)
|
| 323 |
+
ls = np.asarray(ls, dtype=np.uint8).copy()
|
| 324 |
+
lm = np.asarray(lm, dtype=np.uint8).copy()
|
| 325 |
+
codes = np.asarray(codes, dtype=np.uint8).copy()
|
| 326 |
+
d = float(d)
|
| 327 |
+
m = float(m)
|
| 328 |
+
|
| 329 |
+
obj, _ = objective(x, w, d, m, ls, lm, codes, cfg, dc_carry)
|
| 330 |
+
best = (obj, d, m, ls.copy(), lm.copy(), codes.copy())
|
| 331 |
+
stall = 0
|
| 332 |
+
iters = 0
|
| 333 |
+
for it in range(max(1, max_iter)):
|
| 334 |
+
iters = it + 1
|
| 335 |
+
codes = codes_from_params(x, d, m, ls, lm)
|
| 336 |
+
s, t = _sub_ls(x, w, codes)
|
| 337 |
+
q = codes.astype(np.float64).reshape(NS, 16)
|
| 338 |
+
wb = w.reshape(NS, 16)
|
| 339 |
+
ws = np.sum(wb * q * q, axis=1)
|
| 340 |
+
wt = np.sum(wb, axis=1)
|
| 341 |
+
|
| 342 |
+
if pin & 1:
|
| 343 |
+
if d > 1.0e-30:
|
| 344 |
+
ls = np.clip(nearest_int(s / d), 0, 15).astype(np.uint8)
|
| 345 |
+
else:
|
| 346 |
+
ls = np.zeros(NS, dtype=np.uint8)
|
| 347 |
+
else:
|
| 348 |
+
d_new, ls_new = factor_1d(
|
| 349 |
+
s, ws, 15, cfg.factor_iters, cfg.factor_refine_1d
|
| 350 |
+
)
|
| 351 |
+
d, ls = float(d_new), ls_new
|
| 352 |
+
|
| 353 |
+
if pin & 2:
|
| 354 |
+
if m > 1.0e-30:
|
| 355 |
+
lm = np.clip(nearest_int(t / m), 0, 15).astype(np.uint8)
|
| 356 |
+
else:
|
| 357 |
+
lm = np.zeros(NS, dtype=np.uint8)
|
| 358 |
+
else:
|
| 359 |
+
m_new, lm_new = factor_1d(
|
| 360 |
+
t, wt, 15, cfg.factor_iters, cfg.factor_refine_1d
|
| 361 |
+
)
|
| 362 |
+
m, lm = float(m_new), lm_new
|
| 363 |
+
|
| 364 |
+
codes = codes_from_params(x, d, m, ls, lm)
|
| 365 |
+
cur, _ = objective(x, w, d, m, ls, lm, codes, cfg, dc_carry)
|
| 366 |
+
if cur < best[0] - 1.0e-15:
|
| 367 |
+
best = (cur, d, m, ls.copy(), lm.copy(), codes.copy())
|
| 368 |
+
stall = 0
|
| 369 |
+
else:
|
| 370 |
+
stall += 1
|
| 371 |
+
if stall >= 2:
|
| 372 |
+
break
|
| 373 |
+
|
| 374 |
+
obj, d, m, ls, lm, codes = best
|
| 375 |
+
return d, m, ls, lm, codes, iters
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def _emit_polish(x, w, cfg, d, m, ls, lm, codes, dc_carry):
|
| 379 |
+
d16 = np.uint16(to_fp16_bits(np.float32(d)))
|
| 380 |
+
m16 = np.uint16(to_fp16_bits(np.float32(m)))
|
| 381 |
+
d = float(from_fp16_bits(d16))
|
| 382 |
+
m = float(from_fp16_bits(m16))
|
| 383 |
+
codes = codes_from_params(x, d, m, ls, lm)
|
| 384 |
+
obj, sse = objective(x, w, d, m, ls, lm, codes, cfg, dc_carry)
|
| 385 |
+
best = (obj, sse, d, m, d16, m16, ls.copy(), lm.copy(), codes.copy())
|
| 386 |
+
|
| 387 |
+
if cfg.factor_exact_emit and cfg.factor_ulp_radius > 0:
|
| 388 |
+
d_choices = fp16_ulp_neighbors(d16, cfg.factor_ulp_radius)
|
| 389 |
+
m_choices = fp16_ulp_neighbors(m16, cfg.factor_ulp_radius)
|
| 390 |
+
for db in d_choices:
|
| 391 |
+
dd = float(from_fp16_bits(db))
|
| 392 |
+
for mb in m_choices:
|
| 393 |
+
mm = float(from_fp16_bits(mb))
|
| 394 |
+
c = codes_from_params(x, dd, mm, ls, lm)
|
| 395 |
+
o, s = objective(x, w, dd, mm, ls, lm, c, cfg, dc_carry)
|
| 396 |
+
if o < best[0] - 1.0e-15:
|
| 397 |
+
best = (o, s, dd, mm, np.uint16(db), np.uint16(mb), ls.copy(), lm.copy(), c)
|
| 398 |
+
obj, sse, d, m, d16, m16, ls, lm, codes = best
|
| 399 |
+
|
| 400 |
+
if cfg.factor_spectral_refine:
|
| 401 |
+
refined = refine_codes_hadamard(
|
| 402 |
+
x, w, d, m, ls, lm, codes, cfg, dc_carry
|
| 403 |
+
)
|
| 404 |
+
o, s = objective(x, w, d, m, ls, lm, refined, cfg, dc_carry)
|
| 405 |
+
if o < obj - 1.0e-15:
|
| 406 |
+
obj, sse, codes = o, s, refined
|
| 407 |
+
return d, m, d16, m16, ls, lm, codes, sse, obj
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
def factor_q2k_block(
|
| 411 |
+
x,
|
| 412 |
+
w=None,
|
| 413 |
+
cfg: HPCConfig | None = None,
|
| 414 |
+
init_s=None,
|
| 415 |
+
init_t=None,
|
| 416 |
+
d_seed: float = 0.0,
|
| 417 |
+
m_seed: float = 0.0,
|
| 418 |
+
pin: int = 0,
|
| 419 |
+
use_spectral: bool | None = None,
|
| 420 |
+
max_iter: int | None = None,
|
| 421 |
+
starts: int | None = None,
|
| 422 |
+
dc_carry: float = 0.0,
|
| 423 |
+
) -> FactorResult:
|
| 424 |
+
cfg = (cfg or HPCConfig()).validate()
|
| 425 |
+
x = np.asarray(x, dtype=np.float32).reshape(QK_K)
|
| 426 |
+
w = (
|
| 427 |
+
np.ones(QK_K, dtype=np.float32)
|
| 428 |
+
if w is None
|
| 429 |
+
else np.asarray(w, dtype=np.float32).reshape(QK_K)
|
| 430 |
+
)
|
| 431 |
+
use_spectral = cfg.use_spectral if use_spectral is None else use_spectral
|
| 432 |
+
max_iter = cfg.factor_standalone_iters if max_iter is None else max_iter
|
| 433 |
+
starts = cfg.factor_starts if starts is None else starts
|
| 434 |
+
|
| 435 |
+
candidates = []
|
| 436 |
+
if init_s is not None and init_t is not None and d_seed > 1.0e-30 and m_seed > 1.0e-30:
|
| 437 |
+
d, m, ls, lm, codes = _seeded_init(x, init_s, init_t, d_seed, m_seed)
|
| 438 |
+
candidates.append((d, m, ls, lm, codes))
|
| 439 |
+
else:
|
| 440 |
+
d, m, ls, lm, codes = _range_init(x, w, cfg)
|
| 441 |
+
if d_seed > 1.0e-30:
|
| 442 |
+
d = float(d_seed)
|
| 443 |
+
if init_s is not None:
|
| 444 |
+
ls = np.clip(nearest_int(np.asarray(init_s) / d), 0, 15).astype(np.uint8)
|
| 445 |
+
if m_seed > 1.0e-30:
|
| 446 |
+
m = float(m_seed)
|
| 447 |
+
if init_t is not None:
|
| 448 |
+
lm = np.clip(nearest_int(np.asarray(init_t) / m), 0, 15).astype(np.uint8)
|
| 449 |
+
candidates.append((d, m, ls, lm, codes))
|
| 450 |
+
if starts > 1:
|
| 451 |
+
candidates.append(_ls_init(x, w, cfg))
|
| 452 |
+
|
| 453 |
+
best = None
|
| 454 |
+
total_iters = 0
|
| 455 |
+
for d, m, ls, lm, codes in candidates:
|
| 456 |
+
d, m, ls, lm, codes, it = _solve(
|
| 457 |
+
x, w, cfg, d, m, ls, lm, codes, pin, use_spectral, max_iter, dc_carry
|
| 458 |
+
)
|
| 459 |
+
total_iters += it
|
| 460 |
+
d, m, d16, m16, ls, lm, codes, sse, obj = _emit_polish(
|
| 461 |
+
x, w, cfg, d, m, ls, lm, codes, dc_carry
|
| 462 |
+
)
|
| 463 |
+
if best is None or obj < best.objective - 1.0e-15:
|
| 464 |
+
best = FactorResult(
|
| 465 |
+
d=np.float32(d),
|
| 466 |
+
dmin=np.float32(m),
|
| 467 |
+
d16=np.uint16(d16),
|
| 468 |
+
m16=np.uint16(m16),
|
| 469 |
+
ls=ls.astype(np.uint8),
|
| 470 |
+
lm=lm.astype(np.uint8),
|
| 471 |
+
codes=codes.astype(np.uint8),
|
| 472 |
+
sse=float(sse),
|
| 473 |
+
objective=float(obj),
|
| 474 |
+
iterations=total_iters,
|
| 475 |
+
starts=len(candidates),
|
| 476 |
+
)
|
| 477 |
+
return best
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def factor_candidates(
|
| 481 |
+
x,
|
| 482 |
+
w,
|
| 483 |
+
scales,
|
| 484 |
+
mins,
|
| 485 |
+
base_d,
|
| 486 |
+
base_m,
|
| 487 |
+
cfg: HPCConfig,
|
| 488 |
+
dc_carry: float = 0.0,
|
| 489 |
+
) -> np.ndarray:
|
| 490 |
+
"""Factored candidate field indexed exactly like the C 24x24 lattice."""
|
| 491 |
+
out = np.full(TOTAL_CANDIDATES, np.inf, dtype=np.float32)
|
| 492 |
+
nd = int(cfg.n_cand_d)
|
| 493 |
+
nm = int(cfg.n_cand_m)
|
| 494 |
+
for a in range(nd):
|
| 495 |
+
di = a if nd == N_CAND_D else (a * N_CAND_D) // nd
|
| 496 |
+
ds = float(round_fp16(np.float32(base_d) * MULTS_D[di]))
|
| 497 |
+
for b in range(nm):
|
| 498 |
+
mi = b if nm == N_CAND_M else (b * N_CAND_M) // nm
|
| 499 |
+
ms = float(round_fp16(np.float32(base_m) * MULTS_M[mi]))
|
| 500 |
+
res = factor_q2k_block(
|
| 501 |
+
x,
|
| 502 |
+
w,
|
| 503 |
+
cfg,
|
| 504 |
+
init_s=scales,
|
| 505 |
+
init_t=mins,
|
| 506 |
+
d_seed=ds,
|
| 507 |
+
m_seed=ms,
|
| 508 |
+
pin=cfg.factor_pin,
|
| 509 |
+
use_spectral=cfg.use_spectral,
|
| 510 |
+
max_iter=cfg.factor_iters,
|
| 511 |
+
starts=1,
|
| 512 |
+
dc_carry=dc_carry,
|
| 513 |
+
)
|
| 514 |
+
if nd == N_CAND_D and nm == N_CAND_M:
|
| 515 |
+
out[di * N_CAND_M + mi] = np.float32(res.objective)
|
| 516 |
+
else:
|
| 517 |
+
cb = di >> 2
|
| 518 |
+
fb = mi >> 2
|
| 519 |
+
for aa in range(4):
|
| 520 |
+
for bb in range(4):
|
| 521 |
+
idx = (cb * 4 + aa) * N_CAND_M + fb * 4 + bb
|
| 522 |
+
if res.objective < out[idx]:
|
| 523 |
+
out[idx] = np.float32(res.objective)
|
| 524 |
+
return out
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def factor_only_solve(
|
| 528 |
+
x, w, cfg: HPCConfig, dc_carry: float = 0.0
|
| 529 |
+
) -> FactorResult:
|
| 530 |
+
return factor_q2k_block(
|
| 531 |
+
x,
|
| 532 |
+
w,
|
| 533 |
+
cfg,
|
| 534 |
+
pin=0,
|
| 535 |
+
use_spectral=cfg.use_spectral,
|
| 536 |
+
max_iter=cfg.factor_standalone_iters,
|
| 537 |
+
starts=cfg.factor_starts,
|
| 538 |
+
dc_carry=dc_carry,
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
def candidate_pair(base_d, base_m, cidx: int) -> tuple[np.uint16, np.uint16]:
|
| 543 |
+
di = int(cidx) // N_CAND_M
|
| 544 |
+
mi = int(cidx) % N_CAND_M
|
| 545 |
+
d16 = np.uint16(to_fp16_bits(np.float32(base_d) * MULTS_D[di]))
|
| 546 |
+
m16 = np.uint16(to_fp16_bits(np.float32(base_m) * MULTS_M[mi]))
|
| 547 |
+
return d16, m16
|
fp16.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bit-exact GGML numerical primitives.
|
| 2 |
+
|
| 3 |
+
The C engine relies on two non-obvious primitives:
|
| 4 |
+
|
| 5 |
+
* ``gguf_nearest_int`` implements round-to-nearest-even with the classic
|
| 6 |
+
``float + 1.5*2**23`` mantissa trick rather than ``rint``. The trick is
|
| 7 |
+
reproduced here so that every quantized code is bit-identical to the
|
| 8 |
+
reference implementation.
|
| 9 |
+
* ``gguf_fp32_to_fp16`` / ``gguf_fp16_to_fp32`` are the emitted parameter
|
| 10 |
+
domain. Every solver in V2 may optimise in float32, but it reports and
|
| 11 |
+
stores only fp16-rounded values.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
+
_MAGIC = np.float32(12582912.0)
|
| 19 |
+
_F16_MAX_BITS = 0x7BFF
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def nearest_int(value) -> np.ndarray:
|
| 23 |
+
"""GGML-compatible nearest-integer rounding (ties to even)."""
|
| 24 |
+
arr = np.asarray(value, dtype=np.float32)
|
| 25 |
+
shifted = arr + _MAGIC
|
| 26 |
+
bits = shifted.view(np.int32)
|
| 27 |
+
return ((bits & 0x007FFFFF) - 0x00400000).astype(np.int64)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def to_fp16_bits(value) -> np.ndarray:
|
| 31 |
+
arr = np.asarray(value, dtype=np.float32)
|
| 32 |
+
return arr.astype(np.float16).view(np.uint16).astype(np.uint16)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def from_fp16_bits(bits) -> np.ndarray:
|
| 36 |
+
arr = np.asarray(bits, dtype=np.uint16)
|
| 37 |
+
return arr.view(np.float16).astype(np.float32)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def round_fp16(value) -> np.ndarray:
|
| 41 |
+
return from_fp16_bits(to_fp16_bits(value))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def fp16_ulp_neighbors(bits, radius: int) -> np.ndarray:
|
| 45 |
+
"""All non-negative finite fp16 bit patterns within ``radius`` ULPs."""
|
| 46 |
+
if radius < 0:
|
| 47 |
+
raise ValueError("radius must be non-negative")
|
| 48 |
+
b = int(bits)
|
| 49 |
+
lo = max(0, b - radius)
|
| 50 |
+
hi = min(_F16_MAX_BITS, b + radius)
|
| 51 |
+
return np.arange(lo, hi + 1, dtype=np.uint16)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def epsilon_for(scale: np.ndarray, relative: float = 1.0e-7) -> np.ndarray:
|
| 55 |
+
return relative * np.abs(scale) + np.float32(1.0e-30)
|
pipeline.py
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""End-to-end Q2_K pipeline preserving the HPC machinery.
|
| 2 |
+
|
| 3 |
+
Pipeline stages (mirroring the C engine, with V2 fixes):
|
| 4 |
+
|
| 5 |
+
1. kurtosis-gated outlier clamp and batched GGML seeds;
|
| 6 |
+
2. vectorised WLS superblock solve;
|
| 7 |
+
3. factored candidate field + standalone factored floor;
|
| 8 |
+
4. sequential Sieve over coarse/fine symbolic states;
|
| 9 |
+
5. 36-state Viterbi sequence optimisation;
|
| 10 |
+
6. candidate-aware factor consistency pass;
|
| 11 |
+
7. rolling DC residual carry;
|
| 12 |
+
8. sub-block Sieve assembly, LS refit, fp16-ULP polish, spectral shaping;
|
| 13 |
+
9. objective-consistent local-safety floors and DC nudge.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
from math import ceil
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
from .config import HPCConfig
|
| 24 |
+
from .factored import (
|
| 25 |
+
CAND_TO_QUHIT,
|
| 26 |
+
N_CAND_M,
|
| 27 |
+
TOTAL_CANDIDATES,
|
| 28 |
+
FactorResult,
|
| 29 |
+
candidate_pair,
|
| 30 |
+
factor_candidates,
|
| 31 |
+
factor_only_solve,
|
| 32 |
+
factor_q2k_block,
|
| 33 |
+
)
|
| 34 |
+
from .fp16 import from_fp16_bits, nearest_int, round_fp16, to_fp16_bits
|
| 35 |
+
from .q2k import (
|
| 36 |
+
NS,
|
| 37 |
+
QK_K,
|
| 38 |
+
Q2KBlock,
|
| 39 |
+
codes_from_params,
|
| 40 |
+
codes_from_params_batch,
|
| 41 |
+
dequantize_params,
|
| 42 |
+
dequantize_params_batch,
|
| 43 |
+
derive_subscales,
|
| 44 |
+
make_block,
|
| 45 |
+
pack_codes,
|
| 46 |
+
pack_scales,
|
| 47 |
+
unpack_codes,
|
| 48 |
+
weighted_sse,
|
| 49 |
+
weighted_sse_batch,
|
| 50 |
+
)
|
| 51 |
+
from .seeds import build_seeds, seed_clamp_value
|
| 52 |
+
from .sieve import block_chain_edges, sieve_measure_chain, subblock_chain_edges
|
| 53 |
+
from .spectral import dc_nudge_codes, objective, refine_codes_hadamard
|
| 54 |
+
from .viterbi import robust_temperature, viterbi_select
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@dataclass(slots=True)
|
| 58 |
+
class TensorStats:
|
| 59 |
+
n_blocks: int
|
| 60 |
+
total_sse: float
|
| 61 |
+
weighted_sse: float
|
| 62 |
+
rmse: float
|
| 63 |
+
sigma: float
|
| 64 |
+
rmse_over_sigma: float
|
| 65 |
+
temperature: float
|
| 66 |
+
factor_floor_used: int
|
| 67 |
+
candidate_floor_used: int
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass(slots=True)
|
| 71 |
+
class QuantizedTensor:
|
| 72 |
+
blocks: list[Q2KBlock]
|
| 73 |
+
scales: np.ndarray
|
| 74 |
+
qs: np.ndarray
|
| 75 |
+
dm: np.ndarray
|
| 76 |
+
best_candidate: np.ndarray
|
| 77 |
+
factor_objectives: np.ndarray
|
| 78 |
+
config: HPCConfig
|
| 79 |
+
stats: TensorStats
|
| 80 |
+
|
| 81 |
+
def dequantize(self) -> np.ndarray:
|
| 82 |
+
return np.concatenate([b.dequantize() for b in self.blocks])
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _phase2_wls(X, W, seeds, clamp_val):
|
| 86 |
+
n = X.shape[0]
|
| 87 |
+
clipped = np.clip(X, -clamp_val, clamp_val).astype(np.float32)
|
| 88 |
+
base_d = seeds["d"].astype(np.float64).copy()
|
| 89 |
+
base_m = seeds["dmin"].astype(np.float64).copy()
|
| 90 |
+
ls = seeds["ls"].copy()
|
| 91 |
+
lm = seeds["lm"].copy()
|
| 92 |
+
scales = seeds["scales"].astype(np.float64)
|
| 93 |
+
mins = seeds["mins"].astype(np.float64)
|
| 94 |
+
ww = np.ones_like(X, dtype=np.float64) if W is None else W.astype(np.float64)
|
| 95 |
+
|
| 96 |
+
for _ in range(5):
|
| 97 |
+
codes = codes_from_params_batch(clipped, base_d, base_m, ls, lm).astype(np.float64)
|
| 98 |
+
A = np.repeat(ls.astype(np.float64), 16, axis=1) * codes
|
| 99 |
+
B = np.repeat(lm.astype(np.float64), 16, axis=1)
|
| 100 |
+
Saa = np.sum(ww * A * A, axis=1)
|
| 101 |
+
Sab = np.sum(ww * A * B, axis=1)
|
| 102 |
+
Sbb = np.sum(ww * B * B, axis=1)
|
| 103 |
+
Sxa = np.sum(ww * clipped.astype(np.float64) * A, axis=1)
|
| 104 |
+
Sxb = np.sum(ww * clipped.astype(np.float64) * B, axis=1)
|
| 105 |
+
det = Saa * Sbb - Sab * Sab
|
| 106 |
+
valid = det > 1.0e-30
|
| 107 |
+
safe_det = np.where(valid, det, 1.0)
|
| 108 |
+
d_new = (Sbb * Sxa - Sab * Sxb) / safe_det
|
| 109 |
+
m_new = (Sab * Sxa - Saa * Sxb) / safe_det
|
| 110 |
+
upd_d = valid & (d_new > 0.0) & (d_new < 4.0 * (seeds["d"] + 1.0e-10))
|
| 111 |
+
upd_m = valid & (m_new > 0.0) & (m_new < 4.0 * (seeds["dmin"] + 1.0e-10))
|
| 112 |
+
base_d = np.where(upd_d, round_fp16(d_new.astype(np.float32)).astype(np.float64), base_d)
|
| 113 |
+
base_m = np.where(upd_m, round_fp16(m_new.astype(np.float32)).astype(np.float64), base_m)
|
| 114 |
+
|
| 115 |
+
safe_d = np.where(base_d > 1.0e-15, base_d, 1.0)
|
| 116 |
+
safe_m = np.where(base_m > 1.0e-15, base_m, 1.0)
|
| 117 |
+
ls = np.where(
|
| 118 |
+
base_d[:, None] > 1.0e-15,
|
| 119 |
+
np.clip(nearest_int(scales / safe_d[:, None]), 0, 15),
|
| 120 |
+
0,
|
| 121 |
+
).astype(np.uint8)
|
| 122 |
+
lm = np.where(
|
| 123 |
+
base_m[:, None] > 1.0e-15,
|
| 124 |
+
np.clip(nearest_int(mins / safe_m[:, None]), 0, 15),
|
| 125 |
+
0,
|
| 126 |
+
).astype(np.uint8)
|
| 127 |
+
|
| 128 |
+
return base_d.astype(np.float32), base_m.astype(np.float32)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _bin_geometry():
|
| 132 |
+
di = np.arange(TOTAL_CANDIDATES) // N_CAND_M
|
| 133 |
+
mi = np.arange(TOTAL_CANDIDATES) % N_CAND_M
|
| 134 |
+
return CAND_TO_QUHIT[di], CAND_TO_QUHIT[mi]
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _sieve_marginals(candidate_errors, graph_blocks, stride, temperature, cfg):
|
| 138 |
+
qi_d, qi_m = _bin_geometry()
|
| 139 |
+
coarse = np.zeros((graph_blocks, 6), dtype=np.float64)
|
| 140 |
+
fine = np.zeros((graph_blocks, 6), dtype=np.float64)
|
| 141 |
+
n_blocks = candidate_errors.shape[0]
|
| 142 |
+
for gi in range(graph_blocks):
|
| 143 |
+
start = gi * stride
|
| 144 |
+
stop = min((gi + 1) * stride, n_blocks)
|
| 145 |
+
group = candidate_errors[start:stop].astype(np.float64)
|
| 146 |
+
finite = np.isfinite(group)
|
| 147 |
+
if np.any(finite):
|
| 148 |
+
agg = np.where(finite, group, np.nan).mean(axis=0)
|
| 149 |
+
valid = np.isfinite(agg)
|
| 150 |
+
mn = float(np.min(agg[valid])) if np.any(valid) else 0.0
|
| 151 |
+
coeff = np.where(
|
| 152 |
+
valid,
|
| 153 |
+
np.exp(-(agg - mn) / (2.0 * max(temperature, 1.0e-12))),
|
| 154 |
+
0.0,
|
| 155 |
+
)
|
| 156 |
+
else:
|
| 157 |
+
coeff = np.zeros(TOTAL_CANDIDATES, dtype=np.float64)
|
| 158 |
+
for v in range(6):
|
| 159 |
+
coarse[gi, v] = float(np.sum(coeff[qi_d == v]))
|
| 160 |
+
fine[gi, v] = float(np.sum(coeff[qi_m == v]))
|
| 161 |
+
cn = float(np.linalg.norm(coarse[gi]))
|
| 162 |
+
fn = float(np.linalg.norm(fine[gi]))
|
| 163 |
+
if cn > 1.0e-30:
|
| 164 |
+
coarse[gi] /= cn
|
| 165 |
+
if fn > 1.0e-30:
|
| 166 |
+
fine[gi] /= fn
|
| 167 |
+
|
| 168 |
+
amps = np.zeros((2 * graph_blocks, 6), dtype=np.float64)
|
| 169 |
+
amps[0::2] = coarse
|
| 170 |
+
amps[1::2] = fine
|
| 171 |
+
marg, _ = sieve_measure_chain(
|
| 172 |
+
amps,
|
| 173 |
+
block_chain_edges(graph_blocks),
|
| 174 |
+
same_penalty=cfg.same_penalty,
|
| 175 |
+
log_slack=cfg.log_slack,
|
| 176 |
+
deterministic=True,
|
| 177 |
+
)
|
| 178 |
+
return marg[0::2], marg[1::2]
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _phase3_select(candidate_errors, cfg):
|
| 182 |
+
n_blocks = candidate_errors.shape[0]
|
| 183 |
+
if cfg.optimizer != "hpc" or n_blocks < 2:
|
| 184 |
+
best = np.argmin(candidate_errors, axis=1).astype(np.int64)
|
| 185 |
+
return best, 0.0, np.zeros(n_blocks, dtype=np.int64)
|
| 186 |
+
|
| 187 |
+
stride = max(1, ceil(n_blocks / cfg.graph_max_blocks))
|
| 188 |
+
graph_blocks = ceil(n_blocks / stride)
|
| 189 |
+
temperature = robust_temperature(candidate_errors, cfg)
|
| 190 |
+
coarse_marg, fine_marg = _sieve_marginals(
|
| 191 |
+
candidate_errors, graph_blocks, stride, temperature, cfg
|
| 192 |
+
)
|
| 193 |
+
best, path = viterbi_select(
|
| 194 |
+
candidate_errors,
|
| 195 |
+
coarse_marg,
|
| 196 |
+
fine_marg,
|
| 197 |
+
temperature,
|
| 198 |
+
stride,
|
| 199 |
+
graph_blocks,
|
| 200 |
+
cfg,
|
| 201 |
+
)
|
| 202 |
+
return best, temperature, path
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _compute_dc_carry(X, seeds, best, base_d, base_m, cfg, row_width):
|
| 206 |
+
n = X.shape[0]
|
| 207 |
+
carry = np.zeros(n, dtype=np.float32)
|
| 208 |
+
blocks_per_row = row_width // QK_K if row_width and row_width % QK_K == 0 else 0
|
| 209 |
+
rolling = 0.0
|
| 210 |
+
for i in range(n):
|
| 211 |
+
if blocks_per_row > 0 and i % blocks_per_row == 0:
|
| 212 |
+
rolling = 0.0
|
| 213 |
+
d16, m16 = candidate_pair(base_d[i], base_m[i], best[i])
|
| 214 |
+
d = float(from_fp16_bits(d16))
|
| 215 |
+
m = float(from_fp16_bits(m16))
|
| 216 |
+
ls, lm = derive_subscales(seeds["scales"][i], seeds["mins"][i], d, m)
|
| 217 |
+
codes = codes_from_params(X[i], d, m, ls, lm)
|
| 218 |
+
xh = dequantize_params(d, m, ls, lm, codes)
|
| 219 |
+
carry[i] = np.float32(cfg.dc_decay * rolling)
|
| 220 |
+
dc_res = float(np.sum(X[i].astype(np.float64) - xh.astype(np.float64)))
|
| 221 |
+
rolling = carry[i] + dc_res if cfg.carry_cumulative else dc_res
|
| 222 |
+
return carry
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def _per_sub_top_states(x, w, d, m, k=6):
|
| 226 |
+
k = max(1, min(int(k), 6))
|
| 227 |
+
xb = np.asarray(x, dtype=np.float32).reshape(NS, 16)
|
| 228 |
+
if w is None:
|
| 229 |
+
wb = np.ones((NS, 16), dtype=np.float32)
|
| 230 |
+
else:
|
| 231 |
+
wb = np.asarray(w, dtype=np.float32).reshape(NS, 16)
|
| 232 |
+
state_ls = np.zeros((NS, k), dtype=np.uint8)
|
| 233 |
+
state_lm = np.zeros((NS, k), dtype=np.uint8)
|
| 234 |
+
state_err = np.full((NS, k), np.inf, dtype=np.float64)
|
| 235 |
+
ls_opts = np.arange(16, dtype=np.float32)[:, None]
|
| 236 |
+
lm_opts = np.arange(16, dtype=np.float32)[None, :]
|
| 237 |
+
d_sub = np.float32(d) * ls_opts
|
| 238 |
+
m_sub = np.float32(m) * lm_opts
|
| 239 |
+
safe = d_sub[:, :, None] >= np.float32(1.0e-15)
|
| 240 |
+
safe_d = np.where(safe, d_sub[:, :, None], np.float32(1.0))
|
| 241 |
+
for j in range(NS):
|
| 242 |
+
sub = xb[j]
|
| 243 |
+
wj = wb[j]
|
| 244 |
+
raw = (sub[None, None, :] + m_sub[:, :, None]) / safe_d
|
| 245 |
+
q = np.clip(nearest_int(raw), 0, 3).astype(np.float32)
|
| 246 |
+
q = np.where(safe, q, 0.0)
|
| 247 |
+
deq = d_sub[:, :, None] * q - m_sub[:, :, None]
|
| 248 |
+
err = np.sum(wj[None, None, :] * (sub[None, None, :] - deq) ** 2, axis=2)
|
| 249 |
+
flat = err.ravel()
|
| 250 |
+
order = np.argsort(flat, kind="stable")[:k]
|
| 251 |
+
state_err[j] = flat[order]
|
| 252 |
+
state_ls[j] = (order // 16).astype(np.uint8)
|
| 253 |
+
state_lm[j] = (order % 16).astype(np.uint8)
|
| 254 |
+
return state_ls, state_lm, state_err
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def _select_sub_states(state_ls, state_lm, state_err, cfg):
|
| 258 |
+
if not cfg.subblock_sieve:
|
| 259 |
+
return state_ls[:, 0].copy(), state_lm[:, 0].copy()
|
| 260 |
+
amps = np.zeros((NS, 6), dtype=np.float64)
|
| 261 |
+
for j in range(NS):
|
| 262 |
+
err0 = state_err[j, 0]
|
| 263 |
+
spread = float(state_err[j, 5] - state_err[j, 0])
|
| 264 |
+
temp = spread * 0.3 if spread > 1.0e-15 else 0.1
|
| 265 |
+
temp = max(temp, 1.0e-12)
|
| 266 |
+
amp = np.exp(-(state_err[j] - err0) / temp)
|
| 267 |
+
norm = float(np.linalg.norm(amp))
|
| 268 |
+
amps[j] = amp / norm if norm > 1.0e-30 else 1.0 / np.sqrt(6.0)
|
| 269 |
+
marg, _ = sieve_measure_chain(
|
| 270 |
+
amps,
|
| 271 |
+
subblock_chain_edges(NS),
|
| 272 |
+
same_penalty=cfg.same_penalty,
|
| 273 |
+
log_slack=cfg.log_slack,
|
| 274 |
+
deterministic=True,
|
| 275 |
+
)
|
| 276 |
+
pick = np.argmax(marg, axis=1)
|
| 277 |
+
ls = np.array([state_ls[j, pick[j]] for j in range(NS)], dtype=np.uint8)
|
| 278 |
+
lm = np.array([state_lm[j, pick[j]] for j in range(NS)], dtype=np.uint8)
|
| 279 |
+
return ls, lm
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def _ls_refit(x, w, d, m, ls, lm, codes, max_d, max_m):
|
| 283 |
+
x = np.asarray(x, dtype=np.float64)
|
| 284 |
+
w = np.ones(QK_K, dtype=np.float64) if w is None else np.asarray(w, dtype=np.float64)
|
| 285 |
+
A = np.repeat(np.asarray(ls, dtype=np.float64), 16) * np.asarray(codes, dtype=np.float64)
|
| 286 |
+
B = np.repeat(np.asarray(lm, dtype=np.float64), 16)
|
| 287 |
+
Saa = float(np.sum(w * A * A))
|
| 288 |
+
Sab = float(np.sum(w * A * B))
|
| 289 |
+
Sbb = float(np.sum(w * B * B))
|
| 290 |
+
Sxa = float(np.sum(w * x * A))
|
| 291 |
+
Sxb = float(np.sum(w * x * B))
|
| 292 |
+
det = Saa * Sbb - Sab * Sab
|
| 293 |
+
if det <= 1.0e-30:
|
| 294 |
+
return float(d), float(m)
|
| 295 |
+
d_new = (Sbb * Sxa - Sab * Sxb) / det
|
| 296 |
+
m_new = (Sab * Sxa - Saa * Sxb) / det
|
| 297 |
+
if d_new <= 0.0 or d_new >= 4.0 * (max_d + 1.0e-10):
|
| 298 |
+
d_new = d
|
| 299 |
+
if m_new <= 0.0 or m_new >= 4.0 * (max_m + 1.0e-10):
|
| 300 |
+
m_new = m
|
| 301 |
+
return float(round_fp16(np.float32(d_new))), float(round_fp16(np.float32(m_new)))
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def _ulp_polish(x, w, d, m, ls, lm, radius):
|
| 305 |
+
if radius <= 0:
|
| 306 |
+
codes = codes_from_params(x, d, m, ls, lm)
|
| 307 |
+
return float(d), float(m), codes, weighted_sse(x, w, d, m, ls, lm, codes)
|
| 308 |
+
d16 = np.uint16(to_fp16_bits(np.float32(d)))
|
| 309 |
+
m16 = np.uint16(to_fp16_bits(np.float32(m)))
|
| 310 |
+
best = None
|
| 311 |
+
for db in range(max(0, int(d16) - radius), min(0x7BFF, int(d16) + radius) + 1):
|
| 312 |
+
dd = float(from_fp16_bits(np.uint16(db)))
|
| 313 |
+
for mb in range(max(0, int(m16) - radius), min(0x7BFF, int(m16) + radius) + 1):
|
| 314 |
+
mm = float(from_fp16_bits(np.uint16(mb)))
|
| 315 |
+
c = codes_from_params(x, dd, mm, ls, lm)
|
| 316 |
+
s = weighted_sse(x, w, dd, mm, ls, lm, c)
|
| 317 |
+
if best is None or s < best[0] - 1.0e-15:
|
| 318 |
+
best = (s, dd, mm, c)
|
| 319 |
+
return best[1], best[2], best[3], best[0]
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def _per_sub_scan(x, w, d, m):
|
| 323 |
+
state_ls, state_lm, _ = _per_sub_top_states(x, w, d, m, k=1)
|
| 324 |
+
return state_ls[:, 0], state_lm[:, 0]
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def _assemble_block(x, w, seeds, base_d, base_m, cidx, dc_carry, cfg):
|
| 328 |
+
d16, m16 = candidate_pair(base_d, base_m, cidx)
|
| 329 |
+
d = float(from_fp16_bits(d16))
|
| 330 |
+
m = float(from_fp16_bits(m16))
|
| 331 |
+
ls, lm = derive_subscales(seeds["scales"], seeds["mins"], d, m)
|
| 332 |
+
max_d, max_m = d, m
|
| 333 |
+
prev = None
|
| 334 |
+
for _ in range(5):
|
| 335 |
+
s_ls, s_lm, s_err = _per_sub_top_states(x, w, d, m, cfg.subblock_states)
|
| 336 |
+
ls, lm = _select_sub_states(s_ls, s_lm, s_err, cfg)
|
| 337 |
+
codes = codes_from_params(x, d, m, ls, lm)
|
| 338 |
+
d, m = _ls_refit(x, w, d, m, ls, lm, codes, max_d, max_m)
|
| 339 |
+
cur = (int(to_fp16_bits(np.float32(d))), int(to_fp16_bits(np.float32(m))))
|
| 340 |
+
if cur == prev:
|
| 341 |
+
break
|
| 342 |
+
prev = cur
|
| 343 |
+
|
| 344 |
+
d, m, codes, _ = _ulp_polish(x, w, d, m, ls, lm, cfg.ulp_radius)
|
| 345 |
+
ls, lm = _per_sub_scan(x, w, d, m)
|
| 346 |
+
codes = codes_from_params(x, d, m, ls, lm)
|
| 347 |
+
codes = refine_codes_hadamard(x, w, d, m, ls, lm, codes, cfg, dc_carry)
|
| 348 |
+
obj, sse = objective(x, w, d, m, ls, lm, codes, cfg, dc_carry)
|
| 349 |
+
return d, m, ls, lm, codes, obj, sse
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def _candidate_config(seeds, base_d, base_m, cidx):
|
| 353 |
+
d16, m16 = candidate_pair(base_d, base_m, cidx)
|
| 354 |
+
d = float(from_fp16_bits(d16))
|
| 355 |
+
m = float(from_fp16_bits(m16))
|
| 356 |
+
ls, lm = derive_subscales(seeds["scales"], seeds["mins"], d, m)
|
| 357 |
+
return d, m, ls, lm
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def _apply_dc_nudge(X, W, blocks, cfg, row_width):
|
| 361 |
+
n = X.shape[0]
|
| 362 |
+
blocks_per_row = row_width // QK_K if row_width and row_width % QK_K == 0 else 0
|
| 363 |
+
rolling = 0.0
|
| 364 |
+
for i in range(n):
|
| 365 |
+
if blocks_per_row > 0 and i % blocks_per_row == 0:
|
| 366 |
+
rolling = 0.0
|
| 367 |
+
block = blocks[i]
|
| 368 |
+
carry = np.float32(cfg.dc_decay * rolling)
|
| 369 |
+
d = float(block.d_float)
|
| 370 |
+
m = float(block.dmin_float)
|
| 371 |
+
ls, lm = block.unpack_scales()
|
| 372 |
+
codes = unpack_codes(block.qs)
|
| 373 |
+
w = None if W is None else W[i]
|
| 374 |
+
codes = dc_nudge_codes(X[i], d, m, ls, lm, codes, w, float(carry), cfg)
|
| 375 |
+
blocks[i] = Q2KBlock(
|
| 376 |
+
scales=pack_scales(ls, lm),
|
| 377 |
+
qs=pack_codes(codes),
|
| 378 |
+
d=block.d,
|
| 379 |
+
dmin=block.dmin,
|
| 380 |
+
)
|
| 381 |
+
xh = blocks[i].dequantize()
|
| 382 |
+
dc_res = float(np.sum(X[i].astype(np.float64) - xh.astype(np.float64)))
|
| 383 |
+
rolling = float(carry) + dc_res if cfg.carry_cumulative else dc_res
|
| 384 |
+
return blocks
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
class Q2KQuantizer:
|
| 388 |
+
def __init__(self, config: HPCConfig | None = None):
|
| 389 |
+
self.config = (config or HPCConfig()).validate()
|
| 390 |
+
|
| 391 |
+
def quantize(
|
| 392 |
+
self,
|
| 393 |
+
weights: np.ndarray,
|
| 394 |
+
importance: np.ndarray | None = None,
|
| 395 |
+
row_width: int | None = None,
|
| 396 |
+
) -> QuantizedTensor:
|
| 397 |
+
cfg = self.config
|
| 398 |
+
x = np.asarray(weights, dtype=np.float32)
|
| 399 |
+
if x.size % QK_K != 0:
|
| 400 |
+
raise ValueError("weight count must be a multiple of 256")
|
| 401 |
+
if x.ndim == 2 and row_width is None and x.shape[1] % QK_K == 0:
|
| 402 |
+
row_width = int(x.shape[1])
|
| 403 |
+
x = x.ravel()
|
| 404 |
+
n_blocks = x.size // QK_K
|
| 405 |
+
X = x.reshape(n_blocks, QK_K)
|
| 406 |
+
|
| 407 |
+
if importance is not None:
|
| 408 |
+
imp = np.asarray(importance, dtype=np.float32).ravel()
|
| 409 |
+
if imp.size != x.size:
|
| 410 |
+
raise ValueError("importance must match the weight count")
|
| 411 |
+
W = imp.reshape(n_blocks, QK_K)
|
| 412 |
+
else:
|
| 413 |
+
W = None
|
| 414 |
+
|
| 415 |
+
seeds = build_seeds(X, W, cfg)
|
| 416 |
+
clamp_val = seed_clamp_value(x, cfg)
|
| 417 |
+
base_d, base_m = _phase2_wls(X, W, seeds, clamp_val)
|
| 418 |
+
|
| 419 |
+
candidate_errors = np.full((n_blocks, TOTAL_CANDIDATES), np.inf, dtype=np.float32)
|
| 420 |
+
standalone: list[FactorResult] = []
|
| 421 |
+
for i in range(n_blocks):
|
| 422 |
+
wi = None if W is None else W[i]
|
| 423 |
+
candidate_errors[i] = factor_candidates(
|
| 424 |
+
X[i],
|
| 425 |
+
wi,
|
| 426 |
+
seeds["scales"][i],
|
| 427 |
+
seeds["mins"][i],
|
| 428 |
+
base_d[i],
|
| 429 |
+
base_m[i],
|
| 430 |
+
cfg,
|
| 431 |
+
)
|
| 432 |
+
standalone.append(factor_only_solve(X[i], wi, cfg))
|
| 433 |
+
|
| 434 |
+
best, temperature, _ = _phase3_select(candidate_errors, cfg)
|
| 435 |
+
carry = _compute_dc_carry(X, seeds, best, base_d, base_m, cfg, row_width)
|
| 436 |
+
|
| 437 |
+
at_slot: list[FactorResult] = []
|
| 438 |
+
for i in range(n_blocks):
|
| 439 |
+
wi = None if W is None else W[i]
|
| 440 |
+
d16, m16 = candidate_pair(base_d[i], base_m[i], best[i])
|
| 441 |
+
at_slot.append(
|
| 442 |
+
factor_q2k_block(
|
| 443 |
+
X[i],
|
| 444 |
+
wi,
|
| 445 |
+
cfg,
|
| 446 |
+
init_s=seeds["scales"][i],
|
| 447 |
+
init_t=seeds["mins"][i],
|
| 448 |
+
d_seed=float(from_fp16_bits(d16)),
|
| 449 |
+
m_seed=float(from_fp16_bits(m16)),
|
| 450 |
+
pin=cfg.factor_pin,
|
| 451 |
+
use_spectral=cfg.use_spectral,
|
| 452 |
+
max_iter=cfg.factor_iters,
|
| 453 |
+
starts=1,
|
| 454 |
+
dc_carry=0.0,
|
| 455 |
+
)
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
blocks: list[Q2KBlock] = []
|
| 459 |
+
factor_floor_used = 0
|
| 460 |
+
candidate_floor_used = 0
|
| 461 |
+
for i in range(n_blocks):
|
| 462 |
+
wi = None if W is None else W[i]
|
| 463 |
+
d, m, ls, lm, codes, obj, sse = _assemble_block(
|
| 464 |
+
X[i], wi, seeds, base_d[i], base_m[i], best[i], carry[i], cfg
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
if cfg.use_candidate_floor:
|
| 468 |
+
cd, cm, cls, clm = _candidate_config(
|
| 469 |
+
{k: v[i] for k, v in seeds.items() if k in ("scales", "mins")},
|
| 470 |
+
base_d[i],
|
| 471 |
+
base_m[i],
|
| 472 |
+
best[i],
|
| 473 |
+
)
|
| 474 |
+
ccodes = codes_from_params(X[i], cd, cm, cls, clm)
|
| 475 |
+
cobj, csse = objective(
|
| 476 |
+
X[i], wi, cd, cm, cls, clm, ccodes, cfg, float(carry[i])
|
| 477 |
+
)
|
| 478 |
+
if cobj < obj - 1.0e-15:
|
| 479 |
+
d, m, ls, lm, codes, obj, sse = cd, cm, cls, clm, ccodes, cobj, csse
|
| 480 |
+
candidate_floor_used += 1
|
| 481 |
+
|
| 482 |
+
if cfg.use_factor_floor:
|
| 483 |
+
fac = at_slot[i]
|
| 484 |
+
fobj, fsse = objective(
|
| 485 |
+
X[i], wi, fac.d, fac.dmin, fac.ls, fac.lm, fac.codes, cfg, float(carry[i])
|
| 486 |
+
)
|
| 487 |
+
if fobj < obj - 1.0e-15:
|
| 488 |
+
d = float(fac.d)
|
| 489 |
+
m = float(fac.dmin)
|
| 490 |
+
ls = fac.ls.copy()
|
| 491 |
+
lm = fac.lm.copy()
|
| 492 |
+
codes = fac.codes.copy()
|
| 493 |
+
obj, sse = fobj, fsse
|
| 494 |
+
factor_floor_used += 1
|
| 495 |
+
|
| 496 |
+
blocks.append(make_block(d, m, ls, lm, codes))
|
| 497 |
+
|
| 498 |
+
if cfg.apply_dc_carry and (cfg.dc_lambda > 0.0 or cfg.dc_decay > 0.0):
|
| 499 |
+
blocks = _apply_dc_nudge(X, W, blocks, cfg, row_width)
|
| 500 |
+
|
| 501 |
+
total_sse = 0.0
|
| 502 |
+
weighted = 0.0
|
| 503 |
+
for i, b in enumerate(blocks):
|
| 504 |
+
e = X[i].astype(np.float64) - b.dequantize().astype(np.float64)
|
| 505 |
+
total_sse += float(np.dot(e, e))
|
| 506 |
+
if W is None:
|
| 507 |
+
weighted += float(np.dot(e, e))
|
| 508 |
+
else:
|
| 509 |
+
weighted += float(np.dot(W[i].astype(np.float64), e * e))
|
| 510 |
+
|
| 511 |
+
sigma = float(np.sqrt(np.mean(x.astype(np.float64) ** 2)))
|
| 512 |
+
rmse = float(np.sqrt(total_sse / x.size))
|
| 513 |
+
stats = TensorStats(
|
| 514 |
+
n_blocks=n_blocks,
|
| 515 |
+
total_sse=total_sse,
|
| 516 |
+
weighted_sse=weighted,
|
| 517 |
+
rmse=rmse,
|
| 518 |
+
sigma=sigma,
|
| 519 |
+
rmse_over_sigma=rmse / sigma if sigma > 1.0e-15 else 0.0,
|
| 520 |
+
temperature=temperature,
|
| 521 |
+
factor_floor_used=factor_floor_used,
|
| 522 |
+
candidate_floor_used=candidate_floor_used,
|
| 523 |
+
)
|
| 524 |
+
scales, qs, dm = _stack_blocks(blocks)
|
| 525 |
+
factor_objectives = np.array([r.objective for r in standalone], dtype=np.float64)
|
| 526 |
+
result = QuantizedTensor(
|
| 527 |
+
blocks=blocks,
|
| 528 |
+
scales=scales,
|
| 529 |
+
qs=qs,
|
| 530 |
+
dm=dm,
|
| 531 |
+
best_candidate=best,
|
| 532 |
+
factor_objectives=factor_objectives,
|
| 533 |
+
config=cfg,
|
| 534 |
+
stats=stats,
|
| 535 |
+
)
|
| 536 |
+
if cfg.verbose:
|
| 537 |
+
_report(stats)
|
| 538 |
+
return result
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def _stack_blocks(blocks):
|
| 542 |
+
n = len(blocks)
|
| 543 |
+
scales = np.empty((n, NS), dtype=np.uint8)
|
| 544 |
+
qs = np.empty((n, QK_K // 4), dtype=np.uint8)
|
| 545 |
+
dm = np.empty((n, 2), dtype=np.uint16)
|
| 546 |
+
for i, b in enumerate(blocks):
|
| 547 |
+
scales[i] = b.scales
|
| 548 |
+
qs[i] = b.qs
|
| 549 |
+
dm[i, 0] = b.d
|
| 550 |
+
dm[i, 1] = b.dmin
|
| 551 |
+
return scales, qs, dm
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
def _report(stats: TensorStats) -> None:
|
| 555 |
+
print(
|
| 556 |
+
f"Q2_K V2: blocks={stats.n_blocks} rmse={stats.rmse:.6e} "
|
| 557 |
+
f"rmse/sigma={stats.rmse_over_sigma:.5f} temp={stats.temperature:.3e} "
|
| 558 |
+
f"floors(cand={stats.candidate_floor_used}, factor={stats.factor_floor_used})"
|
| 559 |
+
)
|
q2k.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bit-exact Q2_K block representation, packing and reconstruction.
|
| 2 |
+
|
| 3 |
+
Layout (84 bytes per 256 weights)::
|
| 4 |
+
|
| 5 |
+
scales[16] uint8 low nibble = Ls[j], high nibble = Lm[j]
|
| 6 |
+
qs[64] uint8 four 2-bit codes per byte
|
| 7 |
+
d fp16 super-block scale
|
| 8 |
+
dmin fp16 super-block minimum scale
|
| 9 |
+
|
| 10 |
+
The decoder reconstructs::
|
| 11 |
+
|
| 12 |
+
x_hat[16*j + k] = d * Ls[j] * L[16*j + k] - dmin * Lm[j]
|
| 13 |
+
|
| 14 |
+
with ``L in {0,1,2,3}`` and ``Ls, Lm in {0..15}``.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
from dataclasses import dataclass
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
from .fp16 import from_fp16_bits, nearest_int, to_fp16_bits
|
| 24 |
+
|
| 25 |
+
QK_K = 256
|
| 26 |
+
NS = QK_K // 16
|
| 27 |
+
NQ = 4
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass(slots=True)
|
| 31 |
+
class Q2KBlock:
|
| 32 |
+
scales: np.ndarray
|
| 33 |
+
qs: np.ndarray
|
| 34 |
+
d: np.uint16
|
| 35 |
+
dmin: np.uint16
|
| 36 |
+
|
| 37 |
+
@property
|
| 38 |
+
def d_float(self) -> np.float32:
|
| 39 |
+
return np.float32(from_fp16_bits(np.uint16(self.d)))
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def dmin_float(self) -> np.float32:
|
| 43 |
+
return np.float32(from_fp16_bits(np.uint16(self.dmin)))
|
| 44 |
+
|
| 45 |
+
def unpack_scales(self) -> tuple[np.ndarray, np.ndarray]:
|
| 46 |
+
scales = np.asarray(self.scales, dtype=np.uint8)
|
| 47 |
+
return scales & 0x0F, scales >> 4
|
| 48 |
+
|
| 49 |
+
def dequantize(self) -> np.ndarray:
|
| 50 |
+
ls, lm = self.unpack_scales()
|
| 51 |
+
return dequantize_params(
|
| 52 |
+
self.d_float, self.dmin_float, ls, lm, unpack_codes(self.qs)
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
def error(self, x: np.ndarray) -> float:
|
| 56 |
+
e = np.asarray(x, dtype=np.float64) - self.dequantize().astype(np.float64)
|
| 57 |
+
return float(np.dot(e, e))
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def unpack_codes(qs: np.ndarray) -> np.ndarray:
|
| 61 |
+
qs = np.asarray(qs, dtype=np.uint8)
|
| 62 |
+
codes = np.empty(QK_K, dtype=np.uint8)
|
| 63 |
+
for base in (0, 128):
|
| 64 |
+
q = qs[base // 4 : base // 4 + 32]
|
| 65 |
+
for shift_level in range(4):
|
| 66 |
+
lo = base + 32 * shift_level
|
| 67 |
+
codes[lo : lo + 32] = (q >> np.uint8(2 * shift_level)) & np.uint8(3)
|
| 68 |
+
return codes
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def pack_codes(codes: np.ndarray) -> np.ndarray:
|
| 72 |
+
codes = np.asarray(codes, dtype=np.uint8)
|
| 73 |
+
if codes.shape != (QK_K,):
|
| 74 |
+
raise ValueError("codes must have shape (256,)")
|
| 75 |
+
qs = np.empty(QK_K // 4, dtype=np.uint8)
|
| 76 |
+
for base in (0, 128):
|
| 77 |
+
q = (
|
| 78 |
+
codes[base : base + 32]
|
| 79 |
+
| (codes[base + 32 : base + 64] << np.uint8(2))
|
| 80 |
+
| (codes[base + 64 : base + 96] << np.uint8(4))
|
| 81 |
+
| (codes[base + 96 : base + 128] << np.uint8(6))
|
| 82 |
+
)
|
| 83 |
+
qs[base // 4 : base // 4 + 32] = q
|
| 84 |
+
return qs
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def pack_scales(ls: np.ndarray, lm: np.ndarray) -> np.ndarray:
|
| 88 |
+
return (np.asarray(ls, dtype=np.uint8) & 0x0F) | (
|
| 89 |
+
(np.asarray(lm, dtype=np.uint8) & 0x0F) << np.uint8(4)
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def dequantize_params(
|
| 94 |
+
d, dmin, ls: np.ndarray, lm: np.ndarray, codes: np.ndarray
|
| 95 |
+
) -> np.ndarray:
|
| 96 |
+
ls = np.asarray(ls, dtype=np.float32)
|
| 97 |
+
lm = np.asarray(lm, dtype=np.float32)
|
| 98 |
+
codes = np.asarray(codes, dtype=np.float32)
|
| 99 |
+
d_sub = np.repeat(np.float32(d) * ls, 16)
|
| 100 |
+
m_sub = np.repeat(np.float32(dmin) * lm, 16)
|
| 101 |
+
return d_sub * codes - m_sub
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def codes_from_params(
|
| 105 |
+
x: np.ndarray, d, dmin, ls: np.ndarray, lm: np.ndarray
|
| 106 |
+
) -> np.ndarray:
|
| 107 |
+
x = np.asarray(x, dtype=np.float32)
|
| 108 |
+
d_sub = np.repeat(np.float32(d) * np.asarray(ls, dtype=np.float32), 16)
|
| 109 |
+
m_sub = np.repeat(np.float32(dmin) * np.asarray(lm, dtype=np.float32), 16)
|
| 110 |
+
safe = d_sub >= np.float32(1.0e-15)
|
| 111 |
+
codes = np.zeros(QK_K, dtype=np.uint8)
|
| 112 |
+
if np.any(safe):
|
| 113 |
+
q = nearest_int((x[safe] + m_sub[safe]) / d_sub[safe])
|
| 114 |
+
codes[safe] = np.clip(q, 0, 3).astype(np.uint8)
|
| 115 |
+
return codes
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def derive_subscales(
|
| 119 |
+
scales: np.ndarray, mins: np.ndarray, d, dmin
|
| 120 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 121 |
+
scales = np.asarray(scales, dtype=np.float32)
|
| 122 |
+
mins = np.asarray(mins, dtype=np.float32)
|
| 123 |
+
if float(d) > 1.0e-15:
|
| 124 |
+
ls = np.clip(nearest_int(scales / np.float32(d)), 0, 15).astype(np.uint8)
|
| 125 |
+
else:
|
| 126 |
+
ls = np.zeros(NS, dtype=np.uint8)
|
| 127 |
+
if float(dmin) > 1.0e-15:
|
| 128 |
+
lm = np.clip(nearest_int(mins / np.float32(dmin)), 0, 15).astype(np.uint8)
|
| 129 |
+
else:
|
| 130 |
+
lm = np.zeros(NS, dtype=np.uint8)
|
| 131 |
+
return ls, lm
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def weighted_sse(
|
| 135 |
+
x: np.ndarray,
|
| 136 |
+
w: np.ndarray | None,
|
| 137 |
+
d,
|
| 138 |
+
dmin,
|
| 139 |
+
ls: np.ndarray,
|
| 140 |
+
lm: np.ndarray,
|
| 141 |
+
codes: np.ndarray,
|
| 142 |
+
) -> float:
|
| 143 |
+
xh = dequantize_params(d, dmin, ls, lm, codes)
|
| 144 |
+
e = np.asarray(x, dtype=np.float64) - xh.astype(np.float64)
|
| 145 |
+
if w is None:
|
| 146 |
+
return float(np.dot(e, e))
|
| 147 |
+
return float(np.dot(np.asarray(w, dtype=np.float64), e * e))
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def make_block(d, dmin, ls, lm, codes) -> Q2KBlock:
|
| 151 |
+
return Q2KBlock(
|
| 152 |
+
scales=pack_scales(ls, lm),
|
| 153 |
+
qs=pack_codes(codes),
|
| 154 |
+
d=np.uint16(to_fp16_bits(np.float32(d))),
|
| 155 |
+
dmin=np.uint16(to_fp16_bits(np.float32(dmin))),
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def blocks_to_arrays(blocks: list[Q2KBlock]) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 160 |
+
n = len(blocks)
|
| 161 |
+
scales = np.empty((n, NS), dtype=np.uint8)
|
| 162 |
+
qs = np.empty((n, QK_K // 4), dtype=np.uint8)
|
| 163 |
+
dm = np.empty((n, 2), dtype=np.uint16)
|
| 164 |
+
for i, b in enumerate(blocks):
|
| 165 |
+
scales[i] = b.scales
|
| 166 |
+
qs[i] = b.qs
|
| 167 |
+
dm[i, 0] = b.d
|
| 168 |
+
dm[i, 1] = b.dmin
|
| 169 |
+
return scales, qs, dm
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def dequantize_blocks(blocks: list[Q2KBlock]) -> np.ndarray:
|
| 173 |
+
if not blocks:
|
| 174 |
+
return np.empty(0, dtype=np.float32)
|
| 175 |
+
return np.concatenate([b.dequantize() for b in blocks])
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def codes_from_params_batch(
|
| 179 |
+
x: np.ndarray, d: np.ndarray, dmin: np.ndarray, ls: np.ndarray, lm: np.ndarray
|
| 180 |
+
) -> np.ndarray:
|
| 181 |
+
x = np.asarray(x, dtype=np.float32)
|
| 182 |
+
n = x.shape[0]
|
| 183 |
+
d_sub = np.repeat(
|
| 184 |
+
np.asarray(d, dtype=np.float32)[:, None] * np.asarray(ls, dtype=np.float32), 16, axis=1
|
| 185 |
+
)
|
| 186 |
+
m_sub = np.repeat(
|
| 187 |
+
np.asarray(dmin, dtype=np.float32)[:, None] * np.asarray(lm, dtype=np.float32), 16, axis=1
|
| 188 |
+
)
|
| 189 |
+
safe = d_sub >= np.float32(1.0e-15)
|
| 190 |
+
codes = np.zeros((n, QK_K), dtype=np.uint8)
|
| 191 |
+
if np.any(safe):
|
| 192 |
+
q = nearest_int((x[safe] + m_sub[safe]) / d_sub[safe])
|
| 193 |
+
codes[safe] = np.clip(q, 0, 3).astype(np.uint8)
|
| 194 |
+
return codes
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def dequantize_params_batch(
|
| 198 |
+
d: np.ndarray, dmin: np.ndarray, ls: np.ndarray, lm: np.ndarray, codes: np.ndarray
|
| 199 |
+
) -> np.ndarray:
|
| 200 |
+
n = codes.shape[0]
|
| 201 |
+
d_sub = np.repeat(
|
| 202 |
+
np.asarray(d, dtype=np.float32)[:, None] * np.asarray(ls, dtype=np.float32), 16, axis=1
|
| 203 |
+
)
|
| 204 |
+
m_sub = np.repeat(
|
| 205 |
+
np.asarray(dmin, dtype=np.float32)[:, None] * np.asarray(lm, dtype=np.float32), 16, axis=1
|
| 206 |
+
)
|
| 207 |
+
return d_sub * np.asarray(codes, dtype=np.float32) - m_sub
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def weighted_sse_batch(
|
| 211 |
+
x: np.ndarray,
|
| 212 |
+
w: np.ndarray | None,
|
| 213 |
+
d: np.ndarray,
|
| 214 |
+
dmin: np.ndarray,
|
| 215 |
+
ls: np.ndarray,
|
| 216 |
+
lm: np.ndarray,
|
| 217 |
+
codes: np.ndarray,
|
| 218 |
+
) -> np.ndarray:
|
| 219 |
+
xh = dequantize_params_batch(d, dmin, ls, lm, codes)
|
| 220 |
+
e = np.asarray(x, dtype=np.float64) - xh.astype(np.float64)
|
| 221 |
+
if w is None:
|
| 222 |
+
return np.sum(e * e, axis=1)
|
| 223 |
+
return np.sum(np.asarray(w, dtype=np.float64) * e * e, axis=1)
|
seeds.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Batched GGML seed quantizers and activation-aware seed weighting.
|
| 2 |
+
|
| 3 |
+
The C engine performs ``make_qkx2_quants`` once per 16-weight sub-block and
|
| 4 |
+
``make_qp_quants`` once per 16-sub-block superblock. V2 batches both across
|
| 5 |
+
every sub-block / superblock in the tensor with NumPy, which removes the
|
| 6 |
+
per-block Python overhead while preserving the exact search rules.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
from .config import HPCConfig
|
| 14 |
+
from .fp16 import nearest_int
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def make_qkx2_batch(
|
| 18 |
+
x: np.ndarray, w: np.ndarray, nmax: int = 3
|
| 19 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 20 |
+
"""Batched ``make_qkx2_quants``. Returns (scale, min_abs, codes)."""
|
| 21 |
+
x = np.asarray(x, dtype=np.float64)
|
| 22 |
+
w = np.asarray(w, dtype=np.float64)
|
| 23 |
+
rows, n = x.shape
|
| 24 |
+
xmin = x.min(axis=1)
|
| 25 |
+
xmax = x.max(axis=1)
|
| 26 |
+
xmin = np.minimum(xmin, 0.0)
|
| 27 |
+
span = xmax - xmin
|
| 28 |
+
degenerate = (xmax == xmin) | (span <= 1.0e-30)
|
| 29 |
+
|
| 30 |
+
sum_w = w.sum(axis=1)
|
| 31 |
+
sum_x = (w * x).sum(axis=1)
|
| 32 |
+
|
| 33 |
+
safe_span = np.where(span > 1.0e-30, span, 1.0)
|
| 34 |
+
iscale = nmax / safe_span
|
| 35 |
+
scale = 1.0 / iscale
|
| 36 |
+
codes = np.clip(nearest_int(iscale[:, None] * (x - xmin[:, None])), 0, nmax)
|
| 37 |
+
best_mad = np.sum(w * np.abs(scale[:, None] * codes + xmin[:, None] - x), axis=1)
|
| 38 |
+
best_scale = scale.copy()
|
| 39 |
+
best_min = xmin.copy()
|
| 40 |
+
best_codes = codes.copy()
|
| 41 |
+
|
| 42 |
+
for step in range(16):
|
| 43 |
+
try_iscale = (-0.5 + 0.1 * step + nmax) / safe_span
|
| 44 |
+
aux = np.clip(nearest_int(try_iscale[:, None] * (x - xmin[:, None])), 0, nmax)
|
| 45 |
+
sl = (w * aux).sum(axis=1)
|
| 46 |
+
sl2 = (w * aux * aux).sum(axis=1)
|
| 47 |
+
sxl = (w * aux * x).sum(axis=1)
|
| 48 |
+
det = sum_w * sl2 - sl * sl
|
| 49 |
+
valid = det > 1.0e-30
|
| 50 |
+
safe_det = np.where(valid, det, 1.0)
|
| 51 |
+
this_scale = (sum_w * sxl - sum_x * sl) / safe_det
|
| 52 |
+
this_min = (sl2 * sum_x - sl * sxl) / safe_det
|
| 53 |
+
positive_min = this_min > 0.0
|
| 54 |
+
this_min = np.where(positive_min, 0.0, this_min)
|
| 55 |
+
this_scale = np.where(
|
| 56 |
+
positive_min & (sl2 > 1.0e-30), sxl / np.where(sl2 > 1.0e-30, sl2, 1.0), this_scale
|
| 57 |
+
)
|
| 58 |
+
mad = np.sum(w * np.abs(this_scale[:, None] * aux + this_min[:, None] - x), axis=1)
|
| 59 |
+
take = valid & (mad < best_mad)
|
| 60 |
+
if np.any(take):
|
| 61 |
+
best_mad = np.where(take, mad, best_mad)
|
| 62 |
+
best_scale = np.where(take, this_scale, best_scale)
|
| 63 |
+
best_min = np.where(take, this_min, best_min)
|
| 64 |
+
best_codes[take] = aux[take]
|
| 65 |
+
|
| 66 |
+
best_scale = np.where(degenerate, 0.0, best_scale)
|
| 67 |
+
best_codes[degenerate] = 0
|
| 68 |
+
best_min = np.where(degenerate, xmin, best_min)
|
| 69 |
+
return best_scale, -best_min, best_codes.astype(np.uint8)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def make_qp_batch(
|
| 73 |
+
values: np.ndarray, weights: np.ndarray, nmax: int = 15
|
| 74 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 75 |
+
"""Batched ``make_qp_quants``. Factors positive values into d * code."""
|
| 76 |
+
x = np.asarray(values, dtype=np.float64)
|
| 77 |
+
w = np.asarray(weights, dtype=np.float64)
|
| 78 |
+
rows, n = x.shape
|
| 79 |
+
xmax = x.max(axis=1)
|
| 80 |
+
dead = xmax < 1.0e-15
|
| 81 |
+
safe_max = np.where(dead, 1.0, xmax)
|
| 82 |
+
|
| 83 |
+
iscale = nmax / safe_max
|
| 84 |
+
codes = np.clip(nearest_int(iscale[:, None] * x), 0, nmax)
|
| 85 |
+
scale = 1.0 / iscale
|
| 86 |
+
best_mse = np.sum(w * (x - scale[:, None] * codes) ** 2, axis=1)
|
| 87 |
+
best_iscale = iscale.copy()
|
| 88 |
+
|
| 89 |
+
for step in range(-4, 5):
|
| 90 |
+
if step == 0:
|
| 91 |
+
continue
|
| 92 |
+
trial_iscale = (0.1 * step + nmax) / safe_max
|
| 93 |
+
trial_scale = 1.0 / trial_iscale
|
| 94 |
+
trial_codes = np.clip(nearest_int(trial_iscale[:, None] * x), 0, nmax)
|
| 95 |
+
mse = np.sum(w * (x - trial_scale[:, None] * trial_codes) ** 2, axis=1)
|
| 96 |
+
take = mse < best_mse
|
| 97 |
+
if np.any(take):
|
| 98 |
+
best_mse = np.where(take, mse, best_mse)
|
| 99 |
+
best_iscale = np.where(take, trial_iscale, best_iscale)
|
| 100 |
+
|
| 101 |
+
codes = np.clip(nearest_int(best_iscale[:, None] * x), 0, nmax)
|
| 102 |
+
sumlx = (w * x * codes).sum(axis=1)
|
| 103 |
+
suml2 = (w * codes * codes).sum(axis=1)
|
| 104 |
+
|
| 105 |
+
for _ in range(5):
|
| 106 |
+
changed = False
|
| 107 |
+
for i in range(n):
|
| 108 |
+
wi = w[:, i]
|
| 109 |
+
xi = x[:, i]
|
| 110 |
+
li = codes[:, i].astype(np.float64)
|
| 111 |
+
slx = sumlx - wi * xi * li
|
| 112 |
+
sl2 = suml2 - wi * li * li
|
| 113 |
+
valid = (slx > 0.0) & (sl2 > 0.0)
|
| 114 |
+
safe_slx = np.where(valid, slx, 1.0)
|
| 115 |
+
new_l = np.clip(nearest_int(xi * sl2 / safe_slx), 0, nmax)
|
| 116 |
+
trial = valid & (new_l != codes[:, i])
|
| 117 |
+
if not np.any(trial):
|
| 118 |
+
continue
|
| 119 |
+
slx2 = slx + wi * xi * new_l
|
| 120 |
+
sl2_2 = sl2 + wi * new_l * new_l
|
| 121 |
+
better = trial & (slx2 * slx2 * suml2 > sumlx * sumlx * sl2_2)
|
| 122 |
+
if np.any(better):
|
| 123 |
+
codes[better, i] = new_l[better].astype(np.uint8)
|
| 124 |
+
sumlx = np.where(better, slx2, sumlx)
|
| 125 |
+
suml2 = np.where(better, sl2_2, suml2)
|
| 126 |
+
changed = True
|
| 127 |
+
if not changed:
|
| 128 |
+
break
|
| 129 |
+
|
| 130 |
+
d = np.where(suml2 > 1.0e-30, sumlx / np.where(suml2 > 1.0e-30, suml2, 1.0), 0.0)
|
| 131 |
+
d = np.where(dead, 0.0, d)
|
| 132 |
+
codes[dead] = 0
|
| 133 |
+
return d, codes.astype(np.uint8)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def seed_clamp_value(weights: np.ndarray, cfg: HPCConfig) -> float:
|
| 137 |
+
x = np.asarray(weights, dtype=np.float64).ravel()
|
| 138 |
+
mean_sq = float(np.mean(x * x))
|
| 139 |
+
if mean_sq <= 1.0e-30:
|
| 140 |
+
return float("inf")
|
| 141 |
+
mean_fourth = float(np.mean(x * x * x * x))
|
| 142 |
+
kurtosis = mean_fourth / (mean_sq * mean_sq)
|
| 143 |
+
sigma = float(np.sqrt(mean_sq))
|
| 144 |
+
if kurtosis <= cfg.clamp_kurtosis_off:
|
| 145 |
+
return float("inf")
|
| 146 |
+
if kurtosis <= cfg.clamp_kurtosis_soft:
|
| 147 |
+
return cfg.clamp_sigma_soft * sigma
|
| 148 |
+
return cfg.clamp_sigma_hard * sigma
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def seed_weights(
|
| 152 |
+
block: np.ndarray, importance: np.ndarray | None, sigma_sq: float, clamp_val: float
|
| 153 |
+
) -> np.ndarray:
|
| 154 |
+
if importance is not None:
|
| 155 |
+
return np.asarray(importance, dtype=np.float64)
|
| 156 |
+
return np.sqrt(sigma_sq + np.minimum(block, clamp_val) ** 2)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def build_seeds(
|
| 160 |
+
x: np.ndarray, importance: np.ndarray | None, cfg: HPCConfig
|
| 161 |
+
) -> dict[str, np.ndarray]:
|
| 162 |
+
"""Phase-1 seeds for all blocks: per-sub (scale,min) and shared (d,dmin)."""
|
| 163 |
+
n_blocks = x.shape[0]
|
| 164 |
+
blocks = np.asarray(x, dtype=np.float64).reshape(n_blocks, 16, 16)
|
| 165 |
+
if importance is None:
|
| 166 |
+
sigma_sq = np.mean(blocks * blocks, axis=(1, 2), keepdims=True)
|
| 167 |
+
weights = np.sqrt(sigma_sq + blocks * blocks)
|
| 168 |
+
else:
|
| 169 |
+
weights = np.asarray(importance, dtype=np.float64).reshape(n_blocks, 16, 16)
|
| 170 |
+
|
| 171 |
+
flat_x = blocks.reshape(-1, 16)
|
| 172 |
+
flat_w = weights.reshape(-1, 16)
|
| 173 |
+
scales, mins, codes = make_qkx2_batch(flat_x, flat_w, nmax=3)
|
| 174 |
+
scales = scales.reshape(n_blocks, 16)
|
| 175 |
+
mins = mins.reshape(n_blocks, 16)
|
| 176 |
+
codes = codes.reshape(n_blocks, 256)
|
| 177 |
+
sw = weights.sum(axis=2)
|
| 178 |
+
d, ls = make_qp_batch(scales, sw, nmax=15)
|
| 179 |
+
dmin, lm = make_qp_batch(mins, sw, nmax=15)
|
| 180 |
+
return {
|
| 181 |
+
"scales": scales.astype(np.float32),
|
| 182 |
+
"mins": mins.astype(np.float32),
|
| 183 |
+
"codes": codes.astype(np.uint8),
|
| 184 |
+
"sw": sw.astype(np.float32),
|
| 185 |
+
"d": d.astype(np.float32),
|
| 186 |
+
"dmin": dmin.astype(np.float32),
|
| 187 |
+
"ls": ls.astype(np.uint8),
|
| 188 |
+
"lm": lm.astype(np.uint8),
|
| 189 |
+
"weights": weights.astype(np.float32),
|
| 190 |
+
}
|
sieve.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sequential six-state Sieve, ported from the HPC graph measurement.
|
| 2 |
+
|
| 3 |
+
The C engine builds an :class:`HPCGraph` and runs a column-first sequential
|
| 4 |
+
measurement with real back-action. Only the following semantics are used by
|
| 5 |
+
the Q2_K pipeline, so V2 implements them directly on a chain:
|
| 6 |
+
|
| 7 |
+
* local Born probabilities ``|amplitude|^2``;
|
| 8 |
+
* a multiplicative neighbour mix with ``same_penalty`` for equal bins;
|
| 9 |
+
* a log-domain slack filter;
|
| 10 |
+
* deterministic MAP collapse (the production setting);
|
| 11 |
+
* back-action that biases still-unmeasured neighbours away from the outcome.
|
| 12 |
+
|
| 13 |
+
This is mathematically identical to the production path while removing the
|
| 14 |
+
graph allocation, edge bookkeeping and triality view machinery.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def sieve_measure_chain(
|
| 23 |
+
amplitudes: np.ndarray,
|
| 24 |
+
edges: list[tuple[int, int]],
|
| 25 |
+
same_penalty: float = 0.85,
|
| 26 |
+
log_slack: float = 1.5,
|
| 27 |
+
deterministic: bool = True,
|
| 28 |
+
rng: np.random.Generator | None = None,
|
| 29 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 30 |
+
amps = np.array(amplitudes, dtype=np.float64, copy=True)
|
| 31 |
+
n_sites, n_states = amps.shape
|
| 32 |
+
if n_states != 6:
|
| 33 |
+
raise ValueError("sieve expects six symbolic states per site")
|
| 34 |
+
adj: list[list[int]] = [[] for _ in range(n_sites)]
|
| 35 |
+
for eid, (a, b) in enumerate(edges):
|
| 36 |
+
adj[a].append(eid)
|
| 37 |
+
adj[b].append(eid)
|
| 38 |
+
alive = np.ones(len(edges), dtype=bool)
|
| 39 |
+
marginals = np.zeros((n_sites, 6), dtype=np.float64)
|
| 40 |
+
measured = np.zeros(n_sites, dtype=np.int64)
|
| 41 |
+
generator = rng or np.random.default_rng(271828)
|
| 42 |
+
|
| 43 |
+
for site in range(n_sites - 1, -1, -1):
|
| 44 |
+
local = amps[site] * amps[site]
|
| 45 |
+
total = float(local.sum())
|
| 46 |
+
probs0 = local / total if total > 1.0e-30 else np.full(6, 1.0 / 6.0)
|
| 47 |
+
log_score = np.log(probs0 + 1.0e-30)
|
| 48 |
+
|
| 49 |
+
for eid in adj[site]:
|
| 50 |
+
if not alive[eid]:
|
| 51 |
+
continue
|
| 52 |
+
a, b = edges[eid]
|
| 53 |
+
partner = b if a == site else a
|
| 54 |
+
q = amps[partner] * amps[partner]
|
| 55 |
+
qt = float(q.sum())
|
| 56 |
+
q = q / qt if qt > 1.0e-30 else np.full(6, 1.0 / 6.0)
|
| 57 |
+
mix = np.empty(6)
|
| 58 |
+
for d in range(6):
|
| 59 |
+
mix[d] = sum(q[w] * (same_penalty if w == d else 1.0) for w in range(6))
|
| 60 |
+
log_score += np.log(mix + 1.0e-30)
|
| 61 |
+
|
| 62 |
+
best = float(log_score.max())
|
| 63 |
+
probs = np.where(best - log_score > log_slack, 0.0, np.exp(log_score - best))
|
| 64 |
+
total = float(probs.sum())
|
| 65 |
+
probs = probs / total if total > 1.0e-30 else np.full(6, 1.0 / 6.0)
|
| 66 |
+
marginals[site] = probs
|
| 67 |
+
|
| 68 |
+
if deterministic:
|
| 69 |
+
outcome = int(np.argmax(probs))
|
| 70 |
+
else:
|
| 71 |
+
outcome = int(np.searchsorted(np.cumsum(probs), generator.random(), side="right"))
|
| 72 |
+
outcome = min(outcome, 5)
|
| 73 |
+
measured[site] = outcome
|
| 74 |
+
|
| 75 |
+
amps[site] = 0.0
|
| 76 |
+
amps[site, outcome] = 1.0
|
| 77 |
+
for eid in adj[site]:
|
| 78 |
+
if not alive[eid]:
|
| 79 |
+
continue
|
| 80 |
+
a, b = edges[eid]
|
| 81 |
+
partner = b if a == site else a
|
| 82 |
+
amps[partner, outcome] *= same_penalty
|
| 83 |
+
norm = float(np.linalg.norm(amps[partner]))
|
| 84 |
+
if norm > 1.0e-30:
|
| 85 |
+
amps[partner] /= norm
|
| 86 |
+
for eid in adj[site]:
|
| 87 |
+
alive[eid] = False
|
| 88 |
+
adj[site] = []
|
| 89 |
+
|
| 90 |
+
return marginals, measured
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def block_chain_edges(n_blocks: int) -> list[tuple[int, int]]:
|
| 94 |
+
"""The production two-site-per-block chain, including cross-block links."""
|
| 95 |
+
edges: list[tuple[int, int]] = []
|
| 96 |
+
for i in range(n_blocks):
|
| 97 |
+
s0, s1 = 2 * i, 2 * i + 1
|
| 98 |
+
edges.append((s0, s1))
|
| 99 |
+
if i + 1 < n_blocks:
|
| 100 |
+
edges.append((s0, 2 * (i + 1)))
|
| 101 |
+
edges.append((s1, 2 * (i + 1) + 1))
|
| 102 |
+
return edges
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def subblock_chain_edges(n_subblocks: int) -> list[tuple[int, int]]:
|
| 106 |
+
return [(i, i + 1) for i in range(n_subblocks - 1)]
|
spectral.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HPC spectral objective: folded error pyramid, DC/vesica penalty, shaping.
|
| 2 |
+
|
| 3 |
+
The C engine scores a block with::
|
| 4 |
+
|
| 5 |
+
E = sum_i w_i e_i^2
|
| 6 |
+
+ (dc_lambda / n) * (sum_i e_i)^2
|
| 7 |
+
+ (vw_lambda / n) * sum_{pairs} (e_i + e_{i+n/2})^2
|
| 8 |
+
|
| 9 |
+
and the fold tree generalises the vesica term to every periodic level. V2
|
| 10 |
+
keeps the exact same objective and adds a vectorised implementation of the
|
| 11 |
+
D6/vesica greedy code descent.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
+
from .config import HPCConfig
|
| 19 |
+
from .q2k import QK_K, dequantize_params, weighted_sse
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def fold_weights(n: int, cfg: HPCConfig) -> np.ndarray:
|
| 23 |
+
levels = int(np.log2(n))
|
| 24 |
+
if (1 << levels) != n:
|
| 25 |
+
raise ValueError("fold pyramid requires a power-of-two length")
|
| 26 |
+
weights = np.zeros(levels, dtype=np.float64)
|
| 27 |
+
gamma = 1.0
|
| 28 |
+
for k in range(1, levels + 1):
|
| 29 |
+
if k == levels:
|
| 30 |
+
weights[k - 1] = cfg.dc_lambda / n
|
| 31 |
+
elif k <= cfg.fold_depth:
|
| 32 |
+
weights[k - 1] = (cfg.vw_lambda / n) * gamma
|
| 33 |
+
gamma *= cfg.fold_gamma
|
| 34 |
+
return weights
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def fold_build(errors: np.ndarray) -> np.ndarray:
|
| 38 |
+
e = np.asarray(errors, dtype=np.float64)
|
| 39 |
+
n = e.size
|
| 40 |
+
py = np.empty(n - 1, dtype=np.float64)
|
| 41 |
+
src = e
|
| 42 |
+
offset = 0
|
| 43 |
+
m = n // 2
|
| 44 |
+
while m >= 1:
|
| 45 |
+
dst = src[:m] + src[m : 2 * m]
|
| 46 |
+
py[offset : offset + m] = dst
|
| 47 |
+
src = dst
|
| 48 |
+
offset += m
|
| 49 |
+
m //= 2
|
| 50 |
+
return py
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def fold_energy(pyramid: np.ndarray, weights: np.ndarray) -> float:
|
| 54 |
+
acc = 0.0
|
| 55 |
+
offset = 0
|
| 56 |
+
m = pyramid.size // 2 + 1
|
| 57 |
+
level = 0
|
| 58 |
+
while m >= 1:
|
| 59 |
+
if weights[level] != 0.0:
|
| 60 |
+
v = pyramid[offset : offset + m]
|
| 61 |
+
acc += float(weights[level] * np.dot(v, v))
|
| 62 |
+
offset += m
|
| 63 |
+
m //= 2
|
| 64 |
+
level += 1
|
| 65 |
+
return acc
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def spectral_penalty(
|
| 69 |
+
errors: np.ndarray, cfg: HPCConfig, dc_carry: float = 0.0
|
| 70 |
+
) -> float:
|
| 71 |
+
if not cfg.use_spectral or (cfg.dc_lambda == 0.0 and cfg.vw_lambda == 0.0):
|
| 72 |
+
return 0.0
|
| 73 |
+
pyramid = fold_build(errors)
|
| 74 |
+
pyramid[-1] += dc_carry
|
| 75 |
+
return fold_energy(pyramid, fold_weights(errors.size, cfg))
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def objective(
|
| 79 |
+
x: np.ndarray,
|
| 80 |
+
w: np.ndarray | None,
|
| 81 |
+
d,
|
| 82 |
+
dmin,
|
| 83 |
+
ls: np.ndarray,
|
| 84 |
+
lm: np.ndarray,
|
| 85 |
+
codes: np.ndarray,
|
| 86 |
+
cfg: HPCConfig,
|
| 87 |
+
dc_carry: float = 0.0,
|
| 88 |
+
) -> tuple[float, float]:
|
| 89 |
+
xh = dequantize_params(d, dmin, ls, lm, codes)
|
| 90 |
+
e = np.asarray(x, dtype=np.float64) - xh.astype(np.float64)
|
| 91 |
+
sse = float(np.dot(np.asarray(w, dtype=np.float64), e * e)) if w is not None else float(np.dot(e, e))
|
| 92 |
+
return sse + spectral_penalty(e, cfg, dc_carry), sse
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def refine_codes_hadamard(
|
| 96 |
+
x: np.ndarray,
|
| 97 |
+
w: np.ndarray | None,
|
| 98 |
+
d,
|
| 99 |
+
dmin,
|
| 100 |
+
ls: np.ndarray,
|
| 101 |
+
lm: np.ndarray,
|
| 102 |
+
codes: np.ndarray,
|
| 103 |
+
cfg: HPCConfig,
|
| 104 |
+
dc_carry: float = 0.0,
|
| 105 |
+
) -> np.ndarray:
|
| 106 |
+
"""Deterministic greedy descent of ``4*vesica^2 + (DC + carry)^2``.
|
| 107 |
+
|
| 108 |
+
Same acceptance rule as the C Phase-4 shaper, but the whole 256-element
|
| 109 |
+
search is evaluated as four vectorised candidate matrices per pass.
|
| 110 |
+
"""
|
| 111 |
+
x = np.asarray(x, dtype=np.float64)
|
| 112 |
+
out = np.asarray(codes, dtype=np.uint8).copy()
|
| 113 |
+
ww = np.ones(QK_K, dtype=np.float64) if w is None else np.asarray(w, dtype=np.float64)
|
| 114 |
+
d_sub = np.repeat(np.float32(d) * np.asarray(ls, dtype=np.float32), 16).astype(np.float64)
|
| 115 |
+
m_sub = np.repeat(np.float32(dmin) * np.asarray(lm, dtype=np.float32), 16).astype(np.float64)
|
| 116 |
+
active = d_sub >= 1.0e-15
|
| 117 |
+
if not np.any(active):
|
| 118 |
+
return out
|
| 119 |
+
|
| 120 |
+
xh = d_sub * out.astype(np.float64) - m_sub
|
| 121 |
+
e_live = x - xh
|
| 122 |
+
sse_live = float(np.dot(ww, e_live * e_live))
|
| 123 |
+
sse_cap = sse_live * (1.0 + cfg.sse_budget)
|
| 124 |
+
v_live = e_live[: QK_K // 2] + e_live[QK_K // 2 :]
|
| 125 |
+
vesica = float(np.dot(v_live, v_live))
|
| 126 |
+
dc = float(np.sum(e_live)) + dc_carry
|
| 127 |
+
metric = 4.0 * vesica + dc * dc
|
| 128 |
+
|
| 129 |
+
idx = np.arange(QK_K)
|
| 130 |
+
pi = np.where(idx < QK_K // 2, idx, idx - QK_K // 2)
|
| 131 |
+
for _ in range(QK_K):
|
| 132 |
+
best_delta = 0.0
|
| 133 |
+
best_k = -1
|
| 134 |
+
best_q = 0
|
| 135 |
+
for q_try in range(4):
|
| 136 |
+
e_new = x - (d_sub * q_try - m_sub)
|
| 137 |
+
delta_e = e_new - e_live
|
| 138 |
+
sse_alt = sse_live + ww * (e_new * e_new - e_live * e_live)
|
| 139 |
+
mask = (sse_alt <= sse_cap) & active & (out != q_try)
|
| 140 |
+
if not np.any(mask):
|
| 141 |
+
continue
|
| 142 |
+
v_new = v_live[pi] + delta_e
|
| 143 |
+
vesica_alt = vesica - v_live[pi] * v_live[pi] + v_new * v_new
|
| 144 |
+
dc_alt = dc + delta_e
|
| 145 |
+
delta = metric - (4.0 * vesica_alt + dc_alt * dc_alt)
|
| 146 |
+
delta = np.where(mask, delta, -np.inf)
|
| 147 |
+
k = int(np.argmax(delta))
|
| 148 |
+
if delta[k] > best_delta:
|
| 149 |
+
best_delta = float(delta[k])
|
| 150 |
+
best_k = k
|
| 151 |
+
best_q = q_try
|
| 152 |
+
if best_k < 0:
|
| 153 |
+
break
|
| 154 |
+
old_e = e_live[best_k]
|
| 155 |
+
new_e = x[best_k] - (d_sub[best_k] * best_q - m_sub[best_k])
|
| 156 |
+
de = new_e - old_e
|
| 157 |
+
p = pi[best_k]
|
| 158 |
+
v_live[p] += de
|
| 159 |
+
vesica += v_live[p] * v_live[p] - (v_live[p] - de) * (v_live[p] - de)
|
| 160 |
+
dc += de
|
| 161 |
+
metric = 4.0 * vesica + dc * dc
|
| 162 |
+
sse_live += ww[best_k] * (new_e * new_e - old_e * old_e)
|
| 163 |
+
e_live[best_k] = new_e
|
| 164 |
+
out[best_k] = np.uint8(best_q)
|
| 165 |
+
return out
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def dc_nudge_codes(
|
| 169 |
+
x: np.ndarray,
|
| 170 |
+
d,
|
| 171 |
+
dmin,
|
| 172 |
+
ls: np.ndarray,
|
| 173 |
+
lm: np.ndarray,
|
| 174 |
+
codes: np.ndarray,
|
| 175 |
+
w: np.ndarray | None,
|
| 176 |
+
carry: float,
|
| 177 |
+
cfg: HPCConfig,
|
| 178 |
+
max_passes: int = 4,
|
| 179 |
+
) -> np.ndarray:
|
| 180 |
+
"""Spend a bounded SSE budget to cancel the rolling DC residual.
|
| 181 |
+
|
| 182 |
+
Equivalent in spirit to Phase 4.8 of the C engine: the reconstruction
|
| 183 |
+
target is always the true weight, only the codeword choice changes.
|
| 184 |
+
"""
|
| 185 |
+
x = np.asarray(x, dtype=np.float64)
|
| 186 |
+
out = np.asarray(codes, dtype=np.uint8).copy()
|
| 187 |
+
ww = np.ones(QK_K, dtype=np.float64) if w is None else np.asarray(w, dtype=np.float64)
|
| 188 |
+
d_sub = np.repeat(np.float32(d) * np.asarray(ls, dtype=np.float32), 16).astype(np.float64)
|
| 189 |
+
m_sub = np.repeat(np.float32(dmin) * np.asarray(lm, dtype=np.float32), 16).astype(np.float64)
|
| 190 |
+
active = d_sub >= 1.0e-15
|
| 191 |
+
if not np.any(active):
|
| 192 |
+
return out
|
| 193 |
+
e = x - (d_sub * out.astype(np.float64) - m_sub)
|
| 194 |
+
sse = float(np.dot(ww, e * e))
|
| 195 |
+
cap = sse * (1.0 + cfg.sse_budget)
|
| 196 |
+
residual = float(np.sum(e)) + carry
|
| 197 |
+
for _ in range(max_passes):
|
| 198 |
+
best_improvement = 0.0
|
| 199 |
+
best_k = -1
|
| 200 |
+
best_q = 0
|
| 201 |
+
for q_try in range(4):
|
| 202 |
+
e_new = x - (d_sub * q_try - m_sub)
|
| 203 |
+
mask = active & (out != q_try)
|
| 204 |
+
sse_alt = sse + ww * (e_new * e_new - e * e)
|
| 205 |
+
mask &= sse_alt <= cap
|
| 206 |
+
if not np.any(mask):
|
| 207 |
+
continue
|
| 208 |
+
improvement = np.abs(residual) - np.abs(residual + (e_new - e))
|
| 209 |
+
improvement = np.where(mask, improvement, -np.inf)
|
| 210 |
+
k = int(np.argmax(improvement))
|
| 211 |
+
if improvement[k] > best_improvement:
|
| 212 |
+
best_improvement = float(improvement[k])
|
| 213 |
+
best_k = k
|
| 214 |
+
best_q = q_try
|
| 215 |
+
if best_k < 0:
|
| 216 |
+
break
|
| 217 |
+
old_e = e[best_k]
|
| 218 |
+
new_e = x[best_k] - (d_sub[best_k] * best_q - m_sub[best_k])
|
| 219 |
+
de = new_e - old_e
|
| 220 |
+
residual += de
|
| 221 |
+
sse += ww[best_k] * (new_e * new_e - old_e * old_e)
|
| 222 |
+
e[best_k] = new_e
|
| 223 |
+
out[best_k] = np.uint8(best_q)
|
| 224 |
+
if abs(residual) < 1.0e-12:
|
| 225 |
+
break
|
| 226 |
+
return out
|
viterbi.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""36-state Viterbi selection over the Q2 symbolic lattice.
|
| 2 |
+
|
| 3 |
+
This is a direct port of Phase 3 of the C engine. Each graph block owns a
|
| 4 |
+
``6 x 6`` coarse/fine lattice; candidate errors are reduced to a free-energy
|
| 5 |
+
per bin, the Sieve marginals become a log-prior bonus, and an exact DP finds
|
| 6 |
+
the minimum-cost state sequence with a Manhattan transition penalty.
|
| 7 |
+
|
| 8 |
+
The forward pass is vectorised over the 36 states instead of the C engine's
|
| 9 |
+
triple nested scalar loop.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
from .config import HPCConfig
|
| 17 |
+
from .factored import CAND_TO_QUHIT, N_CAND_M, TOTAL_CANDIDATES
|
| 18 |
+
|
| 19 |
+
N_STATES = 36
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _median_upper(values: np.ndarray) -> float:
|
| 23 |
+
arr = np.asarray(values)
|
| 24 |
+
if arr.size == 0:
|
| 25 |
+
return 0.0
|
| 26 |
+
k = arr.size // 2
|
| 27 |
+
return float(np.partition(arr, k)[k])
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def robust_temperature(
|
| 31 |
+
candidate_errors: np.ndarray, cfg: HPCConfig
|
| 32 |
+
) -> float:
|
| 33 |
+
n_blocks = candidate_errors.shape[0]
|
| 34 |
+
if n_blocks <= 0:
|
| 35 |
+
return 1.0e-10
|
| 36 |
+
step = max(1, n_blocks // cfg.temperature_sample_blocks)
|
| 37 |
+
medians = []
|
| 38 |
+
for b in range(0, n_blocks, step):
|
| 39 |
+
row = candidate_errors[b]
|
| 40 |
+
finite = row[np.isfinite(row)]
|
| 41 |
+
if finite.size == 0:
|
| 42 |
+
continue
|
| 43 |
+
medians.append(_median_upper(np.maximum(0.0, row - finite.min())))
|
| 44 |
+
if len(medians) >= cfg.temperature_sample_blocks:
|
| 45 |
+
break
|
| 46 |
+
t = _median_upper(np.asarray(medians))
|
| 47 |
+
return t if np.isfinite(t) and t > 1.0e-10 else 1.0e-10
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _bin_geometry():
|
| 51 |
+
di = np.arange(TOTAL_CANDIDATES) // N_CAND_M
|
| 52 |
+
mi = np.arange(TOTAL_CANDIDATES) % N_CAND_M
|
| 53 |
+
qi_d = CAND_TO_QUHIT[di]
|
| 54 |
+
qi_m = CAND_TO_QUHIT[mi]
|
| 55 |
+
return qi_d, qi_m, qi_d * 6 + qi_m
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def viterbi_select(
|
| 59 |
+
candidate_errors: np.ndarray,
|
| 60 |
+
coarse_marg: np.ndarray,
|
| 61 |
+
fine_marg: np.ndarray,
|
| 62 |
+
temperature: float,
|
| 63 |
+
stride: int,
|
| 64 |
+
graph_blocks: int,
|
| 65 |
+
cfg: HPCConfig,
|
| 66 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 67 |
+
n_blocks = candidate_errors.shape[0]
|
| 68 |
+
qi_d, qi_m, bins = _bin_geometry()
|
| 69 |
+
bin_err = np.full((graph_blocks, N_STATES), np.inf, dtype=np.float64)
|
| 70 |
+
bin_cand = np.full((graph_blocks, N_STATES), -1, dtype=np.int64)
|
| 71 |
+
log_prior = np.zeros((graph_blocks, N_STATES), dtype=np.float64)
|
| 72 |
+
temp = max(float(temperature), 1.0e-12)
|
| 73 |
+
|
| 74 |
+
for gi in range(graph_blocks):
|
| 75 |
+
start = gi * stride
|
| 76 |
+
stop = min((gi + 1) * stride, n_blocks)
|
| 77 |
+
group = candidate_errors[start:stop].astype(np.float64)
|
| 78 |
+
flat = group.ravel()
|
| 79 |
+
finite = np.isfinite(flat)
|
| 80 |
+
group_min = np.full(N_STATES, np.inf)
|
| 81 |
+
if np.any(finite):
|
| 82 |
+
np.minimum.at(group_min, bins[finite], flat[finite])
|
| 83 |
+
counts = np.bincount(bins[finite], minlength=N_STATES)
|
| 84 |
+
sums = np.zeros(N_STATES, dtype=np.float64)
|
| 85 |
+
if np.any(finite):
|
| 86 |
+
shifted = flat[finite] - group_min[bins[finite]]
|
| 87 |
+
amps = np.exp(-shifted / (2.0 * temp))
|
| 88 |
+
sums = np.bincount(bins[finite], weights=amps, minlength=N_STATES)
|
| 89 |
+
valid = (counts > 0) & np.isfinite(group_min)
|
| 90 |
+
if np.any(valid):
|
| 91 |
+
mean_exp = np.maximum(sums[valid] / counts[valid], 1.0e-300)
|
| 92 |
+
bin_err[gi, valid] = group_min[valid] - 2.0 * temp * np.log(mean_exp)
|
| 93 |
+
|
| 94 |
+
rep = candidate_errors[start].astype(np.float64)
|
| 95 |
+
for s in range(N_STATES):
|
| 96 |
+
idx = np.where((bins == s) & np.isfinite(rep))[0]
|
| 97 |
+
if idx.size:
|
| 98 |
+
bin_cand[gi, s] = int(idx[int(np.argmin(rep[idx]))])
|
| 99 |
+
|
| 100 |
+
c_tot = float(coarse_marg[gi].sum())
|
| 101 |
+
f_tot = float(fine_marg[gi].sum())
|
| 102 |
+
pc = coarse_marg[gi] / c_tot if c_tot > 1.0e-30 else np.full(6, 1.0 / 6.0)
|
| 103 |
+
pf = fine_marg[gi] / f_tot if f_tot > 1.0e-30 else np.full(6, 1.0 / 6.0)
|
| 104 |
+
for s in range(N_STATES):
|
| 105 |
+
log_prior[gi, s] = np.log(pc[s // 6] + 1.0e-30) + np.log(pf[s % 6] + 1.0e-30)
|
| 106 |
+
|
| 107 |
+
finite_err = bin_err[np.isfinite(bin_err)]
|
| 108 |
+
scale_err = float(finite_err.mean()) if finite_err.size else 1.0e-10
|
| 109 |
+
scale_err = max(scale_err, 1.0e-20)
|
| 110 |
+
|
| 111 |
+
local = np.where(
|
| 112 |
+
np.isfinite(bin_err),
|
| 113 |
+
bin_err - cfg.viterbi_beta * scale_err * log_prior,
|
| 114 |
+
np.inf,
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
state_d = np.arange(N_STATES) // 6
|
| 118 |
+
state_m = np.arange(N_STATES) % 6
|
| 119 |
+
trans = np.empty((N_STATES, N_STATES), dtype=np.float64)
|
| 120 |
+
for sp in range(N_STATES):
|
| 121 |
+
trans[sp, :] = cfg.viterbi_alpha * scale_err * (
|
| 122 |
+
np.abs(state_d - state_d[sp]) + np.abs(state_m - state_m[sp])
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
dp = np.full((graph_blocks, N_STATES), np.inf, dtype=np.float64)
|
| 126 |
+
back = np.zeros((graph_blocks, N_STATES), dtype=np.int64)
|
| 127 |
+
dp[0] = local[0]
|
| 128 |
+
for gi in range(1, graph_blocks):
|
| 129 |
+
totals = dp[gi - 1][:, None] + trans
|
| 130 |
+
best = np.argmin(totals, axis=0)
|
| 131 |
+
best_val = totals[best, np.arange(N_STATES)]
|
| 132 |
+
dp[gi] = np.where(np.isfinite(best_val), best_val + local[gi], np.inf)
|
| 133 |
+
back[gi] = best
|
| 134 |
+
|
| 135 |
+
if not np.any(np.isfinite(dp[-1])):
|
| 136 |
+
path = np.zeros(graph_blocks, dtype=np.int64)
|
| 137 |
+
else:
|
| 138 |
+
path = np.zeros(graph_blocks, dtype=np.int64)
|
| 139 |
+
path[-1] = int(np.argmin(dp[-1]))
|
| 140 |
+
for gi in range(graph_blocks - 2, -1, -1):
|
| 141 |
+
path[gi] = back[gi + 1, path[gi + 1]]
|
| 142 |
+
|
| 143 |
+
best_candidate = np.zeros(n_blocks, dtype=np.int64)
|
| 144 |
+
for gi in range(graph_blocks):
|
| 145 |
+
s = int(path[gi])
|
| 146 |
+
qd, qm = s // 6, s % 6
|
| 147 |
+
rep_block = gi * stride
|
| 148 |
+
if bin_cand[gi, s] >= 0:
|
| 149 |
+
best_candidate[rep_block] = bin_cand[gi, s]
|
| 150 |
+
start = rep_block + 1
|
| 151 |
+
stop = min((gi + 1) * stride, n_blocks)
|
| 152 |
+
for b in range(start, stop):
|
| 153 |
+
mask = (qi_d == qd) & (qi_m == qm)
|
| 154 |
+
idx = np.where(mask)[0]
|
| 155 |
+
if idx.size:
|
| 156 |
+
best_candidate[b] = int(idx[int(np.argmin(candidate_errors[b, idx]))])
|
| 157 |
+
|
| 158 |
+
for b in range(n_blocks):
|
| 159 |
+
cur = candidate_errors[b, best_candidate[b]]
|
| 160 |
+
g = int(np.argmin(candidate_errors[b]))
|
| 161 |
+
if candidate_errors[b, g] < cur * cfg.greedy_override_ratio:
|
| 162 |
+
best_candidate[b] = g
|
| 163 |
+
|
| 164 |
+
return best_candidate, path
|