tchbcb commited on
Commit
da74071
·
verified ·
1 Parent(s): a3f5513

add convert_minicpm_mixed.py

Browse files
Files changed (1) hide show
  1. convert_minicpm_mixed.py +247 -0
convert_minicpm_mixed.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """convert_minicpm_mixed.py - Mixed-precision converter: MiniCPM5-2B bf16 safetensors -> LAL GPQ8.
2
+
3
+ Mixed-precision scheme (user requirement: 4-bit main body, part 8-bit, part 3-value):
4
+ - embed_tokens / lm_head -> Q8 per-row (qtype=1) [8-bit, quality-critical]
5
+ - layernorms / model.norm -> F32 (qtype=0)
6
+ - layers 0 and 41 (edges) -> Q8_0 (qtype=3) [8-bit, sensitive first/last layers]
7
+ - layers 1..40 q/k/v/o/down -> Q4_K (qtype=5) [4-bit main body]
8
+ - layers 1..7,32..40 gate/up -> Q4_K (qtype=5)
9
+ - layers 8..31 gate/up -> TERNARY 3-value (qtype=6) {-s,0,+s}, 2-bit codes
10
+
11
+ Formats:
12
+ Q4_K : 144B / 256 elems (fp16 d + fp16 dmin + 12B 6-bit packed scales/mins + 128B qs)
13
+ Q8_0 : 34B / 32 elems (fp16 scale + 32 int8)
14
+ Q8_row : int8 + per-row fp32 scale (stored in trailing scale array)
15
+ TERNARY : 18B / 64 elems (fp16 scale + 16B codes: 8B bit0 stream + 8B bit1 stream)
16
+ code t in {-1,0,+1}: bit0=(t==+1), bit1=(t==-1); kernel computes (S0-S1)
17
+ """
18
+ import struct, json, os, sys, time
19
+ import numpy as np
20
+
21
+ SRC = 'C:/models/model-00000-of-00001.safetensors'
22
+ OUT = 'C:/models/minicpm_mixed.bin'
23
+ LOG = 'C:/models/convert_status.log'
24
+ TERN_COS_MIN = 0.8970 # per-matrix cosine gate: below this, fall back to Q4_K
25
+
26
+ TERN_RANGE = (8, 31) # inclusive layer range for 3-value gate/up candidates
27
+ EDGE_LAYERS = (0, 41) # layers fully Q8_0 (8-bit)
28
+ n_tern_stats = []
29
+ # top-8 candidates by measured cosine (highest fidelity 3-value matrices):
30
+ TERN_KEEP = {
31
+ 'model.layers.31.mlp.up_proj.weight', # cos=0.8989
32
+ 'model.layers.10.mlp.up_proj.weight', # cos=0.8986
33
+ 'model.layers.11.mlp.up_proj.weight', # cos=0.8984
34
+ 'model.layers.10.mlp.gate_proj.weight', # cos=0.8981
35
+ 'model.layers.19.mlp.up_proj.weight', # cos=0.8980
36
+ 'model.layers.30.mlp.up_proj.weight', # cos=0.8979
37
+ 'model.layers.31.mlp.gate_proj.weight', # cos=0.8979
38
+ 'model.layers.18.mlp.up_proj.weight', # cos=0.8978
39
+ }
40
+
41
+ log = open(LOG, 'w', buffering=1)
42
+ def L(msg):
43
+ log.write(msg + '\n')
44
+
45
+ # ---------- dtype conversions ----------
46
+ def to_f32(raw, dtype):
47
+ if dtype == 'BF16':
48
+ u16 = np.frombuffer(raw, dtype=np.uint16)
49
+ return (u16.astype(np.uint32) << 16).view(np.float32)
50
+ if dtype == 'F32':
51
+ return np.frombuffer(raw, dtype=np.float32).copy()
52
+ if dtype == 'F16':
53
+ return np.frombuffer(raw, dtype=np.float16).astype(np.float32)
54
+ raise ValueError('unsupported dtype ' + dtype)
55
+
56
+ # ---------- quantizers ----------
57
+ def q8_per_row(w2d):
58
+ """Q8 per-row: int8 + per-row fp32 scale -> (bytes, scales)"""
59
+ w = w2d.astype(np.float32)
60
+ s = np.abs(w).max(axis=1) / 127.0
61
+ s = np.where(s < 1e-12, 1e-12, s).astype(np.float32)
62
+ q = np.clip(np.round(w / s[:, None]), -127, 127).astype(np.int8)
63
+ return q.tobytes(), s.tobytes()
64
+
65
+ def q8_0(w2d):
66
+ """Q8_0 blocks: per-32 fp16 scale + 32 int8 -> 34B/32elem"""
67
+ w = w2d.astype(np.float32)
68
+ out_dim, in_dim = w.shape
69
+ assert in_dim % 32 == 0
70
+ nb = in_dim // 32
71
+ wb = w.reshape(out_dim, nb, 32)
72
+ amax = np.abs(wb).max(axis=2) / 127.0
73
+ amax = np.where(amax < 1e-12, 1e-12, amax).astype(np.float32)
74
+ q = np.clip(np.round(wb / amax[:, :, None]), -127, 127).astype(np.int8)
75
+ blocks = np.zeros((out_dim, nb, 34), dtype=np.uint8)
76
+ h16 = amax.astype(np.float16).view(np.uint8).reshape(out_dim, nb, 2)
77
+ blocks[:, :, 0:2] = h16
78
+ blocks[:, :, 2:34] = q.view(np.uint8).reshape(out_dim, nb, 32)
79
+ return blocks.tobytes()
80
+
81
+ def q4_k(w2d):
82
+ """Q4_K superblocks: 144B/256elem (llama.cpp-compatible, matches lal kernel)"""
83
+ w = w2d.astype(np.float32)
84
+ out_dim, in_dim = w.shape
85
+ assert in_dim % 256 == 0
86
+ n_super = in_dim // 256
87
+ wb = w.reshape(out_dim, n_super, 8, 32)
88
+ sub_min = wb.min(axis=3)
89
+ sub_max = wb.max(axis=3)
90
+ sub_scale = (sub_max - sub_min) / 15.0
91
+ sub_scale = np.where(sub_scale < 1e-8, 1e-8, sub_scale)
92
+ max_scale = np.where(sub_scale.max(axis=2) < 1e-8, 1e-8, sub_scale.max(axis=2)).astype(np.float32)
93
+ max_min = np.where(np.abs(sub_min).max(axis=2) < 1e-8, 1e-8, np.abs(sub_min).max(axis=2)).astype(np.float32)
94
+ d, dmin = max_scale, max_min
95
+ sc6 = np.clip(np.round(sub_scale / d[:, :, None] * 63), 0, 63).astype(np.uint8)
96
+ m6 = np.clip(np.round(np.abs(sub_min) / dmin[:, :, None] * 63), 0, 63).astype(np.uint8)
97
+ act_s = d[:, :, None] * sc6.astype(np.float32) / 63.0
98
+ act_m = dmin[:, :, None] * m6.astype(np.float32) / 63.0
99
+ q = np.clip(np.round((wb + act_m[:, :, :, None]) / (act_s[:, :, :, None] + 1e-8)), 0, 15).astype(np.uint8)
100
+ qf = q.reshape(out_dim, n_super, 256)
101
+ qs = np.zeros((out_dim, n_super, 128), dtype=np.uint8)
102
+ for sub in range(8):
103
+ for i in range(16):
104
+ qs[:, :, sub * 16 + i] = qf[:, :, sub * 32 + 2 * i] | (qf[:, :, sub * 32 + 2 * i + 1] << 4)
105
+ # 16 x 6-bit values (8 scales + 8 mins) -> continuous 96-bit little-endian
106
+ # bitstream -> 12 bytes (llama.cpp Q4_K packing).
107
+ flat = np.zeros((out_dim * n_super, 16), dtype=np.uint64)
108
+ flat[:, 0:8] = sc6.reshape(-1, 8)
109
+ flat[:, 8:16] = m6.reshape(-1, 8)
110
+ outb = np.zeros((flat.shape[0], 12), dtype=np.uint8)
111
+ for j in range(16):
112
+ v = flat[:, j]
113
+ bp = j * 6
114
+ for b in range(6):
115
+ bit = bp + b
116
+ byte_i, bit_i = bit // 8, bit % 8
117
+ outb[:, byte_i] |= (((v >> np.uint64(b)) & np.uint64(1)) << np.uint64(bit_i)).astype(np.uint8)
118
+ blocks = np.zeros((out_dim, n_super, 144), dtype=np.uint8)
119
+ blocks[:, :, 0:2] = d.astype(np.float16).view(np.uint8).reshape(out_dim, n_super, 2)
120
+ blocks[:, :, 2:4] = dmin.astype(np.float16).view(np.uint8).reshape(out_dim, n_super, 2)
121
+ blocks[:, :, 4:16] = outb.reshape(out_dim, n_super, 12)
122
+ blocks[:, :, 16:144] = qs
123
+ return blocks.tobytes()
124
+
125
+ def ternary(w2d):
126
+ """3-value blocks: 18B/64elem = fp16 scale + 8B bit0 + 8B bit1.
127
+ Structure: TWN threshold delta = 0.7*mean|w| picks the +/-1 support.
128
+ Scale: per-block least-squares s* = sum(w*t)/sum(t*t), the minimizer of
129
+ ||w - s*t||^2 -- restores the energy that plain absmean destroys (PTQ)."""
130
+ w = w2d.astype(np.float32)
131
+ out_dim, in_dim = w.shape
132
+ assert in_dim % 64 == 0
133
+ nb = in_dim // 64
134
+ wb = w.reshape(out_dim, nb, 64)
135
+ aabs = np.abs(wb)
136
+ delta = 0.7 * aabs.mean(axis=2, keepdims=True)
137
+ t = np.where(wb > delta, 1, np.where(wb < -delta, -1, 0)).astype(np.float32)
138
+ denom = (t * t).sum(axis=2, keepdims=True)
139
+ s = np.where(denom > 0, (wb * t).sum(axis=2, keepdims=True) / np.maximum(denom, 1.0), 0.0)
140
+ s = s.astype(np.float32)
141
+ ti = t.astype(np.int8)
142
+ bit0 = (ti == 1).astype(np.uint8).reshape(out_dim, nb, 64)
143
+ bit1 = (ti == -1).astype(np.uint8).reshape(out_dim, nb, 64)
144
+ blocks = np.zeros((out_dim, nb, 18), dtype=np.uint8)
145
+ blocks[:, :, 0:2] = s.astype(np.float16).view(np.uint8).reshape(out_dim, nb, 2)
146
+ blocks[:, :, 2:10] = np.packbits(bit0, axis=2, bitorder='little').reshape(out_dim, nb, 8)
147
+ blocks[:, :, 10:18] = np.packbits(bit1, axis=2, bitorder='little').reshape(out_dim, nb, 8)
148
+ return blocks.tobytes()
149
+
150
+ # ---------- main ----------
151
+ def qtype_for(key, shape):
152
+ if key == 'model.embed_tokens.weight' or key == 'lm_head.weight':
153
+ return 1
154
+ if key == 'model.norm.weight' or 'layernorm' in key:
155
+ return 0
156
+ import re
157
+ m = re.search(r'layers\.(\d+)\.', key)
158
+ li = int(m.group(1))
159
+ if li in EDGE_LAYERS:
160
+ return 3
161
+ if ('gate_proj' in key or 'up_proj' in key) and TERN_RANGE[0] <= li <= TERN_RANGE[1] and key in TERN_KEEP:
162
+ return 66 # candidate: decide after cosine test
163
+ return 5
164
+
165
+ def tern_cos(w2d):
166
+ """weight-space cosine between bf16 W and its 3-value reconstruction"""
167
+ out_dim, in_dim = w2d.shape
168
+ blocks = ternary(w2d)
169
+ nb = in_dim // 64
170
+ b = np.frombuffer(blocks, dtype=np.uint8).reshape(out_dim, nb, 18)
171
+ s = b[:, :, 0:2].copy().view(np.float16).astype(np.float32).reshape(out_dim, nb, 1)
172
+ def expand(ab):
173
+ bits = np.zeros((ab.shape[0], ab.shape[1], 64), dtype=np.int8)
174
+ for bi in range(8):
175
+ for bit in range(8):
176
+ bits[:, :, bi * 8 + bit] = ((ab[:, :, bi] >> bit) & 1).astype(np.int8)
177
+ return bits
178
+ T = (expand(b[:, :, 2:10]) - expand(b[:, :, 10:18])).astype(np.float32)
179
+ Wh = (T * s).reshape(out_dim, in_dim)
180
+ W = w2d.reshape(out_dim, in_dim)
181
+ return float((W * Wh).sum() / (np.sqrt((W * W).sum()) * np.sqrt((Wh * Wh).sum()) + 1e-12))
182
+
183
+ # ---------- main ----------
184
+ def main():
185
+ t0 = time.time()
186
+ with open(SRC, 'rb') as f:
187
+ hlen = struct.unpack('<Q', f.read(8))[0]
188
+ hdr = json.loads(f.read(hlen).decode('utf-8'))
189
+ data_start = 8 + hlen
190
+ meta = [(k, v) for k, v in hdr.items() if k != '__metadata__']
191
+ L('TENSORS %d' % len(meta))
192
+ stats = {}
193
+ out = open(OUT, 'wb')
194
+ out.write(b'GPQ8')
195
+ out.write(struct.pack('<I', len(meta)))
196
+ src = open(SRC, 'rb')
197
+ for idx, (key, m) in enumerate(meta):
198
+ shape, dtype, off = m['shape'], m['dtype'], m['data_offsets']
199
+ src.seek(data_start + off[0])
200
+ raw = src.read(off[1] - off[0])
201
+ f32 = to_f32(raw, dtype)
202
+ qt = qtype_for(key, shape)
203
+ scales_payload = b''
204
+ if len(shape) == 2:
205
+ w2d = f32.reshape(shape[0], shape[1])
206
+ else:
207
+ w2d = f32.reshape(shape)
208
+ if qt == 66:
209
+ c = tern_cos(w2d)
210
+ qt = 6 if c >= TERN_COS_MIN else 5
211
+ n_tern_stats.append((key, c))
212
+ if qt == 0:
213
+ data, n_scale = f32.astype(np.float32).tobytes(), 0
214
+ elif qt == 1:
215
+ data, sc = q8_per_row(w2d)
216
+ n_scale, scales_payload = shape[0], sc # per-row fp32 scales
217
+ elif qt == 3:
218
+ data, n_scale = q8_0(w2d), 0
219
+ elif qt == 5:
220
+ data, n_scale = q4_k(w2d), 0
221
+ elif qt == 6:
222
+ data, n_scale = ternary(w2d), 0
223
+ kb = key.encode('utf-8')
224
+ out.write(struct.pack('<I', len(kb))); out.write(kb)
225
+ out.write(struct.pack('<I', len(shape)))
226
+ for d in shape: out.write(struct.pack('<I', d))
227
+ out.write(struct.pack('<B', qt))
228
+ out.write(struct.pack('<Q', len(data)))
229
+ out.write(data)
230
+ out.write(struct.pack('<I', n_scale))
231
+ if scales_payload:
232
+ out.write(scales_payload)
233
+ stats[qt] = stats.get(qt, 0) + len(data)
234
+ if idx % 20 == 0:
235
+ L('[%d/%d] %s qtype=%d %.1fMB %.0fs' % (idx, len(meta), key, qt, len(data) / 1048576, time.time() - t0))
236
+ out.close(); src.close()
237
+ total = os.path.getsize(OUT)
238
+ L('DONE total=%.2f GB (%.0f MB f32-eq saved) in %.0fs' % (total / 1073741824, total / 1048576, time.time() - t0))
239
+ for key, c in n_tern_stats:
240
+ L('TERN %s cos=%.4f' % (key, c))
241
+ kept = sum(1 for _, c in n_tern_stats if c >= TERN_COS_MIN)
242
+ L('TERN_SUMMARY candidates=%d kept=%d fell_back=%d' % (len(n_tern_stats), kept, len(n_tern_stats) - kept))
243
+ for qt in sorted(stats):
244
+ L(' qtype %d: %.1f MB' % (qt, stats[qt] / 1048576))
245
+
246
+ if __name__ == '__main__':
247
+ main()