File size: 13,841 Bytes
fdc6474
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#!/usr/bin/env python3
"""Converged activation-aware AQLM for the cold experts of GLM-5.2.

Per (layer, projection), over the layer's cold experts:
  objective:  sum_e || (W_e - What_e) diag(sqrt(h_e)) ||_F^2
              with h_e = per-expert diagonal input Hessian E[x^2] from real
              routed calibration activations (w13: layer input x; w2: the
              expert's own silu(gate)*up computed with TEACHER weights)
  variables:  shared 65536x8 fp16 codebook (1 book, groups of 8 along the
              input dim), int16 codes per group, per-expert per-out-channel
              fp16 scales
  algorithm:  warm start from the existing shipped codes; iterate
              [weighted coordinate-descent re-encode (batched GEMM argmax
              under the h-weighted metric) -> closed-form weighted codebook
              update -> closed-form weighted scale refit] until the relative
              weighted error improvement < 0.2% or MAX_ITERS.

Teacher weights: original NVFP4 per-expert tensors, dequantized to fp32
(from /tmp/glm52-hot-dl2 ranged downloads; layers 3,4,5,8,74-77 from
/data/glm52-old-layerwise which stores all 256 experts).

Output: /data/glm52-aqlm-conv/layer_N.pt with
  expert_ids int32 [nC] (ascending), w13_codes int16 [nC,1,4096,768],
  w13_codebooks fp16 [1,65536,8], w13_scales fp16 [nC,4096], w2c_* likewise
  ([nC,1,6144,256] codes), plus before/after weighted rel-err metrics.

Usage: aqlm_converge.py [--layers a,b,...] [--gpus 0,..7]
"""

import argparse
import json
import os
import time
from concurrent.futures import ProcessPoolExecutor

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CKPT = "/data/glm52"
OLD_LAYERWISE = "/data/glm52-old-layerwise"
DL = "/tmp/glm52-hot-dl2"
ACTS = "/data/glm52-acts"
OUT = "/data/glm52-aqlm-conv"
LOCAL_TEACHER_LAYERS = {3, 4, 5, 8, 74, 75, 76, 77}

ENTRIES = 65536
G = 8
MAX_ITERS = 6
TOL = 2e-3
CHUNK = 16384          # groups per scoring GEMM chunk
FP4_LUT = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0,
           -0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0]


# ---------------------------------------------------------------- teachers
class TeacherReader:
    """Original NVFP4 per-expert tensors from download regions or the
    old layer-wise checkpoint."""

    DT = None

    def __init__(self):
        import torch

        TeacherReader.DT = {"U8": torch.uint8, "F8_E4M3": torch.uint8,
                            "BF16": torch.bfloat16, "F32": torch.float32}
        self.headers = json.load(open(f"{DL}/headers.json"))
        self.wm = {}
        for shard, h in self.headers.items():
            for name in h["header"]:
                if name != "__metadata__":
                    self.wm[name] = shard
        self.regions = {}
        for shard in self.headers:
            d = f"{DL}/regions/{shard}"
            regs = []
            if os.path.isdir(d):
                for f in os.listdir(d):
                    p = os.path.join(d, f)
                    regs.append((int(f[:-4]), p, os.path.getsize(p)))
            self.regions[shard] = sorted(regs)
        idx = json.load(
            open(f"{OLD_LAYERWISE}/model.safetensors.index.json"))
        self.old_wm = idx["weight_map"]
        self._old_open = {}

    def _from_regions(self, name):
        import torch

        shard = self.wm[name]
        info = self.headers[shard]["header"][name]
        b, e = info["data_offsets"]
        for rb, path, sz in self.regions[shard]:
            if rb <= b and e <= rb + sz:
                with open(path, "rb") as fh:
                    fh.seek(b - rb)
                    buf = fh.read(e - b)
                return torch.frombuffer(
                    bytearray(buf), dtype=self.DT[info["dtype"]]
                ).reshape(info["shape"])
        raise KeyError(name)

    def _from_old(self, name):
        from safetensors import safe_open

        shard = self.old_wm[name]
        if shard not in self._old_open:
            self._old_open[shard] = safe_open(
                f"{OLD_LAYERWISE}/{shard}", framework="pt")
        return self._old_open[shard].get_tensor(name)

    def get(self, li, e, proj, part):
        name = f"model.layers.{li}.mlp.experts.{e}.{proj}.{part}"
        if li in LOCAL_TEACHER_LAYERS:
            return self._from_old(name)
        return self._from_regions(name)


def dequant_teacher(tr, li, e, proj, device, lut):
    import torch

    packed = tr.get(li, e, proj, "weight").to(device)
    ws = tr.get(li, e, proj, "weight_scale").view(torch.uint8).to(device)
    ws2 = tr.get(li, e, proj, "weight_scale_2").to(device).float()
    lo = (packed & 0x0F).long()
    hi = (packed >> 4).long()
    vals = torch.empty(packed.shape[0], packed.shape[1] * 2,
                       dtype=torch.float32, device=device)
    vals[:, 0::2] = lut[lo]
    vals[:, 1::2] = lut[hi]
    scale = ws.view(torch.float8_e4m3fn).float().repeat_interleave(16, dim=1)
    return vals * scale * ws2


# ------------------------------------------------------------- optimization
def weighted_encode(vecs, hw, cent, out_codes):
    """CD re-encode: for each group i pick argmin_c sum_j hw[i,j]*(v-c)^2.
    vecs,hw: [N,8] fp32 (hw = per-group diag weights). cent: [K,8] fp32."""
    import torch

    for s in range(0, vecs.shape[0], CHUNK):
        v = vecs[s:s + CHUNK]
        w = hw[s:s + CHUNK]
        # cost = sum_j w_j c_j^2 - 2 sum_j w_j v_j c_j  (+const)
        a = w @ (cent.t() ** 2)          # [n,K]
        b = (v * w) @ cent.t()           # [n,K]
        out_codes[s:s + CHUNK] = (a - 2 * b).argmin(-1).to(torch.int32)
    return out_codes


def codebook_update(vecs, hw, codes, K):
    """Closed-form weighted LS per entry per dim:
    c_kj = sum_{i in k} w_ij v_ij / sum w_ij."""
    import torch

    num = torch.zeros(K, G, device=vecs.device)
    den = torch.zeros(K, G, device=vecs.device)
    idx = codes.long()
    num.index_add_(0, idx, vecs * hw)
    den.index_add_(0, idx, hw)
    cent = num / den.clamp_min(1e-12)
    dead = den.sum(-1) < 1e-12
    return cent, dead


def fit_projection(w, h, codes0, cb0, scales0, gen, log, tag):
    """w [E,M,K] fp16 teacher; h [E,K] diag Hessian; warm-start codes/cb/scales.
    Expert-chunked to bound peak memory. Returns codes int16 [E,1,M,K/8],
    cb fp16 [1,65536,8], scales fp16 [E,M]."""
    import torch

    E, M, K = w.shape
    NG = K // G
    dev = w.device
    EC = max(1, min(48, E))            # experts per pass
    scales = scales0.float().clamp_min(1e-8)
    cent = cb0[0].float().clone()
    codes = (codes0.reshape(E, M, NG).to(torch.int32) & 0xFFFF).clone()

    def chunk_tgt(e0, e1):
        tgt = (w[e0:e1].float() / scales[e0:e1].unsqueeze(-1))
        hh = (h[e0:e1].reshape(e1 - e0, 1, NG, G)
              .expand(e1 - e0, M, NG, G))
        return tgt.reshape(e1 - e0, M, NG, G), hh

    def weighted_err():
        num = torch.zeros((), device=dev, dtype=torch.float64)
        den = torch.zeros((), device=dev, dtype=torch.float64)
        for e0 in range(0, E, 16):
            e1 = min(e0 + 16, E)
            tgt, hh = chunk_tgt(e0, e1)
            dec = cent[codes[e0:e1].long().reshape(-1)].reshape(
                e1 - e0, M, NG, G)
            num += ((tgt - dec).pow(2) * hh).sum().double()
            den += (tgt.pow(2) * hh).sum().double()
            del tgt, hh, dec
        return (num / den.clamp_min(1e-12)).item()

    e0_err = weighted_err()
    prev = e0_err
    for it in range(MAX_ITERS):
        num_cb = torch.zeros(ENTRIES, G, device=dev)
        den_cb = torch.zeros(ENTRIES, G, device=dev)
        for e0 in range(0, E, EC):
            e1 = min(e0 + EC, E)
            tgt, hh = chunk_tgt(e0, e1)
            v = tgt.reshape(-1, G)
            hwv = hh.reshape(-1, G)
            cflat = codes[e0:e1].reshape(-1).clone()
            weighted_encode(v, hwv, cent, cflat)
            codes[e0:e1] = cflat.reshape(e1 - e0, M, NG)
            idx = cflat.long()
            num_cb.index_add_(0, idx, v * hwv)
            den_cb.index_add_(0, idx, hwv)
            del tgt, hh, v, hwv, cflat
            torch.cuda.empty_cache()
        newc = num_cb / den_cb.clamp_min(1e-12)
        dead = den_cb.sum(-1) < 1e-12
        newc[dead] = cent[dead]
        cent = newc
        # weighted per-channel scale refit, chunked
        for e0 in range(0, E, EC):
            e1 = min(e0 + EC, E)
            dec = cent[codes[e0:e1].long().reshape(-1)].reshape(
                e1 - e0, M, K)
            wf = w[e0:e1].float()
            hh = h[e0:e1].unsqueeze(1)
            num = (wf * hh * dec).sum(-1)
            den = ((dec * dec) * hh).sum(-1).clamp_min(1e-10)
            scales[e0:e1] = (num / den).clamp(1e-6, None)
            del dec, wf, hh
            torch.cuda.empty_cache()
        cur = weighted_err()
        log(f"    {tag} iter{it}: weighted rel-err {cur:.5f}")
        if prev - cur < TOL * prev:
            break
        prev = cur

    codes16 = torch.where(codes >= 32768, codes - 65536, codes).to(torch.int16)
    return (codes16.reshape(E, 1, M, NG).cpu(),
            cent.half().unsqueeze(0).cpu(),
            scales.half().cpu(), e0_err, prev)


def process_layer(li, gpu):
    import torch

    t0 = time.time()
    dev = f"cuda:{gpu}"
    torch.cuda.set_device(gpu)
    gen = torch.Generator(device=dev)
    gen.manual_seed(777 + li)
    lut = torch.tensor(FP4_LUT, dtype=torch.float32, device=dev)

    def log(m):
        print(f"[L{li} gpu{gpu}] {m}", flush=True)

    outp = os.path.join(OUT, f"layer_{li}.pt")
    if os.path.exists(outp):
        log("done, skip")
        return li, None

    # cold set + shipped warm start from the live checkpoint
    from safetensors import safe_open

    idx = json.load(open(f"{CKPT}/model.safetensors.index.json"))
    wm = idx["weight_map"]
    p = f"model.layers.{li}.mlp.experts"
    opened = {}

    def ck(name):
        shard = wm[f"{p}.{name}"]
        if shard not in opened:
            opened[shard] = safe_open(f"{CKPT}/{shard}", framework="pt")
        return opened[shard].get_tensor(f"{p}.{name}")

    kind = ck("hyb_kind")
    cold = (kind == 2).nonzero().flatten().tolist()
    nC = len(cold)

    # activations -> per-expert diag Hessians
    acts = torch.load(f"{ACTS}/acts_layer{li}.pt", map_location="cpu",
                      weights_only=True)
    x = acts["x"].to(dev).float()                      # [T,6144]
    tk_ids = acts["topk_ids"].to(dev).long()           # [T,8]
    h13 = torch.zeros(nC, 6144, device=dev)
    h2 = torch.zeros(nC, 2048, device=dev)
    pos = {e: j for j, e in enumerate(cold)}

    tr = TeacherReader()
    log(f"cold={nC}; dequant teachers + Hessians")
    w13 = torch.empty(nC, 4096, 6144, dtype=torch.float16, device=dev)
    w2 = torch.empty(nC, 6144, 2048, dtype=torch.float16, device=dev)
    hits = (tk_ids.unsqueeze(-1) ==
            torch.tensor(cold, device=dev).view(1, 1, -1))   # [T,8,nC]
    tok_of = hits.any(1)                                     # [T,nC]
    for j, e in enumerate(cold):
        gate = dequant_teacher(tr, li, e, "gate_proj", dev, lut)
        up = dequant_teacher(tr, li, e, "up_proj", dev, lut)
        down = dequant_teacher(tr, li, e, "down_proj", dev, lut)
        w13[j, :2048] = gate.half()
        w13[j, 2048:] = up.half()
        w2[j] = down.half()
        xe = x[tok_of[:, j]]
        if xe.shape[0] < 16:                    # rarely-routed: uniform H
            h13[j] = x.pow(2).mean(0)
            mid = torch.nn.functional.silu(x[:512] @ gate.t()) * (x[:512] @ up.t())
        else:
            h13[j] = xe.pow(2).mean(0)
            mid = torch.nn.functional.silu(xe @ gate.t()) * (xe @ up.t())
        h2[j] = mid.float().pow(2).mean(0).clamp_min(1e-10)
        h13[j] = h13[j].clamp_min(1e-10)
    del hits, tok_of, x
    torch.cuda.empty_cache()

    # warm starts (shipped arrays are cold-only, ascending == our order)
    c13 = ck("w13_codes").to(dev).view(torch.int16)
    cb13 = ck("w13_codebooks").to(dev)
    s13 = ck("w13_scales").to(dev)
    c2 = ck("w2c_codes").to(dev)
    cb2 = ck("w2c_codebooks").to(dev)
    s2 = ck("w2c_scales").to(dev)

    log("fit w13")
    r13 = fit_projection(w13, h13, c13.view(torch.uint16).int(), cb13.float(),
                         s13, gen, log, "w13")
    del w13
    torch.cuda.empty_cache()
    log("fit w2")
    r2 = fit_projection(w2, h2, c2.view(torch.uint16).int(), cb2.float(),
                        s2, gen, log, "w2")
    del w2
    torch.cuda.empty_cache()

    torch.save({
        "layer": li,
        "expert_ids": torch.tensor(cold, dtype=torch.int32),
        "w13_codes": r13[0], "w13_codebooks": r13[1], "w13_scales": r13[2],
        "w2c_codes": r2[0], "w2c_codebooks": r2[1], "w2c_scales": r2[2],
        "w13_err_before": r13[3], "w13_err_after": r13[4],
        "w2_err_before": r2[3], "w2_err_after": r2[4],
    }, outp)
    log(f"saved ({time.time()-t0:.0f}s)  w13 {r13[3]:.4f}->{r13[4]:.4f}  "
        f"w2 {r2[3]:.4f}->{r2[4]:.4f}")
    return li, (r13[3], r13[4], r2[3], r2[4])


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--layers", default=None)
    ap.add_argument("--gpus", default="0,1,2,3,4,5,6,7")
    args = ap.parse_args()
    os.makedirs(OUT, exist_ok=True)
    layers = list(range(3, 78))
    if args.layers:
        want = {int(x) for x in args.layers.split(",")}
        layers = [li for li in layers if li in want]
    gpus = [int(g) for g in args.gpus.split(",")]
    jobs = [(li, gpus[i % len(gpus)]) for i, li in enumerate(layers)]
    with ProcessPoolExecutor(max_workers=len(gpus)) as ex:
        for f in [ex.submit(process_layer, *j) for j in jobs]:
            li, m = f.result()
            if m:
                print(f"== L{li}: w13 {m[0]:.4f}->{m[1]:.4f} "
                      f"w2 {m[2]:.4f}->{m[3]:.4f}", flush=True)


if __name__ == "__main__":
    main()