eyad-silx commited on
Commit
54ab5f9
·
verified ·
1 Parent(s): 781cd92

Upload podflow/calib_train3_v9.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. podflow/calib_train3_v9.py +409 -0
podflow/calib_train3_v9.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact-objective joint calibration: per-(row,128-block) PQ2_0 scale factors + inner norms.
2
+
3
+ Objective mirrors eval_service/metrics.py compare() exactly:
4
+ KL = sum over answer rows of KL(teacher || model) where each side's distribution is
5
+ FinalHead/GgufHead applied to the LAST captured layer's state,
6
+ dist = sum over answer rows, mean over the 8 captured layers, of ||b-a||/||a||.
7
+ Teacher targets come from gen_teacher.py states files (hidden states per row x 8 layers).
8
+ Model states come from the surrogate forward (hidden_states[L] for L in 8..64 step 8,
9
+ pre-final-norm, matching llama-states' captured rows).
10
+ """
11
+ import argparse
12
+ import json
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+
18
+ REPO = "/root/ingot"
19
+
20
+
21
+ def main():
22
+ ap = argparse.ArgumentParser()
23
+ ap.add_argument("--surrogate", default="/dev/shm/king_hf.pt")
24
+ ap.add_argument("--gguf", default="/root/models/king-xii")
25
+ ap.add_argument("--teacher", default="/root/models/teacher")
26
+ ap.add_argument("--samples", default="/root/train_samples.json")
27
+ ap.add_argument("--out", default="/root/models/chal-calib")
28
+ ap.add_argument("--steps", type=int, default=400)
29
+ ap.add_argument("--lr", type=float, default=5e-3)
30
+ ap.add_argument("--dist-weight", type=float, default=0.5)
31
+ ap.add_argument("--max-states", type=int, default=64)
32
+ args = ap.parse_args()
33
+
34
+ import faulthandler, traceback
35
+ faulthandler.enable()
36
+ import torch
37
+ import torch.nn.functional as F
38
+ sys.path.insert(0, f"{REPO}/src")
39
+ from transformers import AutoModelForCausalLM
40
+ from common.gguf import read_header
41
+ from common.gguf.dequant import hadamard, rotation
42
+ from common.gguf.types import PQ2_0
43
+ from model_dedup.gguf_checkpoint import GgufCheckpoint, canonical, _untile
44
+ from eval_service import config
45
+ from eval_service.metrics import FinalHead, GgufHead
46
+
47
+ dev = "cuda"
48
+ torch.backends.cuda.matmul.allow_tf32 = True
49
+
50
+ sd = torch.load(args.surrogate, map_location="cpu", weights_only=True)
51
+ model = AutoModelForCausalLM.from_pretrained(args.teacher, dtype=torch.bfloat16,
52
+ device_map={"": dev}) # no 54GB CPU copy: 125GB-RAM pods
53
+ model.load_state_dict({k: v.to(torch.bfloat16) for k, v in sd.items()}, strict=False)
54
+ model.to(dev)
55
+ model.gradient_checkpointing_enable()
56
+ model.config.use_cache = False
57
+
58
+ ck = GgufCheckpoint(args.gguf, "cpu")
59
+ g = ck.g
60
+ hd = ck._heads
61
+ kv = g.kv
62
+ bs = int(kv["prism.hadamard.block_size"].value)
63
+ H = torch.from_numpy(hadamard(bs)).float().to(dev)
64
+
65
+ def tile(w, dim, head):
66
+ shape = list(w.shape)
67
+ x = w.reshape(*shape[:dim], hd.n_k, hd.rep, head, *shape[dim + 1:])
68
+ return x.transpose(dim, dim + 1).reshape(shape)
69
+
70
+ def rot_signs(name):
71
+ listed = [kv["prism.hadamard." + k].value
72
+ for k in ("weight_names", "inverse_weight_names")
73
+ if "prism.hadamard." + k in kv]
74
+ if not any(name in ns for ns in listed):
75
+ return None
76
+ widths = kv["prism.hadamard.sign_widths"].value
77
+ values = kv["prism.hadamard.sign_values"].value
78
+ width = g.tensors[name].shape[0]
79
+ at = 0
80
+ for w_ in widths:
81
+ if w_ == width:
82
+ return torch.from_numpy(np.asarray(values[at:at + w_], np.float32)).to(dev)
83
+ at += w_
84
+ return None
85
+
86
+ def hf_to_stored(name, w):
87
+ if name.startswith("blk.") and hd.rep > 1:
88
+ kind = name.split(".")[2]
89
+ if kind == "attn_qkv":
90
+ qk = 2 * hd.n_k * hd.k_dim
91
+ w = torch.cat([w[:qk], tile(w[qk:], 0, hd.v_dim)])
92
+ elif kind == "attn_gate":
93
+ w = tile(w, 0, hd.v_dim)
94
+ elif kind in ("ssm_alpha", "ssm_beta"):
95
+ w = tile(w, 0, 1)
96
+ elif kind == "ssm_out" and not (rot_signs(name) is not None and ck._grouped):
97
+ w = tile(w.T, 0, hd.v_dim).T
98
+ signs = rot_signs(name)
99
+ if signs is None:
100
+ return w, None
101
+ rows, width = w.shape
102
+ nblk = width // bs
103
+ h_l = H.to(w.device)
104
+ s_l = signs.to(w.device)
105
+ w = ((w.reshape(rows, nblk, bs) * s_l.reshape(nblk, bs)) @ h_l).reshape(rows, width)
106
+ return w, signs
107
+
108
+ # ---- wrap PQ2_0 linears (skip output.weight: GgufHead path) ----
109
+ for p in model.parameters():
110
+ p.requires_grad_(False)
111
+ gguf_to_hf = {}
112
+ sd_keys = list(sd)
113
+ for gg_name in g.tensors:
114
+ if gg_name == "output.weight" or gg_name.endswith("norm.weight"):
115
+ continue
116
+ canon = canonical(gg_name)
117
+ if canon is None or g.tensors[gg_name].ggml_type != PQ2_0:
118
+ continue
119
+ suffix = canon[len("model."):] if canon.startswith("model.") else canon
120
+ cands = [k for k in sd_keys if k == canon or k.endswith(suffix)]
121
+ if cands:
122
+ gguf_to_hf[gg_name] = cands[0]
123
+
124
+ scale_params = {}
125
+
126
+ # Effective-weight linear with per-(row,128-block) scales, dequantized ON THE FLY from the
127
+ # GGUF's native 2-bit blocks (6.8GB total vs 51GB for bf16 weights). Custom autograd.Function:
128
+ # nothing weight-sized is retained for backward (weights are recomputed), and training sees
129
+ # exactly the bytes the validator's eval loads.
130
+ def unpack(c):
131
+ # 4 two-bit codes per byte, LSB-first: code j (j=0..127) from byte j//4, bits (j%4)*2
132
+ return torch.stack([(c >> sh) & 3 for sh in (0, 2, 4, 6)], dim=-1).view(
133
+ c.shape[0], c.shape[1], 128)
134
+
135
+ class _ScaledWFn(torch.autograd.Function):
136
+ @staticmethod
137
+ def build_w(s, codes, d0, hb, signs, rows, nblk_w):
138
+ width = d0.shape[1] * 128
139
+ d_eff = d0.float() * s.squeeze(-1).float() # [rows, nblk] fp32
140
+ q = unpack(codes)
141
+ m1 = ((q.float() - 1.0) * d_eff.unsqueeze(-1)).to(torch.bfloat16).view(rows, width)
142
+ if hb is not None:
143
+ gsz = hb.shape[0]
144
+ m1 = (m1.reshape(rows, width // gsz, gsz) @ hb).reshape(rows, width) * signs
145
+ return m1
146
+
147
+ @staticmethod
148
+ def forward(ctx, x, s, codes, d0, hb, signs, bias, rows, nblk_w, pr, pc):
149
+ # stored order -> HF order: the fork stores linear-attention value heads tiled
150
+ # [rep, n_k, hd]; the HF model expects them grouped [n_k, rep, hd] (GgufCheckpoint
151
+ # _hf_order). Missing this fed head-permuted weights to the model in v7.
152
+ w = _ScaledWFn.build_w(s, codes, d0, hb, signs, rows, nblk_w)
153
+ if pr is not None:
154
+ w = w[pr]
155
+ if pc is not None:
156
+ w = w[:, pc]
157
+ if x.dtype in (torch.long, torch.int, torch.int32, torch.int64):
158
+ y = F.embedding(x, w) # token_embd: indices in, rows gathered from effective weight
159
+ else:
160
+ y = F.linear(x, w.to(x.dtype), bias)
161
+ ctx.save_for_backward(x, s, codes, d0, hb, signs, bias, pr, pc)
162
+ ctx.rows = rows
163
+ ctx.nblk_w = nblk_w
164
+ return y
165
+
166
+ @staticmethod
167
+ def backward(ctx, gy):
168
+ x, s, codes, d0, hb, signs, bias, pr, pc = ctx.saved_tensors
169
+ rows, nblk_w = ctx.rows, ctx.nblk_w
170
+ width = d0.shape[1] * 128
171
+ w = _ScaledWFn.build_w(s, codes, d0, hb, signs, rows, nblk_w)
172
+ if pr is not None:
173
+ w = w[pr]
174
+ if pc is not None:
175
+ w = w[:, pc]
176
+ if x.dtype in (torch.long, torch.int, torch.int32, torch.int64):
177
+ gw = torch.zeros(rows, width, device=gy.device, dtype=gy.dtype)
178
+ gw.index_add_(0, x.reshape(-1), gy.reshape(-1, width))
179
+ gx = None
180
+ else:
181
+ gx = (gy @ w.to(gy.dtype)) if ctx.needs_input_grad[0] else None
182
+ gw = gy.reshape(-1, gy.shape[-1]).T @ x.reshape(-1, x.shape[-1])
183
+ # un-permute the gradient back to stored order (inverse of the forward gathers)
184
+ if pc is not None:
185
+ gw_t = torch.empty_like(gw)
186
+ gw_t[:, pc] = gw
187
+ gw = gw_t
188
+ if pr is not None:
189
+ gw_t = torch.empty_like(gw)
190
+ gw_t[pr] = gw
191
+ gw = gw_t
192
+ if hb is not None:
193
+ gsz = hb.shape[0]
194
+ g1 = ((gw * signs).reshape(rows, width // gsz, gsz) @ hb.T).reshape(rows, width)
195
+ else:
196
+ g1 = gw
197
+ # stored[r,k] = (q-1)[r,k] * (d0*s)[r,b] -> dL/ds[r,b] = sum_k g1[r,k] (q-1)[r,k] d0[r,b]
198
+ qs = unpack(codes).float() - 1.0
199
+ gs = (qs * g1.view(rows, nblk_w, 128).float()).sum(-1) * d0.float()
200
+ gs = gs.unsqueeze(-1).to(s.dtype)
201
+ return gx, gs, None, None, None, None, None, None, None, None, None
202
+
203
+ def untile_perms(name):
204
+ """Row/col permutations reproducing GgufCheckpoint._hf_order for this tensor."""
205
+ if ck._heads.rep == 1 or not name.startswith("blk."):
206
+ return None, None
207
+ kind = name.split(".")[2]
208
+ rows, width = g.tensors[name].shape[1], g.tensors[name].shape[0]
209
+ pr = pc = None
210
+ if kind == "attn_qkv":
211
+ qk = 2 * ck._heads.n_k * ck._heads.k_dim
212
+ idx = (torch.arange(rows - qk) + qk).reshape(rows - qk, 1)
213
+ pr = torch.cat([torch.arange(qk), _untile(idx, 0, ck._heads, ck._heads.v_dim).reshape(-1)])
214
+ elif kind == "attn_gate":
215
+ pr = _untile(torch.arange(rows).reshape(rows, 1), 0, ck._heads, ck._heads.v_dim).reshape(-1)
216
+ elif kind in ("ssm_alpha", "ssm_beta"):
217
+ pr = _untile(torch.arange(rows).reshape(rows, 1), 0, ck._heads, 1).reshape(-1)
218
+ elif kind == "ssm_out":
219
+ folded = rotation(g, name) is not None
220
+ if not (folded and ck._grouped):
221
+ pc = _untile(torch.arange(width).reshape(1, width), 1, ck._heads, ck._heads.v_dim).reshape(-1)
222
+ if pr is not None:
223
+ pr = pr.to(dev)
224
+ if pc is not None:
225
+ pc = pc.to(dev)
226
+ return pr, pc
227
+
228
+ king_path = next(Path(args.gguf).glob("*.gguf"))
229
+ fk = open(king_path, "rb")
230
+ dequant_checked = set()
231
+ for gg_name, hf_key in gguf_to_hf.items():
232
+ mod_name = hf_key.rsplit(".weight", 1)[0]
233
+ try:
234
+ mod = model.get_submodule(mod_name)
235
+ except Exception:
236
+ continue
237
+ rows, width = sd[hf_key].shape # tile() preserves shape: stored-space layout == HF shape
238
+ nblk_w = width // 128
239
+ if nblk_w == 0 or rows == 0:
240
+ continue
241
+ signs = rot_signs(gg_name)
242
+ t = g.tensors[gg_name]
243
+ fk.seek(g.data_start + t.offset)
244
+ raw = np.frombuffer(fk.read(rows * nblk_w * 34), np.uint8).reshape(rows * nblk_w, 34)
245
+ d0 = torch.from_numpy(raw[:, :2].copy().view("<f2")).view(rows, nblk_w).to(dev)
246
+ codes = torch.from_numpy(raw[:, 2:].copy()).view(rows, nblk_w, 32).to(dev)
247
+ # one-time cross-check: direct dequant must match the surrogate transform path
248
+ kind = "rot" if signs is not None else "plain"
249
+ if kind not in dequant_checked:
250
+ dequant_checked.add(kind)
251
+ w_mine = _ScaledWFn.build_w(torch.ones(rows, nblk_w, 1, device=dev), codes, d0,
252
+ None, None, rows, nblk_w)
253
+ w_ref, _ = hf_to_stored(gg_name, sd[hf_key].float().to(dev))
254
+ rel = (w_mine.float() - w_ref.float()).abs().mean().item() / w_ref.float().abs().mean().item()
255
+ print(f"DEQUANT_CHECK[{kind}] {gg_name}: rel_mean_diff={rel:.3e}", flush=True)
256
+ assert rel < 0.02, f"direct dequant mismatch on {gg_name} ({kind}): {rel}"
257
+ s = torch.nn.Parameter(torch.ones(rows, nblk_w, 1, device=dev))
258
+ # hadamard entries and rotation signs are +-1: exact in bf16
259
+ hb = H.to(torch.bfloat16) if signs is not None else None
260
+ if signs is not None:
261
+ signs = signs.to(torch.bfloat16)
262
+ pr, pc = untile_perms(gg_name)
263
+
264
+ def make_fwd(mod=mod, codes=codes, signs=signs, hb=hb, rows=rows, nblk_w=nblk_w,
265
+ s=s, d0=d0, bias=getattr(mod, "bias", None), pr=pr, pc=pc):
266
+ def fwd(x):
267
+ return _ScaledWFn.apply(x, s, codes, d0, hb, signs, bias, rows, nblk_w, pr, pc)
268
+ return fwd
269
+
270
+ mod.forward = make_fwd()
271
+ with torch.no_grad():
272
+ mod.weight = torch.nn.Parameter(torch.empty(0, device=dev), requires_grad=False)
273
+ scale_params[gg_name] = (s, rows, nblk_w)
274
+
275
+ # inner norms trainable (exclude output/final norm: handled by GgufHead from file)
276
+ norm_params = []
277
+ for n_, p_ in model.named_parameters():
278
+ if n_.endswith("norm.weight") and "model.norm" not in n_ and not n_.endswith(".model.norm.weight"):
279
+ p_.requires_grad_(True)
280
+ norm_params.append((n_, p_))
281
+ trained_norm_names = {n_ for n_, _ in norm_params}
282
+ all_p = [v[0] for v in scale_params.values()] + [p_ for _, p_ in norm_params]
283
+ print(f"wrapped {len(scale_params)} linears "
284
+ f"({sum(v[0].numel() for v in scale_params.values()):,} scale params), "
285
+ f"{len(norm_params)} inner norms", flush=True)
286
+
287
+ # ---- heads ----
288
+ print("WRAP_DONE", flush=True)
289
+ teacher_head = FinalHead(Path(args.teacher), dev)
290
+ own_head = GgufHead(Path(args.gguf), dev)
291
+ LAYERS = list(config.LAYERS) # 8..64 step 8
292
+
293
+ # ---- data: teacher states + teacher logprobs cached ----
294
+ records = json.loads(Path(args.samples).read_text())[: args.max_states]
295
+ data = []
296
+ from safetensors.torch import load_file
297
+ for r in records:
298
+ st = load_file(r["states"])["hidden_states"].float() # [rows, 8, hidden]
299
+ data.append({
300
+ "ids": torch.tensor(r["prompt"] + r["answer"], device=dev).unsqueeze(0),
301
+ "n_prompt": len(r["prompt"]),
302
+ "n_answer": len(r["answer"]),
303
+ "teacher_states": st.cpu(),
304
+ })
305
+ if len(data) >= args.max_states:
306
+ break
307
+ print(f"{len(data)} trajectory samples", flush=True)
308
+ import gc
309
+ try:
310
+ del sd
311
+ except NameError:
312
+ pass
313
+ gc.collect()
314
+ torch.cuda.empty_cache()
315
+ print("MEM after setup:", torch.cuda.memory_allocated() // (1 << 20), "MB", flush=True)
316
+ print("DATA_DONE mem:", torch.cuda.memory_allocated() // (1 << 20), "MB", flush=True)
317
+
318
+ opt = torch.optim.Adam(all_p, lr=args.lr)
319
+ sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=args.steps)
320
+ model.train() # gradient checkpointing (enabled above) only activates in training mode
321
+ step = 0
322
+ while step < args.steps:
323
+ for d in data:
324
+ ids = d["ids"]
325
+ out = model.model(ids, output_hidden_states=True, use_cache=False) # skip lm_head + cache: loss uses hidden states only
326
+ hs = out.hidden_states # tuple: [emb, L1..L64, post-norm]
327
+ n_ans = d["n_answer"]
328
+ start = d["n_prompt"] - 1 # row0 = last prompt token
329
+ rows = slice(start, start + n_ans)
330
+
331
+ own_last = torch.stack([hs[L][0, start:start + n_ans] for L in LAYERS], 1) # [n,8,h]
332
+ t_st = d["teacher_states"].to(dev)
333
+ # states files hold rows 0..start+n_ans-1 of the sequence; the eval window starts at
334
+ # the last prompt row (metrics.py compare: "rows that read or predict answer tokens")
335
+ t_win = t_st[start:start + n_ans]
336
+
337
+ # KL from last captured layer via own head
338
+ ref_last = t_win[:, -1].float()
339
+ kl = torch.zeros((), device=dev)
340
+ CH = 256
341
+ for c0 in range(0, ref_last.shape[0], CH):
342
+ own_lp_c = own_head.logprobs(own_last[c0:c0 + CH, -1].float())
343
+ with torch.no_grad():
344
+ ref_lp_c = teacher_head.logprobs(ref_last[c0:c0 + CH])
345
+ p_c = ref_lp_c.exp()
346
+ kl = kl + (p_c * (ref_lp_c - own_lp_c)).sum(-1).sum()
347
+ del ref_lp_c, own_lp_c, p_c
348
+ kl = kl / ref_last.shape[0]
349
+
350
+ # dist over captured layers, rows 1.. (answer token rows; matches metrics.py)
351
+ a = t_win[1:].float()
352
+ b = own_last[1:].float()
353
+ dist = ((b - a).norm(dim=-1) / (a.norm(dim=-1) + 1e-9)).mean()
354
+
355
+ loss = kl + args.dist_weight * dist
356
+ opt.zero_grad(set_to_none=True)
357
+ loss.backward()
358
+ torch.nn.utils.clip_grad_norm_(all_p, 1.0)
359
+ opt.step()
360
+ step += 1
361
+ sched.step()
362
+ if step % 20 == 0:
363
+ print(f"step {step}: KL {kl.item():.5f} dist {dist.item():.5f}", flush=True)
364
+ if step >= args.steps:
365
+ break
366
+
367
+ # ---- export ----
368
+ import shutil
369
+ king = next(Path(args.gguf).glob("*.gguf"))
370
+ out_dir = Path(args.out)
371
+ out_dir.mkdir(parents=True, exist_ok=True)
372
+ dst = out_dir / king.name
373
+ shutil.copy(king, dst)
374
+ g_out = read_header(dst)
375
+ with open(dst, "r+b") as fh, open(king, "rb") as fk:
376
+ for gg_name, (s, rows, nblk_w) in scale_params.items():
377
+ t = g_out.tensors[gg_name]
378
+ nblk_flat = int(np.prod(t.shape)) // 128
379
+ fk.seek(g_out.data_start + t.offset)
380
+ blocks = np.frombuffer(fk.read(nblk_flat * 34), np.uint8).reshape(-1, 34).copy()
381
+ d0 = blocks[:, :2].copy().view("<f2").astype(np.float32).reshape(rows, nblk_w)
382
+ s_np = s.detach().cpu().numpy().reshape(rows, nblk_w)
383
+ blocks[:, :2] = (d0 * s_np).astype(np.float16).astype("<f2").view(np.uint8).reshape(-1, 2)
384
+ fh.seek(g_out.data_start + t.offset)
385
+ fh.write(blocks.tobytes())
386
+ named = dict(model.named_parameters())
387
+ n_norm = 0
388
+ for gg_name, t in g_out.tensors.items():
389
+ if not gg_name.endswith("norm.weight") or gg_name == "output_norm.weight":
390
+ continue
391
+ parts = gg_name.split(".")
392
+ suffix = (f"layers.{parts[1]}.{parts[2]}.weight" if gg_name.startswith("blk.")
393
+ else None)
394
+ if suffix is None:
395
+ continue
396
+ cand = [n_ for n_ in named if n_.endswith(suffix) and n_ in trained_norm_names]
397
+ if not cand:
398
+ continue
399
+ v = named[cand[0]].detach().float().cpu().numpy() + 1.0
400
+ if v.size != int(np.prod(t.shape)):
401
+ continue
402
+ fh.seek(g_out.data_start + t.offset)
403
+ fh.write(v.astype("<f4").tobytes())
404
+ n_norm += 1
405
+ print(f"exported scales + {n_norm} norms -> {dst}")
406
+
407
+
408
+ if __name__ == "__main__":
409
+ main()