File size: 19,868 Bytes
9ad45b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
"""compress_baselines.py -- pruning / quantization baselines on LLaDA-8B-Base.

Methods: wanda, sparsegpt (pruning), gptq, awq (weight-only FAKE quant: quantize ->
dequantize -> fp16). Each nn.Linear -> a dense bf16 weight saved as {prefix}_weight.pt
(loaded by eval/llada_harness.py replace_with_decoded_weights -- auto-detected since no
*_A.pt is present). Uses the SAME calib .pt as the SVD arms (base/ours/asvd) so all
methods are calibration-matched. X^T X (second moment of inputs) is collected in one
forward pass, GPU-resident and memory-binned, offloaded to CPU per bin (like compress.py).

Pruning sparsity default 0.23 == keep 0.77 == the model_ratio 0.8 retention of the SVD arms.
"""
import os, sys, time, argparse
import torch
import torch.nn as nn

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import common as C


# ── calib X^T X collection (GPU-resident, binned) β€” same as compress.py ──────────
# NB Traj-MC fix: with the noised (ours) calib ~47% of positions are MASK_ID, whose
# input embedding is a single degenerate vector. Accumulating those rows into X^T X
# injects a giant rank-1 spike (breaks the cholesky-inverse error comp in
# gptq/sparsegpt) and washes out the per-channel outlier signal (wanda/awq under-
# respond). The calib DATA is the full trajectory (t~U[0,1], t=1 full-mask kept) β€”
# we only restrict the Monte-Carlo *statistic* to the real-token rows, i.e. the
# activations the compressed layer actually has to preserve. mask_id=None (clean
# base calib, 0% mask) is a no-op, so the base arm is byte-for-byte unchanged.
def collect_xtx_cpu(model, input_ids, target_mods, device, log_prefix="", mask_id=None):
    """Legacy path: per-sample D2H copy + CPU accumulate (~33s/sample on A5000).
    Kept only for --xtx_cpu reproduction; collect_xtx_gpu computes the identical
    statistic on GPU."""
    cov, ntok = {}, {}
    _keep = {"m": None}  # per-sample real-token row mask, shape [L], set before each fwd

    def make_hook(name, in_dim):
        def hook(module, inp, out):
            x = inp[0].detach().reshape(-1, in_dim).float()
            k = _keep["m"]
            if k is not None and k.numel() == x.shape[0]:
                x = x[k]                      # drop MASK-position rows
            if x.shape[0] == 0:               # fully-masked sample -> contributes nothing
                return
            if name not in cov:
                cov[name] = torch.zeros(in_dim, in_dim, dtype=torch.float32)  # CPU 累加,GPU εͺζ”Ύζ¨‘εž‹
                ntok[name] = 0
            cov[name] += (x.t() @ x).cpu()
            ntok[name] += x.shape[0]
        return hook

    handles = [m.register_forward_hook(make_hook(n, m.in_features))
               for n, m in target_mods.items()]
    t0 = time.time()
    with torch.no_grad():
        for i in range(input_ids.shape[0]):
            ids = input_ids[i:i + 1].to(device)
            _keep["m"] = None if mask_id is None else (ids.reshape(-1) != mask_id)
            model(ids)
            torch.cuda.empty_cache()
            if i % 200 == 0:
                print(f"  {log_prefix}collect {i}/{input_ids.shape[0]} ({time.time()-t0:.0f}s)", flush=True)
    for h in handles:
        h.remove()
    out = {n: (cov[n] / ntok[n]) for n in list(cov)}
    for n in list(cov):
        cov[n] = None
    torch.cuda.empty_cache()
    return out


def collect_xtx_gpu(model, input_ids, target_mods, device, log_prefix="", mask_id=None):
    """Same statistic as collect_xtx_cpu (incl. the real-token row restriction), but
    accumulated ON GPU via addmm_ (no per-forward D2H copy), offloaded to CPU once at
    the end β€” ported from compress.py. Caller must ensure this group's total XtX
    (sum of in_features^2 * 4 bytes) fits on the GPU beside the model (use plan_bins)."""
    cov, ntok = {}, {}
    _keep = {"m": None}  # per-sample real-token row mask, shape [L], set before each fwd

    def make_hook(name, in_dim):
        def hook(module, inp, out):
            x = inp[0].detach().reshape(-1, in_dim).float()
            k = _keep["m"]
            if k is not None and k.numel() == x.shape[0]:
                x = x[k]                      # drop MASK-position rows
            if x.shape[0] == 0:               # fully-masked sample -> contributes nothing
                return
            if name not in cov:
                cov[name] = torch.zeros(in_dim, in_dim, dtype=torch.float32, device=device)
                ntok[name] = 0
            cov[name].addmm_(x.t(), x)  # cov += x^T x, in-place on GPU
            ntok[name] += x.shape[0]
        return hook

    handles = [m.register_forward_hook(make_hook(n, m.in_features))
               for n, m in target_mods.items()]
    t0 = time.time()
    with torch.no_grad():
        for i in range(input_ids.shape[0]):
            ids = input_ids[i:i + 1].to(device)
            _keep["m"] = None if mask_id is None else (ids.reshape(-1) != mask_id)
            model(ids)
            if i % 200 == 0:
                print(f"  {log_prefix}collect {i}/{input_ids.shape[0]} ({time.time()-t0:.0f}s)", flush=True)
    for h in handles:
        h.remove()
    out = {}
    for n in list(cov):
        out[n] = (cov[n] / ntok[n]).cpu()
        cov[n] = None
    del cov
    torch.cuda.empty_cache()
    return out


def plan_bins(targets, budget_bytes):
    bins, cur, cur_b = [], {}, 0
    for name, mod in targets:
        b = mod.in_features * mod.in_features * 4
        if cur and cur_b + b > budget_bytes:
            bins.append(cur); cur, cur_b = {}, 0
        cur[name] = mod; cur_b += b
    if cur:
        bins.append(cur)
    return bins


# ── methods (each returns a dense W' of the SAME shape) ──────────────────────────
def wanda_prune(W, XtX, sparsity, prune_n=2, prune_m=4):
    """Wanda: importance = |W| * ||X_col||.
    prune_n:prune_m = N:M STRUCTURED sparsity (default 2:4 = zero the 2 lowest of every
    4 consecutive input cols -> 50% sparse, hardware-accelerated). prune_n=0 falls back
    to UNSTRUCTURED at `sparsity` (no real speedup)."""
    scaler = torch.sqrt(torch.diag(XtX).clamp(min=0)).reshape(1, -1)  # [1, in]
    metric = W.abs() * scaler
    W = W.clone()
    if prune_n > 0:
        out, inc = W.shape
        g = inc // prune_m
        mt = metric[:, :g * prune_m].reshape(out, g, prune_m)
        idx = mt.topk(prune_n, dim=2, largest=False).indices          # 2 lowest per group of 4
        mask = torch.zeros_like(mt, dtype=torch.bool).scatter_(2, idx, True).reshape(out, g * prune_m)
        Wv = W[:, :g * prune_m]
        Wv[mask] = 0.0
        W[:, :g * prune_m] = Wv
    else:
        k = int(W.shape[1] * sparsity)
        if k > 0:
            idx = torch.topk(metric, k, dim=1, largest=False).indices
            W.scatter_(1, idx, 0.0)
    return W


def sparsegpt_prune(W, H, sparsity, blocksize=128, percdamp=0.01, prune_n=2, prune_m=4):
    """Faithful port of SparseGPT fasterprune (Sink-Aware-Pruning/lib/sparsegpt.py).
    H = X X^T (== our XtX). Column-wise pruning with Hessian error compensation.
    prune_n:prune_m = N:M STRUCTURED (default 2:4, hardware-accelerated). prune_n=0 ->
    UNSTRUCTURED at `sparsity`."""
    W = W.clone().float()
    H = H.clone().float()
    cols = W.shape[1]
    dead = torch.diag(H) == 0
    H[dead, dead] = 1.0
    W[:, dead] = 0.0
    damp = percdamp * torch.mean(torch.diag(H))
    diag = torch.arange(cols, device=W.device)
    H[diag, diag] += damp
    H = torch.linalg.cholesky(H)
    H = torch.cholesky_inverse(H)
    H = torch.linalg.cholesky(H, upper=True)
    Hinv = H
    for i1 in range(0, cols, blocksize):
        i2 = min(i1 + blocksize, cols)
        count = i2 - i1
        W1 = W[:, i1:i2].clone()
        Q1 = torch.zeros_like(W1)
        Err1 = torch.zeros_like(W1)
        Hinv1 = Hinv[i1:i2, i1:i2]
        mask1 = torch.zeros_like(W1, dtype=torch.bool)
        if prune_n == 0:   # unstructured: one threshold over the block
            tmp = W1 ** 2 / (torch.diag(Hinv1).reshape((1, -1))) ** 2
            thresh = torch.sort(tmp.flatten())[0][int(tmp.numel() * sparsity)]
            mask1 = tmp <= thresh
        for i in range(count):
            w = W1[:, i]
            d = Hinv1[i, i]
            if prune_n != 0 and i % prune_m == 0:   # N:M: pick prune_n lowest of the next prune_m cols
                tmp = W1[:, i:i + prune_m] ** 2 \
                    / (torch.diag(Hinv1)[i:i + prune_m].reshape((1, -1))) ** 2
                mask1.scatter_(1, i + torch.topk(tmp, prune_n, dim=1, largest=False).indices, True)
            q = w.clone()
            q[mask1[:, i]] = 0.0
            Q1[:, i] = q
            err1 = (w - q) / d
            W1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0))
            Err1[:, i] = err1
        W[:, i1:i2] = Q1
        W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])
    return W


# ── GPTQ (IST-DASLab/gptq) weight-only fake-quant, asym per-row per-group ────────
def _gptq_find_params(W, bits, sym=False):
    maxq = 2 ** bits - 1
    x = W.flatten(1)
    zeros = torch.zeros(x.shape[0], device=x.device)
    xmin = torch.minimum(x.min(1)[0], zeros)
    xmax = torch.maximum(x.max(1)[0], zeros)
    if sym:
        xmax = torch.maximum(xmin.abs(), xmax)
        neg = xmin < 0
        xmin[neg] = -xmax[neg]
    deg = (xmin == 0) & (xmax == 0)
    xmin[deg], xmax[deg] = -1, 1
    scale = (xmax - xmin) / maxq
    zero = torch.full_like(scale, (maxq + 1) / 2) if sym else torch.round(-xmin / scale)
    return scale.unsqueeze(1), zero.unsqueeze(1), maxq


def _gptq_quant(x, scale, zero, maxq):
    q = torch.clamp(torch.round(x / scale) + zero, 0, maxq)
    return scale * (q - zero)


@torch.no_grad()
def gptq_quantize(W, H, bits=3, groupsize=128, percdamp=0.01, blocksize=128, sym=False, actorder=False):
    orig_dtype = W.dtype
    W = W.clone().float()
    H = H.clone().float()
    rows, columns = W.shape
    dev = W.device
    dead = torch.diag(H) == 0
    H[dead, dead] = 1
    W[:, dead] = 0
    invperm = None
    if actorder:
        perm = torch.argsort(torch.diag(H), descending=True)
        W = W[:, perm]; H = H[perm][:, perm]
        invperm = torch.argsort(perm)
    if groupsize == -1:
        scale, zero, maxq = _gptq_find_params(W, bits, sym)
    else:
        maxq = 2 ** bits - 1
        scale = zero = None
    Q = torch.zeros_like(W)
    damp = percdamp * torch.mean(torch.diag(H))
    diag = torch.arange(columns, device=dev)
    H[diag, diag] += damp
    H = torch.linalg.cholesky(H)
    H = torch.cholesky_inverse(H)
    Hinv = torch.linalg.cholesky(H, upper=True)
    for i1 in range(0, columns, blocksize):
        i2 = min(i1 + blocksize, columns)
        count = i2 - i1
        W1 = W[:, i1:i2].clone()
        Q1 = torch.zeros_like(W1)
        Err1 = torch.zeros_like(W1)
        Hinv1 = Hinv[i1:i2, i1:i2]
        for i in range(count):
            w = W1[:, i]
            d = Hinv1[i, i]
            if groupsize != -1 and (i1 + i) % groupsize == 0:
                scale, zero, _ = _gptq_find_params(W[:, (i1 + i):(i1 + i + groupsize)], bits, sym)
            q = _gptq_quant(w.unsqueeze(1), scale, zero, maxq).flatten()
            Q1[:, i] = q
            err1 = (w - q) / d
            W1[:, i:] -= err1.unsqueeze(1).matmul(Hinv1[i, i:].unsqueeze(0))
            Err1[:, i] = err1
        Q[:, i1:i2] = Q1
        W[:, i2:] -= Err1.matmul(Hinv[i1:i2, i2:])
    if actorder:
        Q = Q[:, invperm]
    return Q.to(orig_dtype)


# ── AWQ (mit-han-lab/llm-awq) activation-aware fake-quant, asym group-wise ────────
def _awq_pseudo_quant(w, n_bit=4, group=128, zero_point=True):
    shape = w.shape
    if group > 0:
        assert shape[-1] % group == 0
        w = w.reshape(-1, group)
    if zero_point:
        max_val = w.amax(dim=1, keepdim=True)
        min_val = w.amin(dim=1, keepdim=True)
        max_int, min_int = 2 ** n_bit - 1, 0
        scales = (max_val - min_val).clamp(min=1e-5) / max_int
        zeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int)
        w = (torch.clamp(torch.round(w / scales) + zeros, min_int, max_int) - zeros) * scales
    else:
        max_val = w.abs().amax(dim=1, keepdim=True).clamp(min=1e-5)
        max_int, min_int = 2 ** (n_bit - 1) - 1, -(2 ** (n_bit - 1))
        scales = max_val / max_int
        w = torch.clamp(torch.round(w / scales), min_int, max_int) * scales
    return w.reshape(shape)


@torch.no_grad()
def awq_quantize(W, act_scale, bits=4, groupsize=128, n_grid=20, zero_point=True, H=None, do_clip=True):
    orig_dtype = W.dtype
    W = W.clone().float()
    act_scale = act_scale.float().to(W.device)

    def loss(Wq):
        d = W - Wq
        if H is not None:
            return torch.einsum('oi,ij,oj->', d, H.float(), d).item()
        return (d.pow(2) * act_scale.pow(2).unsqueeze(0)).sum().item()

    best_loss, best_s = float('inf'), torch.ones_like(act_scale)
    for i in range(n_grid):
        ratio = i / n_grid
        s = act_scale.pow(ratio).clamp(min=1e-4)
        s = s / (s.max() * s.min()).sqrt()
        Wq = _awq_pseudo_quant(W * s.unsqueeze(0), bits, groupsize, zero_point) / s.unsqueeze(0)
        L = loss(Wq)
        if L < best_loss:
            best_loss, best_s = L, s
    Ws = W * best_s.unsqueeze(0)
    if do_clip:
        g = groupsize if groupsize > 0 else Ws.shape[1]
        wg = Ws.reshape(Ws.shape[0], -1, g)
        org_max = wg.abs().amax(dim=-1, keepdim=True)
        best_max = org_max.clone()
        best_err = torch.full_like(org_max, float('inf'))
        for i in range(int(0.5 * n_grid)):
            mx = org_max * (1 - i / n_grid)
            q = _awq_pseudo_quant(torch.clamp(wg, -mx, mx).reshape(-1, g), bits, g, zero_point).reshape(wg.shape)
            err = (q - wg).pow(2).mean(dim=-1, keepdim=True)
            better = err < best_err
            best_err = torch.where(better, err, best_err)
            best_max = torch.where(better, mx, best_max)
        Ws = torch.clamp(wg, -best_max, best_max).reshape(Ws.shape)
    Wq = _awq_pseudo_quant(Ws, bits, groupsize, zero_point) / best_s.unsqueeze(0)
    return Wq.to(orig_dtype)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--calib", required=True)
    ap.add_argument("--method", required=True, choices=["wanda", "sparsegpt", "gptq", "awq"])
    ap.add_argument("--sparsity", type=float, default=0.23,
                    help="prune fraction (0.23 == keep 0.77 == model_ratio 0.8 retention)")
    ap.add_argument("--bits", type=int, default=4)
    ap.add_argument("--prune_n", type=int, default=2,
                    help="N:M structured sparsity for wanda/sparsegpt (default 2:4, "
                         "hardware-accelerated). Set 0 for unstructured at --sparsity.")
    ap.add_argument("--prune_m", type=int, default=4)
    ap.add_argument("--groupsize", type=int, default=128)
    ap.add_argument("--layer_type", choices=["all", "attn", "mlp"], default="all")
    ap.add_argument("--model_path", default=C.DEFAULT_MODEL_PATH)
    ap.add_argument("--save_path", required=True)
    ap.add_argument("--nsamples", type=int, default=0)
    ap.add_argument("--xtx_budget_gb", type=float, default=0.0,
                    help="max GB of XtX kept on GPU per collection pass. 0 = AUTO: "
                         "free GPU memory after the model is resident minus headroom, "
                         "so the same command runs on a 24GB or a 48GB card (only the "
                         "number of passes changes -- binning never touches math).")
    ap.add_argument("--xtx_headroom_gb", type=float, default=6.0)
    ap.add_argument("--xtx_cpu", action="store_true",
                    help="legacy per-sample CPU accumulation (slow; repro only)")
    args = ap.parse_args()

    device = "cuda" if torch.cuda.is_available() else "cpu"
    os.makedirs(args.save_path, exist_ok=True)
    t0 = time.time()

    blob = torch.load(args.calib, map_location="cpu")
    input_ids = blob["input_ids"]
    if args.nsamples and args.nsamples < input_ids.shape[0]:
        input_ids = input_ids[: args.nsamples]
    arm = blob.get("arm", "unknown")
    print(f"[baselines] method={args.method} calib={os.path.basename(args.calib)} "
          f"arm={arm} N={input_ids.shape[0]} sparsity={args.sparsity} bits={args.bits}", flush=True)

    model, tok = C.load_model(args.model_path, device=device)
    targets = list(C.iter_target_linears(model, args.layer_type))
    print(f"[baselines] target linears ({args.layer_type}) = {len(targets)}", flush=True)

    if args.xtx_cpu:
        cov = collect_xtx_cpu(model, input_ids, dict(targets), device, mask_id=C.MASK_ID)
    else:
        budget_gb = args.xtx_budget_gb
        if budget_gb <= 0:
            if device == "cuda":
                free_b, total_b = torch.cuda.mem_get_info()
                budget_gb = max(2.0, free_b / 1024 ** 3 - args.xtx_headroom_gb)
                print(f"[baselines] AUTO xtx budget: free={free_b/1024**3:.1f}GB "
                      f"total={total_b/1024**3:.1f}GB headroom={args.xtx_headroom_gb}GB "
                      f"-> budget={budget_gb:.1f}GB", flush=True)
            else:
                budget_gb = 8.0
        bins = plan_bins(targets, budget_gb * 1024 ** 3)
        print(f"[baselines] {len(targets)} target linears -> {len(bins)} XtX pass(es) "
              f"(budget {budget_gb:.1f}GB/pass)", flush=True)
        cov = {}
        for bi, b in enumerate(bins):
            gb = sum(m.in_features ** 2 * 4 for m in b.values()) / 1024 ** 3
            print(f"[baselines] XtX pass {bi+1}/{len(bins)}: {len(b)} layers, {gb:.1f}GB", flush=True)
            cov.update(collect_xtx_gpu(model, input_ids, b, device,
                                       log_prefix=f"p{bi+1} ", mask_id=C.MASK_ID))
    print(f"[baselines] XtX collected for {len(cov)} layers ({time.time()-t0:.0f}s)", flush=True)

    n_done = 0
    for name, mod in targets:
        if name not in cov:
            continue
        W = mod.weight.data.cpu().float().to(device)
        H = cov[name].to(device)
        if args.method == "wanda":
            Wq = wanda_prune(W, H, args.sparsity, args.prune_n, args.prune_m)
        elif args.method == "sparsegpt":
            Wq = sparsegpt_prune(W, H, args.sparsity, prune_n=args.prune_n, prune_m=args.prune_m)
        elif args.method == "gptq":
            Wq = gptq_quantize(W, H, args.bits, args.groupsize)
        elif args.method == "awq":
            act = torch.sqrt(torch.diag(H).clamp(min=0))  # per-channel RMS ~ mean|x| proxy
            Wq = awq_quantize(W, act, args.bits, args.groupsize, H=H)
        torch.save(Wq.to(torch.bfloat16).cpu(),
                   os.path.join(args.save_path, f"{name.replace('.', '_')}_weight.pt"))
        del W, H, Wq
        torch.cuda.empty_cache()
        n_done += 1
        if n_done % 30 == 0:
            print(f"  [{n_done}/{len(targets)}] {name}", flush=True)

    _structured = (args.method in ("wanda", "sparsegpt") and args.prune_n > 0)
    summary = {"method": args.method, "arm": arm, "sparsity": args.sparsity,
               "bits": args.bits, "groupsize": args.groupsize,
               "structured": f"{args.prune_n}:{args.prune_m}" if _structured else "unstructured",
               "real_sparsity": (args.prune_n / args.prune_m) if _structured else args.sparsity,
               "layer_type": args.layer_type, "n_layers": n_done,
               "calib_file": os.path.basename(args.calib), "git_hash": C.git_hash(),
               "wall_clock_sec": time.time() - t0, "save_path": args.save_path}
    C.dump_json(summary, os.path.join(args.save_path, "compression_summary.json"))
    print(f"[baselines] done: {n_done} layers -> {args.save_path}  ({(time.time()-t0)/60:.1f}min)", flush=True)


if __name__ == "__main__":
    main()