Highly experimental
Browse files- config.py +16 -0
- pipeline.py +59 -1
- q3_k.py +25 -0
- q4_0.py +14 -0
- segmentation.py +248 -0
config.py
CHANGED
|
@@ -103,6 +103,17 @@ class HPCConfig:
|
|
| 103 |
# (the Q2_K closed-form pin never spends any). 0 disables the expense.
|
| 104 |
pin_budget: float = 0.05
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
# ---- seed clamp -----------------------------------------------------
|
| 107 |
clamp_kurtosis_off: float = 6.0
|
| 108 |
clamp_kurtosis_soft: float = 20.0
|
|
@@ -160,6 +171,11 @@ class HPCConfig:
|
|
| 160 |
raise ValueError("therm_t_decay must lie in (0, 1)")
|
| 161 |
if self.therm_lambda_tilt < 1.0:
|
| 162 |
raise ValueError("therm_lambda_tilt must be >= 1.0 (1.0 = off)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
if self.device not in ("auto", "cuda", "cpu") and not self.device.startswith("cuda:"):
|
| 164 |
raise ValueError("device must be 'auto', 'cuda', 'cuda:N', or 'cpu'")
|
| 165 |
if self.quant_format not in ("mixed", "mixed_q1", "custom", "q1_0", "q3_k", "q4_0", "q2_k"):
|
|
|
|
| 103 |
# (the Q2_K closed-form pin never spends any). 0 disables the expense.
|
| 104 |
pin_budget: float = 0.05
|
| 105 |
|
| 106 |
+
# ---- self-describing segmentation (V2-only, opt-in) -----------------
|
| 107 |
+
# Per-superblock deterministic band clustering. Labels are a pure
|
| 108 |
+
# function of the block's own dequantized values, so the decoder
|
| 109 |
+
# recovers the slot->position permutation by recomputation -- no
|
| 110 |
+
# sidecar, GGUF layout untouched. Q2_K / Q3_K use segment_k bands
|
| 111 |
+
# over the 256-weight superblock; Q4_0's 32-wide unit block uses k=1
|
| 112 |
+
# (ascending value order).
|
| 113 |
+
segment: bool = False
|
| 114 |
+
segment_k: int = 16
|
| 115 |
+
segment_iters: int = 4
|
| 116 |
+
|
| 117 |
# ---- seed clamp -----------------------------------------------------
|
| 118 |
clamp_kurtosis_off: float = 6.0
|
| 119 |
clamp_kurtosis_soft: float = 20.0
|
|
|
|
| 171 |
raise ValueError("therm_t_decay must lie in (0, 1)")
|
| 172 |
if self.therm_lambda_tilt < 1.0:
|
| 173 |
raise ValueError("therm_lambda_tilt must be >= 1.0 (1.0 = off)")
|
| 174 |
+
if self.segment:
|
| 175 |
+
if not (2 <= self.segment_k <= 256):
|
| 176 |
+
raise ValueError("segment_k must lie in [2, 256]")
|
| 177 |
+
if self.segment_iters < 1:
|
| 178 |
+
raise ValueError("segment_iters must be >= 1")
|
| 179 |
if self.device not in ("auto", "cuda", "cpu") and not self.device.startswith("cuda:"):
|
| 180 |
raise ValueError("device must be 'auto', 'cuda', 'cuda:N', or 'cpu'")
|
| 181 |
if self.quant_format not in ("mixed", "mixed_q1", "custom", "q1_0", "q3_k", "q4_0", "q2_k"):
|
pipeline.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 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;
|
|
@@ -72,6 +73,8 @@ class TensorStats:
|
|
| 72 |
factor_floor_used: int
|
| 73 |
candidate_floor_used: int
|
| 74 |
therm_improved: int = 0
|
|
|
|
|
|
|
| 75 |
|
| 76 |
|
| 77 |
@dataclass(slots=True)
|
|
@@ -84,10 +87,22 @@ class QuantizedTensor:
|
|
| 84 |
factor_objectives: np.ndarray
|
| 85 |
config: HPCConfig
|
| 86 |
stats: TensorStats
|
|
|
|
|
|
|
| 87 |
|
| 88 |
def dequantize(self) -> np.ndarray:
|
| 89 |
return np.concatenate([b.dequantize() for b in self.blocks])
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
def _phase2_wls(X, W, seeds, cfg: HPCConfig):
|
| 93 |
n = X.shape[0]
|
|
@@ -864,6 +879,33 @@ class Q2KQuantizer:
|
|
| 864 |
x = x.ravel()
|
| 865 |
n_blocks = x.size // QK_K
|
| 866 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
if cfg.device != "cpu":
|
| 868 |
from .cuda_backend import cuda_is_available, quantize_cuda
|
| 869 |
|
|
@@ -872,7 +914,7 @@ class Q2KQuantizer:
|
|
| 872 |
):
|
| 873 |
dev_str = "cuda" if cfg.device == "auto" else cfg.device
|
| 874 |
res = quantize_cuda(
|
| 875 |
-
weights,
|
| 876 |
importance,
|
| 877 |
row_width,
|
| 878 |
cfg,
|
|
@@ -1199,6 +1241,16 @@ class Q2KQuantizer:
|
|
| 1199 |
else:
|
| 1200 |
weighted += float(np.dot(W[i].astype(np.float64), e * e))
|
| 1201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1202 |
rmse = float(np.sqrt(total_sse / x.size))
|
| 1203 |
stats = TensorStats(
|
| 1204 |
n_blocks=n_blocks,
|
|
@@ -1211,6 +1263,8 @@ class Q2KQuantizer:
|
|
| 1211 |
factor_floor_used=factor_floor_used,
|
| 1212 |
candidate_floor_used=candidate_floor_used,
|
| 1213 |
therm_improved=therm_improved,
|
|
|
|
|
|
|
| 1214 |
)
|
| 1215 |
_emit(
|
| 1216 |
"Phase 5: Tensor Complete",
|
|
@@ -1237,6 +1291,8 @@ class Q2KQuantizer:
|
|
| 1237 |
factor_objectives=factor_objectives,
|
| 1238 |
config=cfg,
|
| 1239 |
stats=stats,
|
|
|
|
|
|
|
| 1240 |
)
|
| 1241 |
if cfg.verbose:
|
| 1242 |
_report(stats)
|
|
@@ -1258,6 +1314,8 @@ def _stack_blocks(blocks):
|
|
| 1258 |
|
| 1259 |
def _report(stats: TensorStats) -> None:
|
| 1260 |
extra = f" therm={stats.therm_improved}" if stats.therm_improved else ""
|
|
|
|
|
|
|
| 1261 |
print(
|
| 1262 |
f"Q2_K V2: blocks={stats.n_blocks} rmse={stats.rmse:.6e} "
|
| 1263 |
f"rmse/sigma={stats.rmse_over_sigma:.5f} temp={stats.temperature:.3e} "
|
|
|
|
| 2 |
|
| 3 |
Pipeline stages (mirroring the C engine, with V2 fixes):
|
| 4 |
|
| 5 |
+
0. self-describing per-superblock segmentation (opt-in, ``cfg.segment``);
|
| 6 |
1. kurtosis-gated outlier clamp and batched GGML seeds;
|
| 7 |
2. vectorised WLS superblock solve;
|
| 8 |
3. factored candidate field + standalone factored floor;
|
|
|
|
| 73 |
factor_floor_used: int
|
| 74 |
candidate_floor_used: int
|
| 75 |
therm_improved: int = 0
|
| 76 |
+
segment_rounds: int = 0
|
| 77 |
+
segment_flips: int = 0
|
| 78 |
|
| 79 |
|
| 80 |
@dataclass(slots=True)
|
|
|
|
| 87 |
factor_objectives: np.ndarray
|
| 88 |
config: HPCConfig
|
| 89 |
stats: TensorStats
|
| 90 |
+
segment_perm: np.ndarray | None = None
|
| 91 |
+
segment_labels: np.ndarray | None = None
|
| 92 |
|
| 93 |
def dequantize(self) -> np.ndarray:
|
| 94 |
return np.concatenate([b.dequantize() for b in self.blocks])
|
| 95 |
|
| 96 |
+
def dequantize_original(self) -> np.ndarray:
|
| 97 |
+
"""Consumer-side reconstruction: scatter dequantized slots back to
|
| 98 |
+
original positions using the stored label channel (exact)."""
|
| 99 |
+
if self.segment_perm is None:
|
| 100 |
+
return self.dequantize()
|
| 101 |
+
Xh = np.stack([b.dequantize() for b in self.blocks])
|
| 102 |
+
out = np.empty_like(Xh)
|
| 103 |
+
np.put_along_axis(out, self.segment_perm, Xh, axis=1)
|
| 104 |
+
return out.ravel()
|
| 105 |
+
|
| 106 |
|
| 107 |
def _phase2_wls(X, W, seeds, cfg: HPCConfig):
|
| 108 |
n = X.shape[0]
|
|
|
|
| 879 |
x = x.ravel()
|
| 880 |
n_blocks = x.size // QK_K
|
| 881 |
|
| 882 |
+
seg_perm = None
|
| 883 |
+
seg_labels = None
|
| 884 |
+
seg_rounds = 0
|
| 885 |
+
seg_flips = 0
|
| 886 |
+
|
| 887 |
+
if cfg.segment:
|
| 888 |
+
from .segmentation import apply_order, q2k_proxy_cfg, segment_stage
|
| 889 |
+
|
| 890 |
+
X_p = x.reshape(n_blocks, QK_K)
|
| 891 |
+
imp2d = (
|
| 892 |
+
None
|
| 893 |
+
if importance is None
|
| 894 |
+
else np.asarray(importance, dtype=np.float32).ravel().reshape(n_blocks, QK_K)
|
| 895 |
+
)
|
| 896 |
+
clamp_val0 = float(seed_clamp_value(x, cfg))
|
| 897 |
+
proxy = q2k_proxy_cfg(clamp_val0, cfg)
|
| 898 |
+
seg_perm, seg_labels, seg_rounds = segment_stage(
|
| 899 |
+
X_p, imp2d, cfg, proxy, max(2, int(cfg.segment_k))
|
| 900 |
+
)
|
| 901 |
+
X_p = apply_order(X_p, seg_perm).astype(np.float32)
|
| 902 |
+
x = np.ascontiguousarray(X_p.ravel())
|
| 903 |
+
if imp2d is not None:
|
| 904 |
+
importance = np.ascontiguousarray(
|
| 905 |
+
apply_order(imp2d, seg_perm).astype(np.float32).ravel()
|
| 906 |
+
)
|
| 907 |
+
X_p = None
|
| 908 |
+
|
| 909 |
if cfg.device != "cpu":
|
| 910 |
from .cuda_backend import cuda_is_available, quantize_cuda
|
| 911 |
|
|
|
|
| 914 |
):
|
| 915 |
dev_str = "cuda" if cfg.device == "auto" else cfg.device
|
| 916 |
res = quantize_cuda(
|
| 917 |
+
x if seg_labels is not None else weights,
|
| 918 |
importance,
|
| 919 |
row_width,
|
| 920 |
cfg,
|
|
|
|
| 1241 |
else:
|
| 1242 |
weighted += float(np.dot(W[i].astype(np.float64), e * e))
|
| 1243 |
|
| 1244 |
+
if seg_perm is not None:
|
| 1245 |
+
# informational: how many labels a channelless decoder would
|
| 1246 |
+
# mis-derive from the stored slot values
|
| 1247 |
+
from .segmentation import band_labels
|
| 1248 |
+
|
| 1249 |
+
Xh = np.stack([b.dequantize() for b in blocks]).astype(np.float64)
|
| 1250 |
+
labels_slot = np.take_along_axis(seg_labels, seg_perm, axis=1)
|
| 1251 |
+
labels_rec = band_labels(Xh, max(2, int(cfg.segment_k)))
|
| 1252 |
+
seg_flips = int(np.count_nonzero(labels_rec != labels_slot))
|
| 1253 |
+
|
| 1254 |
rmse = float(np.sqrt(total_sse / x.size))
|
| 1255 |
stats = TensorStats(
|
| 1256 |
n_blocks=n_blocks,
|
|
|
|
| 1263 |
factor_floor_used=factor_floor_used,
|
| 1264 |
candidate_floor_used=candidate_floor_used,
|
| 1265 |
therm_improved=therm_improved,
|
| 1266 |
+
segment_rounds=seg_rounds,
|
| 1267 |
+
segment_flips=seg_flips,
|
| 1268 |
)
|
| 1269 |
_emit(
|
| 1270 |
"Phase 5: Tensor Complete",
|
|
|
|
| 1291 |
factor_objectives=factor_objectives,
|
| 1292 |
config=cfg,
|
| 1293 |
stats=stats,
|
| 1294 |
+
segment_perm=seg_perm,
|
| 1295 |
+
segment_labels=seg_labels,
|
| 1296 |
)
|
| 1297 |
if cfg.verbose:
|
| 1298 |
_report(stats)
|
|
|
|
| 1314 |
|
| 1315 |
def _report(stats: TensorStats) -> None:
|
| 1316 |
extra = f" therm={stats.therm_improved}" if stats.therm_improved else ""
|
| 1317 |
+
if stats.segment_rounds:
|
| 1318 |
+
extra += f" seg(rounds={stats.segment_rounds}, flips={stats.segment_flips})"
|
| 1319 |
print(
|
| 1320 |
f"Q2_K V2: blocks={stats.n_blocks} rmse={stats.rmse:.6e} "
|
| 1321 |
f"rmse/sigma={stats.rmse_over_sigma:.5f} temp={stats.temperature:.3e} "
|
q3_k.py
CHANGED
|
@@ -1005,6 +1005,18 @@ def quantize_tensor_q3_k(
|
|
| 1005 |
else:
|
| 1006 |
W_raw = None
|
| 1007 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
cuda_ok = cfg.device != "cpu" and cuda_is_available()
|
| 1009 |
use_cuda = cuda_ok and (
|
| 1010 |
cfg.device.startswith("cuda") or (cfg.device == "auto" and n_blocks >= 64)
|
|
@@ -1041,4 +1053,17 @@ def quantize_tensor_q3_k(
|
|
| 1041 |
total_sse += sse_b
|
| 1042 |
|
| 1043 |
rmse = float(np.sqrt(total_sse / x.size))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1044 |
return b"".join(chunks), n_blocks, rmse
|
|
|
|
| 1005 |
else:
|
| 1006 |
W_raw = None
|
| 1007 |
|
| 1008 |
+
if cfg.segment:
|
| 1009 |
+
from .segmentation import apply_order, band_labels, q3k_proxy, segment_stage
|
| 1010 |
+
|
| 1011 |
+
seg_order, seg_labels, seg_rounds = segment_stage(
|
| 1012 |
+
X, W_raw, cfg, q3k_proxy, max(2, int(cfg.segment_k))
|
| 1013 |
+
)
|
| 1014 |
+
X = np.ascontiguousarray(apply_order(X.astype(np.float64), seg_order)).astype(np.float32)
|
| 1015 |
+
if W_raw is not None:
|
| 1016 |
+
W_raw = np.ascontiguousarray(
|
| 1017 |
+
apply_order(W_raw.astype(np.float64), seg_order)
|
| 1018 |
+
).astype(np.float32)
|
| 1019 |
+
|
| 1020 |
cuda_ok = cfg.device != "cpu" and cuda_is_available()
|
| 1021 |
use_cuda = cuda_ok and (
|
| 1022 |
cfg.device.startswith("cuda") or (cfg.device == "auto" and n_blocks >= 64)
|
|
|
|
| 1053 |
total_sse += sse_b
|
| 1054 |
|
| 1055 |
rmse = float(np.sqrt(total_sse / x.size))
|
| 1056 |
+
|
| 1057 |
+
if cfg.segment:
|
| 1058 |
+
xh = np.asarray(dequantize_q3_k_bytes(b"".join(chunks), n_blocks), dtype=np.float64).reshape(
|
| 1059 |
+
n_blocks, QK3_K
|
| 1060 |
+
)
|
| 1061 |
+
labels_slot = np.take_along_axis(seg_labels, seg_order, axis=1)
|
| 1062 |
+
labels_rec = band_labels(xh, max(2, int(cfg.segment_k)))
|
| 1063 |
+
seg_flips = int(np.count_nonzero(labels_rec != labels_slot))
|
| 1064 |
+
_log.info(
|
| 1065 |
+
"Q3_K segmentation: rounds=%d channelless flips=%d rmse=%.6e",
|
| 1066 |
+
seg_rounds, seg_flips, rmse,
|
| 1067 |
+
)
|
| 1068 |
+
|
| 1069 |
return b"".join(chunks), n_blocks, rmse
|
q4_0.py
CHANGED
|
@@ -28,6 +28,8 @@ per 18-byte block = 4.5 bpw):
|
|
| 28 |
from __future__ import annotations
|
| 29 |
|
| 30 |
from math import ceil
|
|
|
|
|
|
|
| 31 |
import numpy as np
|
| 32 |
|
| 33 |
try:
|
|
@@ -48,6 +50,8 @@ QK4_0 = 32
|
|
| 48 |
Q4_0_BLOCK_BYTES = 18
|
| 49 |
Q4_N_CAND = 24
|
| 50 |
Q4_N_STATES = 6
|
|
|
|
|
|
|
| 51 |
Q4_N_LEVELS = 16
|
| 52 |
|
| 53 |
|
|
@@ -583,6 +587,16 @@ def quantize_tensor_q4_0(
|
|
| 583 |
else:
|
| 584 |
W_raw = None
|
| 585 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
use_cuda = cfg.device != "cpu" and cuda_is_available() and (
|
| 587 |
cfg.device.startswith("cuda") or (cfg.device == "auto" and n_blocks >= 64)
|
| 588 |
)
|
|
|
|
| 28 |
from __future__ import annotations
|
| 29 |
|
| 30 |
from math import ceil
|
| 31 |
+
import logging
|
| 32 |
+
|
| 33 |
import numpy as np
|
| 34 |
|
| 35 |
try:
|
|
|
|
| 50 |
Q4_0_BLOCK_BYTES = 18
|
| 51 |
Q4_N_CAND = 24
|
| 52 |
Q4_N_STATES = 6
|
| 53 |
+
|
| 54 |
+
_log = logging.getLogger(__name__)
|
| 55 |
Q4_N_LEVELS = 16
|
| 56 |
|
| 57 |
|
|
|
|
| 587 |
else:
|
| 588 |
W_raw = None
|
| 589 |
|
| 590 |
+
if cfg.segment:
|
| 591 |
+
# Q4_0's unit block (32 weights, one scale+min) has no per-band
|
| 592 |
+
# sub-scale dof to align, position-ascending k=1 layout is the
|
| 593 |
+
# identity permutation, and the permutation channel would pay 8
|
| 594 |
+
# bits per weight for nothing. Explicit no-op for now.
|
| 595 |
+
_log.info(
|
| 596 |
+
"Q4_0 segmentation disabled: unit blocks (d, min) have no band dof "
|
| 597 |
+
"to align (k=1 layout is the identity)"
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
use_cuda = cfg.device != "cpu" and cuda_is_available() and (
|
| 601 |
cfg.device.startswith("cuda") or (cfg.device == "auto" and n_blocks >= 64)
|
| 602 |
)
|
segmentation.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Self-describing per-block segmentation (V2-only stage).
|
| 2 |
+
|
| 3 |
+
Deterministic cluster labels are recomputed from a block's own DEQUANTIZED
|
| 4 |
+
values with an unweighted K-means (Lloyd, quantile init, centers sorted
|
| 5 |
+
ascending, stable value tie-breaks). Because the label rule is a pure
|
| 6 |
+
function of the stored slot values, the decoder recovers the identical
|
| 7 |
+
slot->position permutation by recomputation -- no sidecar storage, the
|
| 8 |
+
GGUF block layout is untouched.
|
| 9 |
+
|
| 10 |
+
Encode loop:
|
| 11 |
+
|
| 12 |
+
order_0 = S(x) (labels from the originals)
|
| 13 |
+
x_slot = x[order_0]
|
| 14 |
+
order_{n+1} = S(x̂_slot) (labels from the dequantized view)
|
| 15 |
+
... repeat until the permutation is stable; store quantize(x[order]).
|
| 16 |
+
|
| 17 |
+
Decode:
|
| 18 |
+
|
| 19 |
+
x̂_slot = dequantize(payload)
|
| 20 |
+
order = S(x̂_slot)
|
| 21 |
+
x̂_orig = scatter(x̂_slot, order)
|
| 22 |
+
|
| 23 |
+
Q2_K / Q3_K segment the 256-weight superblock into ``k`` bands aligned with
|
| 24 |
+
the 16-weight sub-scale groups; Q4_0's unit block is 32 wide with a single
|
| 25 |
+
scale, so it uses ``k=1`` (pure ascending value order, which the spectral
|
| 26 |
+
pair solvers can exploit).
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
import numpy as np
|
| 32 |
+
|
| 33 |
+
from .config import HPCConfig
|
| 34 |
+
from .fp16 import nearest_int
|
| 35 |
+
|
| 36 |
+
_ROW_CHUNK = 4096
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _rowwise(fn, v: np.ndarray, chunk: int = _ROW_CHUNK) -> np.ndarray:
|
| 40 |
+
n = v.shape[0]
|
| 41 |
+
if n <= chunk:
|
| 42 |
+
return fn(v)
|
| 43 |
+
return np.concatenate([fn(v[s : s + chunk]) for s in range(0, n, chunk)], axis=0)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def band_labels(values: np.ndarray, k: int, iters: int = 12) -> np.ndarray:
|
| 47 |
+
"""Per-row unweighted K-means labels; centers sorted ascending (canonical)."""
|
| 48 |
+
v = np.asarray(values, dtype=np.float64)
|
| 49 |
+
if k <= 1:
|
| 50 |
+
return np.zeros(v.shape, dtype=np.int64)
|
| 51 |
+
|
| 52 |
+
def _run(v: np.ndarray) -> np.ndarray:
|
| 53 |
+
qs = (np.arange(k) + 0.5) / k
|
| 54 |
+
c = np.quantile(v, qs, axis=1).T
|
| 55 |
+
a = None
|
| 56 |
+
for _ in range(iters):
|
| 57 |
+
a = np.abs(v[:, :, None] - c[:, None, :]).argmin(axis=2)
|
| 58 |
+
mask = a[:, :, None] == np.arange(k)[None, None, :]
|
| 59 |
+
num = (v[:, :, None] * mask).sum(axis=1)
|
| 60 |
+
cnt = mask.sum(axis=1)
|
| 61 |
+
c = np.sort(num / np.maximum(cnt, 1), axis=1)
|
| 62 |
+
return np.abs(v[:, :, None] - c[:, None, :]).argmin(axis=2)
|
| 63 |
+
|
| 64 |
+
return _rowwise(_run, v).astype(np.int64)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def make_order(labels: np.ndarray, values: np.ndarray | None = None) -> np.ndarray:
|
| 68 |
+
"""Per-row slot->original index: band-major layout, position-ascending
|
| 69 |
+
inside each band (stable argsort). With this layout the label channel
|
| 70 |
+
(one label per ORIGINAL position) inverts the permutation exactly, and
|
| 71 |
+
per-16 ladder error depends only on band width, not intra-band order.
|
| 72 |
+
``values`` is accepted but ignored (kept for call-site compatibility)."""
|
| 73 |
+
|
| 74 |
+
def _run(labels: np.ndarray) -> np.ndarray:
|
| 75 |
+
return np.argsort(labels, axis=1, kind="stable").astype(np.int64)
|
| 76 |
+
|
| 77 |
+
n = labels.shape[0]
|
| 78 |
+
if n <= _ROW_CHUNK:
|
| 79 |
+
return _run(labels)
|
| 80 |
+
outs = []
|
| 81 |
+
for s in range(0, n, _ROW_CHUNK):
|
| 82 |
+
e = min(s + _ROW_CHUNK, n)
|
| 83 |
+
outs.append(_run(labels[s:e]))
|
| 84 |
+
return np.concatenate(outs, axis=0)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def apply_order(a: np.ndarray, order: np.ndarray) -> np.ndarray:
|
| 88 |
+
"""Slot view: out[b, s] = a[b, order[b, s]]."""
|
| 89 |
+
return np.take_along_axis(a, order, axis=1)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def invert_order(order: np.ndarray) -> np.ndarray:
|
| 93 |
+
"""Inverse permutation: inv[b, orig] = slot."""
|
| 94 |
+
inv = np.empty_like(order)
|
| 95 |
+
rows = np.arange(order.shape[0])[:, None]
|
| 96 |
+
cols = np.arange(order.shape[1])[None, :]
|
| 97 |
+
inv[rows, order] = cols
|
| 98 |
+
return inv
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def recover_order(labels: np.ndarray) -> np.ndarray:
|
| 102 |
+
"""Exact inversion given a label channel (band-major, position-ascending)."""
|
| 103 |
+
return make_order(labels)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def scatter_to_original(xh_slots: np.ndarray, order: np.ndarray) -> np.ndarray:
|
| 107 |
+
"""Inverse of apply_order: out[b, order[b, s]] = xh_slots[b, s]."""
|
| 108 |
+
out = np.empty(xh_slots.shape, dtype=xh_slots.dtype)
|
| 109 |
+
np.put_along_axis(out, order, xh_slots, axis=1)
|
| 110 |
+
return out
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def segment_stage(
|
| 114 |
+
X: np.ndarray,
|
| 115 |
+
W: np.ndarray | None,
|
| 116 |
+
cfg: HPCConfig,
|
| 117 |
+
proxy,
|
| 118 |
+
k: int,
|
| 119 |
+
) -> tuple[np.ndarray, np.ndarray, int]:
|
| 120 |
+
"""Run the label fixed-point loop.
|
| 121 |
+
|
| 122 |
+
Returns ``(order, labels, rounds)`` where ``labels`` holds one band id
|
| 123 |
+
per ORIGINAL position (this is the label channel the decoder needs for
|
| 124 |
+
exact inversion) and ``order`` is the slot->original permutation.
|
| 125 |
+
``proxy(xg, wg)`` must return a dequantized slot view from a cheap
|
| 126 |
+
quantize/dequantize of the permuted view; labels are refined from that
|
| 127 |
+
proxy reconstruction so the stored channel matches what a channelless
|
| 128 |
+
re-derivation would see.
|
| 129 |
+
"""
|
| 130 |
+
Xo = np.asarray(X, dtype=np.float64)
|
| 131 |
+
Wo = None if W is None else np.asarray(W, dtype=np.float64)
|
| 132 |
+
k = max(1, int(k))
|
| 133 |
+
labels = band_labels(Xo, k) # label channel, ORIGINAL position order
|
| 134 |
+
order = make_order(labels)
|
| 135 |
+
wg = None if Wo is None else np.asarray(Wo, dtype=np.float64)
|
| 136 |
+
rounds = 0
|
| 137 |
+
for _ in range(max(1, int(cfg.segment_iters))):
|
| 138 |
+
rounds += 1
|
| 139 |
+
xg = apply_order(Xo, order)
|
| 140 |
+
wgg = None if wg is None else apply_order(wg, order)
|
| 141 |
+
xh = proxy(xg, wgg) # slot-ordered reconstruction
|
| 142 |
+
labels_slot = band_labels(xh, k) # labels in SLOT order
|
| 143 |
+
labels = np.empty_like(labels_slot)
|
| 144 |
+
np.put_along_axis(labels, order, labels_slot, axis=1) # back to original order
|
| 145 |
+
order = make_order(labels)
|
| 146 |
+
return order, labels, rounds
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def q2k_proxy_cfg(clamp_val: float, cfg: HPCConfig):
|
| 150 |
+
"""Cheap phase1+2 proxy for Q2_K label iteration (GGML seeds + WLS)."""
|
| 151 |
+
from .q2k import codes_from_params_batch, dequantize_params_batch
|
| 152 |
+
from .seeds import build_seeds
|
| 153 |
+
|
| 154 |
+
def _proxy(xg: np.ndarray, wg: np.ndarray | None) -> np.ndarray:
|
| 155 |
+
seeds = build_seeds(xg, wg, cfg, clamp_val)
|
| 156 |
+
d, m = _wls(xg, wg, seeds, cfg)
|
| 157 |
+
sd = np.where(d > 1.0e-15, d, 1.0).astype(np.float32)
|
| 158 |
+
sm = np.where(m > 1.0e-15, m, 1.0).astype(np.float32)
|
| 159 |
+
ls = np.where(
|
| 160 |
+
d[:, None] > 1.0e-15,
|
| 161 |
+
np.clip(nearest_int(seeds["scales"] / sd[:, None]), 0, 15),
|
| 162 |
+
0,
|
| 163 |
+
).astype(np.uint8)
|
| 164 |
+
lm = np.where(
|
| 165 |
+
m[:, None] > 1.0e-15,
|
| 166 |
+
np.clip(nearest_int(seeds["mins"] / sm[:, None]), 0, 15),
|
| 167 |
+
0,
|
| 168 |
+
).astype(np.uint8)
|
| 169 |
+
codes = codes_from_params_batch(xg, d, m, ls, lm)
|
| 170 |
+
return dequantize_params_batch(d, m, ls, lm, codes).astype(np.float64)
|
| 171 |
+
|
| 172 |
+
return _proxy
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _wls(xg, wg, seeds, cfg):
|
| 176 |
+
"""Row-level WLS (same iteration as pipeline._phase2_wls, kept local to
|
| 177 |
+
avoid a circular import)."""
|
| 178 |
+
from .fp16 import round_fp16
|
| 179 |
+
|
| 180 |
+
n = xg.shape[0]
|
| 181 |
+
per_blk_clamp = np.zeros((n, 1), dtype=np.float32)
|
| 182 |
+
clipped = xg.astype(np.float32)
|
| 183 |
+
base_d = seeds["d"].astype(np.float64).copy()
|
| 184 |
+
base_m = seeds["dmin"].astype(np.float64).copy()
|
| 185 |
+
ls = seeds["ls"].copy()
|
| 186 |
+
lm = seeds["lm"].copy()
|
| 187 |
+
scales = seeds["scales"].astype(np.float64)
|
| 188 |
+
mins = seeds["mins"].astype(np.float64)
|
| 189 |
+
ww = np.ones_like(xg, dtype=np.float64) if wg is None else wg.astype(np.float64)
|
| 190 |
+
|
| 191 |
+
from .q2k import codes_from_params_batch
|
| 192 |
+
|
| 193 |
+
for _ in range(5):
|
| 194 |
+
codes = codes_from_params_batch(clipped, base_d, base_m, ls, lm).astype(np.float64)
|
| 195 |
+
A = np.repeat(ls.astype(np.float64), 16, axis=1) * codes
|
| 196 |
+
B = np.repeat(lm.astype(np.float64), 16, axis=1)
|
| 197 |
+
Saa = np.sum(ww * A * A, axis=1)
|
| 198 |
+
Sab = np.sum(ww * A * B, axis=1)
|
| 199 |
+
Sbb = np.sum(ww * B * B, axis=1)
|
| 200 |
+
Sxa = np.sum(ww * clipped.astype(np.float64) * A, axis=1)
|
| 201 |
+
Sxb = np.sum(ww * clipped.astype(np.float64) * B, axis=1)
|
| 202 |
+
det = Saa * Sbb - Sab * Sab
|
| 203 |
+
valid = det > 1.0e-30
|
| 204 |
+
safe_det = np.where(valid, det, 1.0)
|
| 205 |
+
d_new = (Sbb * Sxa - Sab * Sxb) / safe_det
|
| 206 |
+
m_new = (Sab * Sxa - Saa * Sxb) / safe_det
|
| 207 |
+
upd_d = valid & (d_new > 0.0) & np.isfinite(d_new)
|
| 208 |
+
upd_m = valid & (m_new > 0.0) & np.isfinite(m_new)
|
| 209 |
+
base_d = np.where(upd_d, round_fp16(d_new.astype(np.float32)).astype(np.float64), base_d)
|
| 210 |
+
base_m = np.where(upd_m, round_fp16(m_new.astype(np.float32)).astype(np.float64), base_m)
|
| 211 |
+
safe_d = np.where(base_d > 1.0e-15, base_d, 1.0)
|
| 212 |
+
safe_m = np.where(base_m > 1.0e-15, base_m, 1.0)
|
| 213 |
+
ls = np.where(
|
| 214 |
+
base_d[:, None] > 1.0e-15,
|
| 215 |
+
np.clip(nearest_int(scales / safe_d[:, None]), 0, 15),
|
| 216 |
+
0,
|
| 217 |
+
).astype(np.uint8)
|
| 218 |
+
lm = np.where(
|
| 219 |
+
base_m[:, None] > 1.0e-15,
|
| 220 |
+
np.clip(nearest_int(mins / safe_m[:, None]), 0, 15),
|
| 221 |
+
0,
|
| 222 |
+
).astype(np.uint8)
|
| 223 |
+
return base_d, base_m
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def q3k_proxy(xg: np.ndarray, wg: np.ndarray | None) -> np.ndarray:
|
| 227 |
+
"""Cheap per-16 sub-block 3-bit ladder proxy for Q3_K label iteration."""
|
| 228 |
+
from .seeds import make_qkx2_batch
|
| 229 |
+
|
| 230 |
+
n, B = xg.shape
|
| 231 |
+
u = np.asarray(xg, dtype=np.float64).reshape(n * (B // 16), 16)
|
| 232 |
+
if wg is None:
|
| 233 |
+
wu = np.ones_like(u)
|
| 234 |
+
else:
|
| 235 |
+
wu = np.asarray(wg, dtype=np.float64).reshape(n * (B // 16), 16)
|
| 236 |
+
scale, xmin, codes = make_qkx2_batch(u, wu, nmax=7)
|
| 237 |
+
deq = scale[:, None] * codes + xmin[:, None]
|
| 238 |
+
return deq.reshape(n, B)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def q4_0_proxy(xg: np.ndarray, wg: np.ndarray | None) -> np.ndarray:
|
| 242 |
+
"""GGML-reference Q4_0 proxy (single scale + min per 32-block)."""
|
| 243 |
+
x = np.asarray(xg, dtype=np.float64)
|
| 244 |
+
xmin = x.min(axis=1, keepdims=True)
|
| 245 |
+
xmax = x.max(axis=1, keepdims=True)
|
| 246 |
+
d = np.maximum((xmax - xmin) / 15.0, 1.0e-30)
|
| 247 |
+
q = np.clip(nearest_int(((x - xmin) / d).astype(np.float32)), 0, 15)
|
| 248 |
+
return xmin + d * q.astype(np.float64)
|