rwkv-quant 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rwkv_quant/__init__.py +4 -0
- rwkv_quant/api.py +111 -0
- rwkv_quant/backends/__init__.py +0 -0
- rwkv_quant/backends/cuda/__init__.py +0 -0
- rwkv_quant/backends/metal/__init__.py +0 -0
- rwkv_quant/backends/metal/quant_linear.py +106 -0
- rwkv_quant/backends/metal/quant_linear_gw.py +1020 -0
- rwkv_quant/backends/metal/quant_linear_v2.py +351 -0
- rwkv_quant/backends/metal/quant_model.py +637 -0
- rwkv_quant/calibration/__init__.py +4 -0
- rwkv_quant/calibration/ablation.py +143 -0
- rwkv_quant/calibration/fake_quant.py +65 -0
- rwkv_quant/calibration/group_config.py +55 -0
- rwkv_quant/calibration/outlier_scan.py +68 -0
- rwkv_quant/formats/__init__.py +6 -0
- rwkv_quant/formats/export_mlx.py +55 -0
- rwkv_quant/formats/reader.py +69 -0
- rwkv_quant/formats/schema.py +163 -0
- rwkv_quant/formats/writer.py +426 -0
- rwkv_quant/models/__init__.py +4 -0
- rwkv_quant/models/naming.py +19 -0
- rwkv_quant/models/rwkv7_ref.py +273 -0
- rwkv_quant/presets.py +73 -0
- rwkv_quant-0.1.0.dist-info/METADATA +303 -0
- rwkv_quant-0.1.0.dist-info/RECORD +28 -0
- rwkv_quant-0.1.0.dist-info/WHEEL +5 -0
- rwkv_quant-0.1.0.dist-info/licenses/LICENSE +201 -0
- rwkv_quant-0.1.0.dist-info/top_level.txt +1 -0
rwkv_quant/__init__.py
ADDED
rwkv_quant/api.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Высокоуровневый API. Два входа:
|
|
3
|
+
- quantize(ckpt, out, preset=...) -- быстрый старт, готовые пресеты
|
|
4
|
+
- quantize(ckpt, out, config=...) -- полный контроль через QuantConfig
|
|
5
|
+
- calibrate(ckpt, corpus) -- автоматически подобрать QuantConfig
|
|
6
|
+
под конкретный чекпоинт вместо
|
|
7
|
+
пресета "с потолка" (см. README:
|
|
8
|
+
чувствительность к квантованию НЕ
|
|
9
|
+
переносится между масштабами модели)
|
|
10
|
+
"""
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
from .presets import PRESETS
|
|
14
|
+
from .calibration import GROUPS, QuantConfig
|
|
15
|
+
from .calibration.ablation import single_group_ablation, perplexity, combined_sanity_check
|
|
16
|
+
from .models.rwkv7_ref import RWKV7Ref
|
|
17
|
+
from .formats import save
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def quantize(checkpoint_path: str, output_path: str, preset: str = "reduction",
|
|
21
|
+
config: QuantConfig = None):
|
|
22
|
+
"""
|
|
23
|
+
Quick-start: quantize(ckpt, out, preset="compression")
|
|
24
|
+
Advanced: quantize(ckpt, out, config=QuantConfig(proj=4, ...))
|
|
25
|
+
|
|
26
|
+
preset игнорируется, если передан config.
|
|
27
|
+
"""
|
|
28
|
+
if config is None:
|
|
29
|
+
if preset not in PRESETS:
|
|
30
|
+
raise ValueError(f"unknown preset {preset!r}, choose from {list(PRESETS)}")
|
|
31
|
+
config = PRESETS[preset]
|
|
32
|
+
|
|
33
|
+
# метаданные (naming/n_layer/...) дешевле всего взять из уже написанного
|
|
34
|
+
# загрузчика RWKV7Ref, вместо повторной ручной детекции формата
|
|
35
|
+
model = RWKV7Ref(checkpoint_path, device="cpu", dtype=torch.bfloat16)
|
|
36
|
+
naming, n_layer, n_embd = model.naming, model.n_layer, model.n_embd
|
|
37
|
+
head_size, vocab_size = model.head_size, model.vocab_size
|
|
38
|
+
del model
|
|
39
|
+
|
|
40
|
+
if checkpoint_path.endswith(".pth"):
|
|
41
|
+
sd = torch.load(checkpoint_path, map_location="cpu")
|
|
42
|
+
else:
|
|
43
|
+
from safetensors.torch import load_file
|
|
44
|
+
sd = load_file(f"{checkpoint_path}/model.safetensors")
|
|
45
|
+
|
|
46
|
+
return save(sd, config, output_path, naming, n_layer, n_embd, head_size, vocab_size)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def calibrate(checkpoint_path: str, eval_corpus_path: str, device: str = "mps",
|
|
50
|
+
ppl_threshold_pct: float = 5.0, spqr_outlier_frac: float = 0.02,
|
|
51
|
+
small_clip_percentile: float = 99.9, verbose: bool = True) -> QuantConfig:
|
|
52
|
+
"""
|
|
53
|
+
Прогоняет single-group ablation на конкретном чекпоинте и подбирает
|
|
54
|
+
QuantConfig вместо использования пресета "с потолка".
|
|
55
|
+
|
|
56
|
+
Эвристика (по находкам из README, не универсальный поиск):
|
|
57
|
+
1. Для каждой группы -- минимальная битность из {8,6,4,2} с Δppl% <=
|
|
58
|
+
ppl_threshold_pct без какой-либо доп. обработки.
|
|
59
|
+
2. Если группа из {proj, cmix, emb_head} не прошла порог даже на INT8 --
|
|
60
|
+
пробуем INT4 + SpQR sparse outlier (percentile-clipping для них НЕ
|
|
61
|
+
пробуем: он делает эти группы хуже, см. README).
|
|
62
|
+
3. Если группа "small" не прошла порог -- пробуем INT6 + percentile
|
|
63
|
+
clip (SpQR для неё не нужен, clip уже полностью решает проблему).
|
|
64
|
+
"""
|
|
65
|
+
model = RWKV7Ref(checkpoint_path, device=device, dtype=torch.bfloat16)
|
|
66
|
+
data = torch.load(eval_corpus_path).to(device)
|
|
67
|
+
|
|
68
|
+
baseline_ppl, results = single_group_ablation(
|
|
69
|
+
model, data, groups=GROUPS, bits_grid=(8, 6, 4, 2), verbose=verbose)
|
|
70
|
+
|
|
71
|
+
best_bits = {}
|
|
72
|
+
for group in GROUPS:
|
|
73
|
+
group_results = [r for r in results if r[0] == group and r[3] <= ppl_threshold_pct]
|
|
74
|
+
best_bits[group] = min((r[1] for r in group_results), default=16)
|
|
75
|
+
|
|
76
|
+
outlier_fracs, clip_percentiles = {}, {}
|
|
77
|
+
|
|
78
|
+
for group in ("proj", "cmix", "emb_head"):
|
|
79
|
+
if best_bits[group] > 4:
|
|
80
|
+
cfg_try = QuantConfig(**{group: 4}, outlier_fracs={group: spqr_outlier_frac})
|
|
81
|
+
ppl_try = perplexity(model, data, cfg_try)
|
|
82
|
+
delta = 100 * (ppl_try - baseline_ppl) / baseline_ppl
|
|
83
|
+
if verbose:
|
|
84
|
+
print(f" retry {group} @ INT4+SpQR({spqr_outlier_frac*100:.0f}%): Δ={delta:+.2f}%")
|
|
85
|
+
if delta <= ppl_threshold_pct * 3: # мягче: это уже "сжатие ценой качества"
|
|
86
|
+
best_bits[group] = 4
|
|
87
|
+
outlier_fracs[group] = spqr_outlier_frac
|
|
88
|
+
|
|
89
|
+
if best_bits["small"] > 6:
|
|
90
|
+
cfg_try = QuantConfig(small=6, clip_percentiles={"small": small_clip_percentile})
|
|
91
|
+
ppl_try = perplexity(model, data, cfg_try)
|
|
92
|
+
delta = 100 * (ppl_try - baseline_ppl) / baseline_ppl
|
|
93
|
+
if verbose:
|
|
94
|
+
print(f" retry small @ INT6+clip(p{small_clip_percentile}): Δ={delta:+.2f}%")
|
|
95
|
+
if delta <= ppl_threshold_pct:
|
|
96
|
+
best_bits["small"] = 6
|
|
97
|
+
clip_percentiles["small"] = small_clip_percentile
|
|
98
|
+
|
|
99
|
+
# single-group ablation выше оценивает каждую группу НЕЗАВИСИМО и не
|
|
100
|
+
# может поймать эффекты взаимодействия при одновременном квантовании
|
|
101
|
+
# (см. presets.py: все 4 LoRA-ветки на INT4 порознь безобидны, вместе --
|
|
102
|
+
# ~150x взрыв ppl). Обязательная проверка целиком перед выдачей конфига.
|
|
103
|
+
best_bits, outlier_fracs, final_ppl, final_delta = combined_sanity_check(
|
|
104
|
+
model, data, best_bits, outlier_fracs, clip_percentiles,
|
|
105
|
+
baseline_ppl=baseline_ppl, ppl_threshold_pct=ppl_threshold_pct, verbose=verbose)
|
|
106
|
+
|
|
107
|
+
config = QuantConfig(clip_percentiles=clip_percentiles, outlier_fracs=outlier_fracs, **best_bits)
|
|
108
|
+
if verbose:
|
|
109
|
+
print(f"\nCalibrated config: {config}\noutlier_fracs={outlier_fracs} clip={clip_percentiles}")
|
|
110
|
+
print(f"Финальный Δppl(целиком) = {final_delta:+.2f}%")
|
|
111
|
+
return config
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
backends/metal/quant_linear.py — INT8 per-row scale + SpQR sparse-outlier
|
|
3
|
+
Linear на Metal (MLX custom kernel), без промежуточного bf16-разворачивания.
|
|
4
|
+
|
|
5
|
+
Заменяет formats.reader.load_dequantized() (весь тензор int8 -> bf16 в
|
|
6
|
+
памяти) на прямой матмул по codes/scale + точечную поправку по
|
|
7
|
+
разреженным outlier'ам. codes уже нулевые в outlier-позициях (см.
|
|
8
|
+
writer._real_quantize_sparse_outlier), поэтому outlier-коррекция — это
|
|
9
|
+
чистое дополнение, а не overwrite.
|
|
10
|
+
"""
|
|
11
|
+
import numpy as np
|
|
12
|
+
import mlx.core as mx
|
|
13
|
+
|
|
14
|
+
from ...formats.schema import int8_codes
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_outlier_csr(outlier_indices, outlier_values, n_rows):
|
|
18
|
+
"""torch [n_outliers,2] (row,col) + [n_outliers] значения ->
|
|
19
|
+
row_offsets[n_rows+1], cols[n_outliers], vals[n_outliers] — отсортировано
|
|
20
|
+
по row, для CSR-обхода внутри kernel'я. Выполняется один раз при загрузке."""
|
|
21
|
+
if outlier_indices is None or outlier_indices.numel() == 0:
|
|
22
|
+
return (mx.array(np.zeros(n_rows + 1, dtype=np.int32)),
|
|
23
|
+
mx.array(np.zeros(1, dtype=np.int32)),
|
|
24
|
+
mx.array(np.zeros(1, dtype=np.float32)))
|
|
25
|
+
|
|
26
|
+
rows = outlier_indices[:, 0].numpy()
|
|
27
|
+
cols = outlier_indices[:, 1].numpy()
|
|
28
|
+
vals = outlier_values.float().numpy()
|
|
29
|
+
|
|
30
|
+
order = np.argsort(rows, kind="stable")
|
|
31
|
+
rows, cols, vals = rows[order], cols[order], vals[order]
|
|
32
|
+
|
|
33
|
+
row_offsets = np.zeros(n_rows + 1, dtype=np.int32)
|
|
34
|
+
row_offsets[1:] = np.cumsum(np.bincount(rows, minlength=n_rows))
|
|
35
|
+
|
|
36
|
+
return (mx.array(row_offsets), mx.array(cols.astype(np.int32)), mx.array(vals.astype(np.float32)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_kernel_cache = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_kernel(IN: int, OUT: int, has_outliers: bool):
|
|
43
|
+
key = (IN, OUT, has_outliers)
|
|
44
|
+
if key in _kernel_cache:
|
|
45
|
+
return _kernel_cache[key]
|
|
46
|
+
|
|
47
|
+
hdr = f"""
|
|
48
|
+
constant uint IN_C = {IN};
|
|
49
|
+
constant uint OUT_C = {OUT};
|
|
50
|
+
"""
|
|
51
|
+
outlier_body = """
|
|
52
|
+
uint start = row_offsets[oi];
|
|
53
|
+
uint end = row_offsets[oi+1];
|
|
54
|
+
for (uint idx = start; idx < end; idx++) {
|
|
55
|
+
uint c = (uint)outlier_cols[idx];
|
|
56
|
+
acc += x[n*IN_C + c] * outlier_vals[idx];
|
|
57
|
+
}
|
|
58
|
+
""" if has_outliers else ""
|
|
59
|
+
|
|
60
|
+
body = """
|
|
61
|
+
uint oi = thread_position_in_grid.x; // output feature
|
|
62
|
+
uint n = thread_position_in_grid.y; // batch row
|
|
63
|
+
|
|
64
|
+
float acc = 0.0f;
|
|
65
|
+
for (uint c = 0; c < IN_C; c++)
|
|
66
|
+
acc += x[n*IN_C + c] * (float)codes[oi*IN_C + c];
|
|
67
|
+
acc *= (float)scale[oi];
|
|
68
|
+
""" + outlier_body + """
|
|
69
|
+
out[n*OUT_C + oi] = acc;
|
|
70
|
+
"""
|
|
71
|
+
kern = mx.fast.metal_kernel(
|
|
72
|
+
name=f"quant_linear_{'spqr' if has_outliers else 'plain'}_{IN}_{OUT}",
|
|
73
|
+
input_names=["x", "codes", "scale", "row_offsets", "outlier_cols", "outlier_vals"],
|
|
74
|
+
output_names=["out"],
|
|
75
|
+
header=hdr, source=body,
|
|
76
|
+
)
|
|
77
|
+
_kernel_cache[key] = kern
|
|
78
|
+
return kern
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class QuantLinear:
|
|
82
|
+
"""Один Linear на int8 codes + per-row scale (+ опц. SpQR), веса живут
|
|
83
|
+
на GPU в упакованном виде — никогда не разворачиваются в полный bf16."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, qt):
|
|
86
|
+
# qt: rwkv_quant.formats.schema.QuantizedTensor, bits < 16
|
|
87
|
+
assert qt.bits < 16, "QuantLinear только для квантованных тензоров"
|
|
88
|
+
self.out_features, self.in_features = qt.shape
|
|
89
|
+
self.codes = mx.array(int8_codes(qt).numpy()) # int8 [out, in] (packed распаковывается: v1 -- референс)
|
|
90
|
+
self.scale = mx.array(qt.scale.float().numpy().reshape(-1)) # fp32 [out]
|
|
91
|
+
self.row_offsets, self.outlier_cols, self.outlier_vals = _build_outlier_csr(
|
|
92
|
+
qt.outlier_indices, qt.outlier_values, self.out_features)
|
|
93
|
+
self.has_outliers = qt.outlier_indices is not None and qt.outlier_indices.numel() > 0
|
|
94
|
+
|
|
95
|
+
def __call__(self, x):
|
|
96
|
+
lead_shape = x.shape[:-1]
|
|
97
|
+
x2d = x.reshape(-1, self.in_features).astype(mx.float32)
|
|
98
|
+
N = x2d.shape[0]
|
|
99
|
+
kern = _get_kernel(self.in_features, self.out_features, self.has_outliers)
|
|
100
|
+
out = kern(
|
|
101
|
+
inputs=[x2d, self.codes, self.scale, self.row_offsets, self.outlier_cols, self.outlier_vals],
|
|
102
|
+
grid=(self.out_features, N, 1), threadgroup=(1, 1, 1),
|
|
103
|
+
output_shapes=[(N, self.out_features)],
|
|
104
|
+
output_dtypes=[mx.float32],
|
|
105
|
+
)[0]
|
|
106
|
+
return out.reshape(*lead_shape, self.out_features)
|