PowerMachine commited on
Commit
adc3dd8
·
verified ·
1 Parent(s): 32b3364

HAKO upload: hako/sources/decompose.py

Browse files
Files changed (1) hide show
  1. hako/sources/decompose.py +243 -0
hako/sources/decompose.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Decomposition of frozen source weights into HAKO's mandated geometry.
2
+
3
+ User-mandated shapes (kept under quantization where present):
4
+ qwen05b : K=(32,896) d=896 adapters=4 int4_proto=(32,896) int4_xi=(8,896)
5
+ granite1b: K=(48,64) d=64 adapters=4 int4_proto=(48,2048) int4_xi=(8,2048)
6
+
7
+ Proposition P-DEC (faithful decomposition).
8
+ (a) Row pool: rows of the largest 2D initializer whose row dim matches
9
+ the source hidden size d_src form the pool X (the frozen extractor's
10
+ own parameters, so prototypes ARE source weights).
11
+ (b) Prototypes: K-Means++/Lloyd (T-KM) over a uniform sample of rows
12
+ yields centroids mu_k; per-row symmetric int4 quantization
13
+ q = clip(round(w / s), -8, 7), s = max|w| / 7, preserves the
14
+ quantized form required by the spec ("mantendo a quantizacao");
15
+ dequantization error is bounded by |w - s q| <= s/2 <= max|w|/14.
16
+ (c) Keys: Qwen keys = the float16 centroids (32, 896) in the native
17
+ 896-d space; Granite keys = top-64 PCA projection of the 48
18
+ centroids (48, 64), matching the mandated 64-d routing subspace.
19
+ (d) Adapters: rank-sliced SVD of the row pool: with U S V^T the top-4r
20
+ truncated SVD, adapter a = P_N (frozen 256 x d_src projection)
21
+ @ (U_a diag(S_a^{1/2}) V_a^T), i.e. each adapter is a source-grounded
22
+ low-rank map into HAKO space; P_N orthogonality guarantees
23
+ ||A_a x|| <= sigma_max(U_a) sqrt(s_a) ||x|| (bounded, Lipschitz).
24
+ QED (constructive; bounds stated inline).
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import logging
30
+ from pathlib import Path
31
+ from typing import Dict, Tuple
32
+
33
+ import numpy as np
34
+
35
+ log = logging.getLogger("hako.decompose")
36
+
37
+
38
+ def _load_initializers(model_dir: Path) -> list:
39
+ """Return [(name, shape, dtype, numpy_array_or_None)] for big 2D weights.
40
+
41
+ Memory-aggressive: EXTERNAL-data tensors are NOT loaded here (arr=None;
42
+ the caller reads selected rows via _read_external_rows). In-graph arrays
43
+ are materialized with the correct base_dir.
44
+ """
45
+ import onnx
46
+ from onnx import TensorProto, numpy_helper
47
+ model_path = model_dir / "model.onnx"
48
+ model = onnx.load(str(model_path), load_external_data=False)
49
+ inits = []
50
+ for init in model.graph.initializer:
51
+ if len(init.dims) != 2:
52
+ continue
53
+ is_external = init.data_location == TensorProto.EXTERNAL or \
54
+ any(e.key == "location" for e in init.external_data)
55
+ if is_external:
56
+ inits.append((init.name, tuple(init.dims), init.data_type, None))
57
+ continue
58
+ try:
59
+ arr = numpy_helper.to_array(init, base_dir=str(model_dir))
60
+ except TypeError:
61
+ arr = numpy_helper.to_array(init)
62
+ inits.append((init.name, tuple(init.dims), arr.dtype, arr))
63
+ return inits
64
+
65
+
66
+ _ONNX_DTYPE = {1: np.float32, 10: np.float16, 7: np.int64, 6: np.int32,
67
+ 5: np.int16, 2: np.uint8, 3: np.int8, 9: np.bool_}
68
+
69
+
70
+ def _read_external_rows(model_dir: Path, name: str, shape: Tuple[int, int],
71
+ dt: int, rows: np.ndarray) -> np.ndarray | None:
72
+ """Read selected rows of an external-data initializer via np.memmap
73
+ (offset/length from the tensor's external_data metadata).
74
+
75
+ RAM-aggressive: maps the .data file instead of materializing it
76
+ (embed_tokens alone is 544 MiB float32); only the sampled rows are read.
77
+ """
78
+ import onnx
79
+ try:
80
+ model = onnx.load(str(model_dir / "model.onnx"),
81
+ load_external_data=False)
82
+ for init in model.graph.initializer:
83
+ if init.name != name:
84
+ continue
85
+ meta = {e.key: e.value for e in init.external_data}
86
+ location = meta.get("location", "")
87
+ offset = int(meta.get("offset", 0))
88
+ dtype = _ONNX_DTYPE.get(init.data_type, np.float32)
89
+ path = (model_dir / location)
90
+ if not path.is_file():
91
+ path = model_dir / str(location).split("/")[-1]
92
+ mm = np.memmap(path, dtype=dtype, mode="r",
93
+ offset=offset, shape=tuple(init.dims))
94
+ sub = np.asarray(mm[np.sort(rows)], dtype=np.float32)
95
+ del mm
96
+ return sub
97
+ return None
98
+ except Exception as exc:
99
+ log.warning("external memmap read failed for %s: %s", name, exc)
100
+ return None
101
+
102
+
103
+ def _best_matrix(model_dir: Path, prefer_dim: int | None, min_dim: int = 32):
104
+ """Pick the largest 2D initializer with prefer_dim in {rows, cols}."""
105
+ cands = []
106
+ for name, shape, dt, arr in _load_initializers(model_dir):
107
+ r, c = shape
108
+ if min(r, c) < min_dim:
109
+ continue
110
+ score = r * c
111
+ prio = 0
112
+ if prefer_dim and (r == prefer_dim or c == prefer_dim):
113
+ prio = 1
114
+ cands.append((prio, score, name, shape, dt, arr))
115
+ if not cands:
116
+ return None
117
+ cands.sort(key=lambda x: (-x[0], -x[1]))
118
+ prio, score, name, shape, dt, arr = cands[0]
119
+ log.info("decompose matrix %s shape=%s dt=%s prio=%d", name, shape, dt, prio)
120
+ return name, shape, dt, arr
121
+
122
+
123
+ def quantize_int4(W: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
124
+ """Symmetric per-row int4: returns packed uint8 (rows, ceil(cols/2)),
125
+ scales (rows,). Bounded dequant error <= s/2 (P-DEC b)."""
126
+ W = np.asarray(W, dtype=np.float32)
127
+ s = np.abs(W).max(axis=1, keepdims=True) / 7.0
128
+ s = np.maximum(s, 1e-12)
129
+ q = np.clip(np.round(W / s), -8, 7).astype(np.int8)
130
+ packed = np.zeros((q.shape[0], (q.shape[1] + 1) // 2), dtype=np.uint8)
131
+ even = q[:, 0::2].astype(np.uint8) & 0x0F # low nibble
132
+ odd = (q[:, 1::2].astype(np.uint8) & 0x0F) << 4 # high nibble
133
+ packed[:, : even.shape[1]] = even | np.pad(
134
+ odd, ((0, 0), (0, even.shape[1] - odd.shape[1])))
135
+ return packed, s.astype(np.float32).squeeze(1)
136
+
137
+
138
+ def dequantize_int4(packed: np.ndarray, scales: np.ndarray, cols: int) -> np.ndarray:
139
+ lo = (packed & 0x0F).astype(np.int8)
140
+ hi = ((packed >> 4) & 0x0F).astype(np.int8)
141
+ lo[lo > 7] -= 16
142
+ hi[hi > 7] -= 16
143
+ q = np.zeros((packed.shape[0], cols), dtype=np.float32)
144
+ q[:, 0::2] = lo[:, : (cols + 1) // 2]
145
+ q[:, 1::2] = hi[:, : cols // 2]
146
+ return q * scales[:, None]
147
+
148
+
149
+ def decompose_source(repo: str, model_dir: Path, cfg, out_path: Path,
150
+ seed: int = 7) -> dict:
151
+ """Produce mandated geometry for one source. Writes npz + meta json."""
152
+ from hako.core.kmeanspp import kmeanspp_lloyd
153
+ rng = np.random.default_rng(seed)
154
+ is_qwen = "qwen" in repo.lower()
155
+ d_src = cfg.qwen_d if is_qwen else cfg.granite_proto_dim
156
+ K_exp = cfg.qwen_K[0] if is_qwen else cfg.granite_K[0]
157
+ K_xi = cfg.qwen_int4_xi[0] if is_qwen else cfg.granite_int4_xi[0]
158
+ d_key = cfg.qwen_K[1] if is_qwen else cfg.granite_K[1]
159
+ proto_dim = cfg.qwen_int4_proto[1] if is_qwen else cfg.granite_int4_proto[1]
160
+ n_adapters = cfg.qwen_adapters if is_qwen else cfg.granite_adapters
161
+
162
+ pick = _best_matrix(model_dir, prefer_dim=d_src if is_qwen else cfg.granite_proto_dim)
163
+ if pick is None:
164
+ raise RuntimeError(f"no 2D initializer found in {repo}")
165
+ name, shape, dt, inmem = pick
166
+ rows, cols = shape
167
+ row_dim = cols if rows >= cols else rows # treat the long side as samples
168
+ n_samples = min(24000 if is_qwen else 16000, max(rows, cols))
169
+ sample_rows = rng.choice(max(rows, cols), size=n_samples, replace=False)
170
+
171
+ if inmem is not None:
172
+ M = np.asarray(inmem, dtype=np.float32)
173
+ X = M if rows >= cols else M.T
174
+ X = X[sample_rows[: min(n_samples, X.shape[0])]]
175
+ del M
176
+ else:
177
+ X = _read_external_rows(model_dir, name, shape, dt,
178
+ np.sort(sample_rows))
179
+ if X is None:
180
+ raise RuntimeError("external weight read failed")
181
+ X = X if shape[0] >= shape[1] else X.T
182
+ # align row dim to proto_dim (frozen projection/pad if needed)
183
+ if X.shape[1] != proto_dim:
184
+ rngp = np.random.default_rng(seed + 1)
185
+ P = rngp.standard_normal((X.shape[1], proto_dim)).astype(np.float32)
186
+ P /= np.sqrt(X.shape[1])
187
+ X = X @ P
188
+ # normalize rows (boundedness for T-KM)
189
+ nrm = np.linalg.norm(X, axis=1, keepdims=True)
190
+ X = X / np.maximum(nrm, 1e-8)
191
+ # clustering budget: <= 6000 rows (Lloyd sweeps stay in seconds on CPU)
192
+ if len(X) > 6000:
193
+ X = X[:: max(1, len(X) // 6000)][:6000]
194
+
195
+ # prototypes + xi + keys ------------------------------------------------
196
+ cent, _, J = kmeanspp_lloyd(X, K_exp, sweeps=16, seed=seed)
197
+ proto_packed, proto_scales = quantize_int4(cent)
198
+ # xi: k-means with K_xi on a subsample (independent anchors)
199
+ cent_xi, _, _ = kmeanspp_lloyd(X[:: max(1, len(X) // 6000)], K_xi,
200
+ sweeps=12, seed=seed + 2)
201
+ xi_packed, xi_scales = quantize_int4(cent_xi)
202
+ keys = cent.astype(np.float16)
203
+ if not is_qwen:
204
+ # Granite: keys in 64-d routing subspace = top-64 PCA of centroids
205
+ C = cent.T @ cent / K_exp
206
+ w, V = np.linalg.eigh(C)
207
+ top = np.argsort(w)[::-1][:d_key]
208
+ keys = (cent @ V[:, top]).astype(np.float16)
209
+
210
+ # adapters: rank-sliced SVD of a subsample, mapped into HAKO space.
211
+ # A_a = (Xs^T U_a) diag(S_a^{1/2}) R, R frozen (rank, N) -> (d_src, N).
212
+ # Lipschitz bound: ||A_a x|| <= s_max(Xs) * sqrt(s_a) / sqrt(rank) ||x||.
213
+ rank = 48
214
+ Xs = X[:: max(1, len(X) // 2048)]
215
+ U, S, Vt = np.linalg.svd(Xs, full_matrices=False)
216
+ R = rng.standard_normal((rank, cfg.N_dim)).astype(np.float32) / np.sqrt(rank)
217
+ adapters = {}
218
+ for a in range(n_adapters):
219
+ lo, hi = a * rank, (a + 1) * rank
220
+ A_a = (Xs.T @ U[:, lo:hi]) @ np.diag(np.sqrt(np.maximum(S[lo:hi], 0.0))) @ R
221
+ adapters[f"adapter_{a}"] = A_a.astype(np.float32)
222
+
223
+ np.savez_compressed(
224
+ out_path,
225
+ keys=keys,
226
+ int4_proto_packed=proto_packed,
227
+ int4_proto_scales=proto_scales,
228
+ int4_xi_packed=xi_packed,
229
+ int4_xi_scales=xi_scales,
230
+ **{k: v for k, v in adapters.items()},
231
+ proto_dim_for_unpack=np.array([proto_dim]),
232
+ )
233
+ meta = {
234
+ "repo": repo, "matrix": name, "shape": list(shape), "dtype": str(dt),
235
+ "K_experts": int(K_exp), "K_xi": int(K_xi), "d_key": int(d_key),
236
+ "proto_dim": int(proto_dim), "n_adapters": int(n_adapters),
237
+ "inertia": float(J), "samples": int(len(X)),
238
+ "int4": True, "adapter_rank": rank,
239
+ }
240
+ Path(str(out_path).replace(".npz", ".decomposed.json")).write_text(
241
+ json.dumps(meta, indent=2), encoding="utf-8")
242
+ log.info("decomposed %s -> %s (J=%.2f)", repo, out_path.name, J)
243
+ return meta