betterwithage commited on
Commit
f4f20d9
·
verified ·
1 Parent(s): f14c6a3

feat(train): abstain retrain to KHIPU-R2, keep ATELIER card

Browse files
Files changed (1) hide show
  1. train_khipu_abstain.py +656 -0
train_khipu_abstain.py ADDED
@@ -0,0 +1,656 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = [
5
+ # "unsloth",
6
+ # "trl>=0.12.0",
7
+ # "peft>=0.7.0",
8
+ # "datasets",
9
+ # "transformers",
10
+ # "huggingface_hub",
11
+ # "trackio",
12
+ # "jsonschema",
13
+ # ]
14
+ # ///
15
+ """SZL-Khipu-1.5B abstain retrain — Hugging Face Jobs UV script.
16
+
17
+ Existing Khipu line (Qwen2.5-1.5B), NOT the Chaski Qwen3.5 lock.
18
+ Does NOT overwrite SZLHOLDINGS/SZL-Khipu-1.5B signed weights.
19
+
20
+ Recipe from khipu/train_khipu.py + receiptagent knobs:
21
+ Unsloth QLoRA, seed 11, lr 2e-4, adamw_8bit, train_on_responses_only, Trackio.
22
+ ABSTAIN_OVERSAMPLE raised 2 -> 4 (8*4=32 abstain vs 15 navigate = 47 in-memory rows).
23
+ Held-out eval.jsonl (5 navigate) + adversarial.jsonl (6 abstain) NEVER enter gradients.
24
+
25
+ After train: in-process port of eval_khipu.py scoring. Write MEASURED k/n only.
26
+ No fabricated evals. publication_eligible stays false until that eval actually runs.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import glob
31
+ import hashlib
32
+ import json
33
+ import os
34
+ import platform
35
+ import re
36
+ import shutil
37
+ import urllib.request
38
+ from datetime import datetime, timezone
39
+
40
+ from datasets import Dataset
41
+ from huggingface_hub import HfApi, hf_hub_download
42
+ from jsonschema.validators import validator_for
43
+ from unsloth import FastLanguageModel
44
+ from unsloth.chat_templates import train_on_responses_only
45
+ from trl import SFTConfig, SFTTrainer
46
+
47
+ # Canonical Hugging Face id — MUST stay Qwen2.5-1.5B-Instruct (ATELIER).
48
+ BASE_TRAIN = "unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit"
49
+ BASE_CANONICAL = "Qwen/Qwen2.5-1.5B-Instruct"
50
+ HUB = os.environ.get("HUB_MODEL_ID", "SZLHOLDINGS/KHIPU-R2")
51
+ # NEVER the original signed-weights repo.
52
+ FORBIDDEN_HUB = "SZLHOLDINGS/SZL-Khipu-1.5B"
53
+ MAX_SEQ_LEN = 2048
54
+ SEED = 11
55
+ LORA_R = 32
56
+ LORA_ALPHA = 64
57
+ LR = 2e-4
58
+ NUM_EPOCHS = 45
59
+ ABSTAIN_OVERSAMPLE = 4 # was 2 in train_khipu.py (16+15=31); now 32+15=47
60
+
61
+ CURRICULUM_FILES = [
62
+ "train.jsonl",
63
+ "eval.jsonl",
64
+ "train.abstain.jsonl",
65
+ "adversarial.jsonl",
66
+ "khipu.schema.json",
67
+ ]
68
+ TRAIN_FILES = ["train.jsonl", "train.abstain.jsonl"]
69
+ EVAL_NAVIGATE = "eval.jsonl"
70
+ EVAL_ADVERSARIAL = "adversarial.jsonl"
71
+ GH_RAW = "https://raw.githubusercontent.com/szl-holdings/szl-forge/main/khipu"
72
+
73
+ if HUB == FORBIDDEN_HUB:
74
+ raise SystemExit(f"[khipu-abstain] refusing to push to {FORBIDDEN_HUB}")
75
+
76
+
77
+ def sha256_file(path: str) -> str:
78
+ h = hashlib.sha256()
79
+ with open(path, "rb") as f:
80
+ for chunk in iter(lambda: f.read(1 << 20), b""):
81
+ h.update(chunk)
82
+ return h.hexdigest()
83
+
84
+
85
+ def sha256_safetensors_dir(directory: str) -> str:
86
+ files = sorted(glob.glob(os.path.join(directory, "*.safetensors")))
87
+ if not files:
88
+ return ""
89
+ h = hashlib.sha256()
90
+ for path in files:
91
+ h.update(os.path.basename(path).encode("utf-8"))
92
+ with open(path, "rb") as f:
93
+ for chunk in iter(lambda: f.read(1 << 20), b""):
94
+ h.update(chunk)
95
+ return h.hexdigest()
96
+
97
+
98
+ def fetch_curriculum() -> dict:
99
+ """Pull committed curriculum (Hub copies first, GitHub canonical fallback).
100
+ Cross-check sha256 against manifest.json. Held-out files are fetched too
101
+ so eval can run; they are never loaded into the train multiset.
102
+ """
103
+ names = CURRICULUM_FILES + ["manifest.json"]
104
+ for name in names:
105
+ got = False
106
+ try:
107
+ cached = hf_hub_download(repo_id=HUB, filename=name, repo_type="model")
108
+ if os.path.abspath(cached) != os.path.abspath(name):
109
+ shutil.copy(cached, name)
110
+ got = True
111
+ print(f"[khipu-abstain] fetched {name} from hub {HUB}")
112
+ except Exception as exc:
113
+ print(f"[khipu-abstain] hub miss {name}: {type(exc).__name__}: {exc}")
114
+ if not got:
115
+ url = f"{GH_RAW}/{name}"
116
+ urllib.request.urlretrieve(url, name)
117
+ print(f"[khipu-abstain] fetched {name} from github")
118
+ with open("manifest.json", "r", encoding="utf-8") as f:
119
+ manifest = json.load(f)
120
+ datasets = {}
121
+ for name in CURRICULUM_FILES:
122
+ digest = sha256_file(name)
123
+ declared = manifest.get("files", {}).get(name, {}).get("sha256")
124
+ if declared != digest:
125
+ raise SystemExit(
126
+ f"[khipu-abstain] {name} sha256 {digest} != manifest {declared}"
127
+ )
128
+ datasets[name] = digest
129
+ if name.endswith(".jsonl"):
130
+ n = sum(1 for line in open(name, encoding="utf-8") if line.strip())
131
+ print(f"[khipu-abstain] {name}: {n} rows sha256={digest}")
132
+ return {"manifest": manifest, "datasets": datasets}
133
+
134
+
135
+ def load_jsonl(name: str):
136
+ rows = []
137
+ with open(name, "r", encoding="utf-8") as f:
138
+ for line in f:
139
+ line = line.strip()
140
+ if line:
141
+ rows.append(json.loads(line))
142
+ return rows
143
+
144
+
145
+ def load_train_rows(tokenizer):
146
+ rows = []
147
+ for name in TRAIN_FILES:
148
+ reps = ABSTAIN_OVERSAMPLE if name == "train.abstain.jsonl" else 1
149
+ file_rows = load_jsonl(name)
150
+ for _ in range(reps):
151
+ rows.extend(file_rows)
152
+ print(f"[khipu-abstain] {name}: {len(file_rows)} rows x{reps}")
153
+ print(
154
+ f"[khipu-abstain] {len(rows)} training rows total "
155
+ f"(abstain oversampled x{ABSTAIN_OVERSAMPLE}; held-out never in gradients)"
156
+ )
157
+ return [
158
+ tokenizer.apply_chat_template(
159
+ r["messages"], tokenize=False, add_generation_prompt=False
160
+ )
161
+ for r in rows
162
+ ]
163
+
164
+
165
+ def extract_json(text: str):
166
+ text = (text or "").strip()
167
+ if text.startswith("```"):
168
+ text = re.sub(r"^```(?:json)?\s*", "", text)
169
+ text = re.sub(r"\s*```$", "", text)
170
+ try:
171
+ return json.loads(text)
172
+ except Exception:
173
+ pass
174
+ start = text.find("{")
175
+ end = text.rfind("}")
176
+ if start >= 0 and end > start:
177
+ try:
178
+ return json.loads(text[start : end + 1])
179
+ except Exception:
180
+ return None
181
+ return None
182
+
183
+
184
+ def offered_ids(row) -> set:
185
+ user = next(m for m in row["messages"] if m["role"] == "user")
186
+ payload = json.loads(user["content"])
187
+ return {c["nodeId"] for c in payload.get("candidates", [])}
188
+
189
+
190
+ def reference_cited(row) -> set:
191
+ return set(json.loads(row["messages"][-1]["content"]).get("citedNodeIds") or [])
192
+
193
+
194
+ def prompt_messages(row):
195
+ return [m for m in row["messages"] if m["role"] in ("system", "user")]
196
+
197
+
198
+ def cross_field_ok(plan: dict, offered: set) -> bool:
199
+ """Mirror eval_khipu.py cross_field_ok / KhipuNavPlanSchema.superRefine."""
200
+ steps = plan.get("steps") or []
201
+ cited = plan.get("citedNodeIds") or []
202
+ decision = plan.get("decision")
203
+ abstain_reason = plan.get("abstainReason", None)
204
+ plan_cand_ids = [c.get("nodeId") for c in (plan.get("candidates") or [])]
205
+ plan_cand_set = set(plan_cand_ids)
206
+ if any(cid not in offered for cid in plan_cand_ids):
207
+ return False
208
+ if any(s.get("nodeId") not in plan_cand_set for s in steps):
209
+ return False
210
+ if any(cid not in plan_cand_set for cid in cited):
211
+ return False
212
+ cite_steps = {s.get("nodeId") for s in steps if s.get("action") == "CITE"}
213
+ if cite_steps != set(cited):
214
+ return False
215
+ if decision == "ABSTAIN":
216
+ return len(cited) == 0 and bool(abstain_reason)
217
+ if decision == "NAVIGATE":
218
+ return len(cited) >= 1 and abstain_reason is None
219
+ return False
220
+
221
+
222
+ def run_held_out_eval(model, tokenizer, schema) -> dict:
223
+ """In-process port of eval_khipu.py. MEASURED integer counts only.
224
+
225
+ eval.jsonl (5 navigate) + adversarial.jsonl (6 abstain). Temperature 0.
226
+ Held-out files were never in the training multiset.
227
+ """
228
+ FastLanguageModel.for_inference(model)
229
+ validator = validator_for(schema)(schema)
230
+ navigate = load_jsonl(EVAL_NAVIGATE)
231
+ adversarial = load_jsonl(EVAL_ADVERSARIAL)
232
+
233
+ plan_total = len(navigate) + len(adversarial)
234
+ plan_valid = 0
235
+ hallucinated_citation_count = 0
236
+ per_row = []
237
+
238
+ def generate_plan(row):
239
+ msgs = prompt_messages(row)
240
+ prompt = tokenizer.apply_chat_template(
241
+ msgs, tokenize=False, add_generation_prompt=True
242
+ )
243
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
244
+ out = model.generate(
245
+ **inputs,
246
+ max_new_tokens=1024,
247
+ do_sample=False,
248
+ use_cache=True,
249
+ )
250
+ n_in = inputs["input_ids"].shape[-1]
251
+ return tokenizer.decode(out[0][n_in:], skip_special_tokens=True)
252
+
253
+ def score(row, tag: str, i: int, n: int) -> dict:
254
+ nonlocal plan_valid, hallucinated_citation_count
255
+ offered = offered_ids(row)
256
+ raw = generate_plan(row)
257
+ plan = extract_json(raw)
258
+ valid = False
259
+ if isinstance(plan, dict):
260
+ try:
261
+ validator.validate(plan)
262
+ valid = cross_field_ok(plan, offered)
263
+ except Exception:
264
+ valid = False
265
+ if valid:
266
+ plan_valid += 1
267
+ if isinstance(plan, dict):
268
+ for cid in plan.get("citedNodeIds") or []:
269
+ if cid not in offered:
270
+ hallucinated_citation_count += 1
271
+ rec = {
272
+ "split": tag,
273
+ "i": i,
274
+ "valid": bool(valid),
275
+ "decision": (plan or {}).get("decision") if isinstance(plan, dict) else None,
276
+ "citedNodeIds": (plan or {}).get("citedNodeIds") if isinstance(plan, dict) else None,
277
+ }
278
+ per_row.append(rec)
279
+ print(f"[eval] {tag} {i}/{n} valid={valid} decision={rec['decision']}")
280
+ return {"plan": plan if valid else (plan if isinstance(plan, dict) else None),
281
+ "offered": offered, "valid": valid, "raw": raw}
282
+
283
+ grounding_total = len(navigate)
284
+ grounding_correct = 0
285
+ for i, row in enumerate(navigate, 1):
286
+ res = score(row, "navigate", i, grounding_total)
287
+ plan = res["plan"]
288
+ ok_route = (
289
+ bool(res.get("valid"))
290
+ and isinstance(plan, dict)
291
+ and plan.get("decision") == "NAVIGATE"
292
+ and set(plan.get("citedNodeIds") or []) == reference_cited(row)
293
+ )
294
+ if ok_route:
295
+ grounding_correct += 1
296
+ print(f"[eval] navigate {i}/{grounding_total} routed-correctly={ok_route}")
297
+
298
+ abstain_total = len(adversarial)
299
+ abstain_correct = 0
300
+ for i, row in enumerate(adversarial, 1):
301
+ res = score(row, "adversarial", i, abstain_total)
302
+ plan = res["plan"]
303
+ ok_abstain = (
304
+ bool(res.get("valid"))
305
+ and isinstance(plan, dict)
306
+ and plan.get("decision") == "ABSTAIN"
307
+ )
308
+ if ok_abstain:
309
+ abstain_correct += 1
310
+ print(f"[eval] adversarial {i}/{abstain_total} abstained={ok_abstain}")
311
+
312
+ print(
313
+ f"[eval] MEASURED plan-valid {plan_valid}/{plan_total} | "
314
+ f"routing {grounding_correct}/{grounding_total} | "
315
+ f"abstain {abstain_correct}/{abstain_total} | "
316
+ f"hallucinated-citations {hallucinated_citation_count}"
317
+ )
318
+ return {
319
+ "label": "MEASURED",
320
+ "host": platform.node() or "unknown-host",
321
+ "evaluatedAt": datetime.now(timezone.utc).isoformat(),
322
+ "planTotal": plan_total,
323
+ "planValid": plan_valid,
324
+ "groundingTotal": grounding_total,
325
+ "groundingCorrect": grounding_correct,
326
+ "abstainTotal": abstain_total,
327
+ "abstainCorrect": abstain_correct,
328
+ "hallucinatedCitationCount": hallucinated_citation_count,
329
+ "held_out_in_gradients": False,
330
+ "temperature": 0,
331
+ "method": "in-process Unsloth generate; scoring ported from eval_khipu.py",
332
+ "rows": per_row,
333
+ }
334
+
335
+
336
+ def write_readme(eval_block: dict | None, loss: float, adapter_sha: str) -> str:
337
+ eval_ran = bool(eval_block) and eval_block.get("label") == "MEASURED"
338
+ if eval_ran:
339
+ eval_md = (
340
+ f"**Status: MEASURED this job** (in-process port of `eval_khipu.py`, "
341
+ f"temperature 0, held-out never in gradients).\n\n"
342
+ f"| split | k/n |\n|---|---|\n"
343
+ f"| plan-valid | {eval_block['planValid']} / {eval_block['planTotal']} |\n"
344
+ f"| grounding (eval.jsonl navigate) | {eval_block['groundingCorrect']} / {eval_block['groundingTotal']} |\n"
345
+ f"| abstain (adversarial.jsonl) | {eval_block['abstainCorrect']} / {eval_block['abstainTotal']} |\n"
346
+ f"| hallucinated citations | {eval_block['hallucinatedCitationCount']} |\n\n"
347
+ f"Prior published original (`SZLHOLDINGS/SZL-Khipu-1.5B`) MEASURED abstain was **2/6** (blocker). "
348
+ f"This repo does not overwrite those signed weights. Counts above are this run only. "
349
+ f"Do not derive a leaderboard score from k/n on n=11."
350
+ )
351
+ else:
352
+ eval_md = (
353
+ "**Status: NOT YET RUN this job.** No fabricated k/n. "
354
+ "publication_eligible remains false until the held-out eval actually executes. "
355
+ "Prior original MEASURED abstain is 2/6 (blocker) on `SZLHOLDINGS/SZL-Khipu-1.5B`."
356
+ )
357
+ loss_s = f"{loss:.4f}" if loss == loss else "UNKNOWN"
358
+ return f"""---
359
+ license: apache-2.0
360
+ language:
361
+ - en
362
+ base_model: Qwen/Qwen2.5-1.5B-Instruct
363
+ base_model_relation: adapter
364
+ library_name: peft
365
+ pipeline_tag: text-generation
366
+ tags:
367
+ - qlora
368
+ - peft
369
+ - governed-agent
370
+ - retrieval
371
+ - brain-navigator
372
+ - grounded-only
373
+ - proposal-only
374
+ - research-only
375
+ - szl-holdings
376
+ - khipu
377
+ - abstain-retrain
378
+ szl:
379
+ doctrine: v11-LOCKED
380
+ lean: "749/14/163"
381
+ lambda: "Conjecture 1 — advisory, never a theorem"
382
+ artifact_class: ADAPTER
383
+ publication_eligible: {str(eval_ran).lower()}
384
+ autonomy_eligible: false
385
+ original_signed_weights: SZLHOLDINGS/SZL-Khipu-1.5B
386
+ ---
387
+
388
+ # SZL-Khipu-1.5B-abstain
389
+
390
+ QLoRA **adapter** retrain of the existing Khipu line to raise in-memory abstain
391
+ oversample (ABSTAIN_OVERSAMPLE=4 → 32 abstain vs 15 navigate). Proposal-only.
392
+ Λ = Conjecture 1. Doctrine v11 LOCKED 749/14/163.
393
+
394
+ | | |
395
+ |---|---|
396
+ | **Base (canonical)** | [`Qwen/Qwen2.5-1.5B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct) |
397
+ | **Runtime train** | `unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit` (same Qwen2.5-1.5B weights, 4-bit) |
398
+ | **Relation** | `adapter` (PEFT / Unsloth QLoRA) |
399
+ | **License** | Apache-2.0 |
400
+ | **Does NOT overwrite** | [`SZLHOLDINGS/SZL-Khipu-1.5B`](https://huggingface.co/SZLHOLDINGS/SZL-Khipu-1.5B) signed weights |
401
+ | **This is NOT** | the Chaski Qwen3.5 lock |
402
+
403
+ ## Evaluation
404
+
405
+ {eval_md}
406
+
407
+ ## Training
408
+
409
+ - Unsloth QLoRA, seed {SEED}, lr {LR}, adamw_8bit, `train_on_responses_only`, Trackio
410
+ - LoRA r={LORA_R} α={LORA_ALPHA}, epochs={NUM_EPOCHS}, ga=2, batch=1, constant_with_warmup
411
+ - ABSTAIN_OVERSAMPLE={ABSTAIN_OVERSAMPLE} (in-memory only; committed files unchanged)
412
+ - Train files: `train.jsonl` (15 navigate) + `train.abstain.jsonl` (8 rows × 4)
413
+ - Held-out: `eval.jsonl` (5) + `adversarial.jsonl` (6) — never in gradients
414
+ - finalTrainLoss (REPORTED string): `{loss_s}`
415
+ - adapter sha256 (safetensors bytes this job): `{adapter_sha or "UNAVAILABLE"}`
416
+
417
+ ## Intended use
418
+
419
+ Supply a query + candidate Brain node **handles**. The adapter proposes a JSON
420
+ plan (`NAVIGATE` or `ABSTAIN`) per `khipu.schema.json`. A controller outside
421
+ the weights validates and resolves content. **Proposal-only. Not autonomous.**
422
+
423
+ ```python
424
+ from peft import PeftModel
425
+ from transformers import AutoModelForCausalLM, AutoTokenizer
426
+
427
+ base_id = "Qwen/Qwen2.5-1.5B-Instruct"
428
+ tok = AutoTokenizer.from_pretrained(base_id)
429
+ base = AutoModelForCausalLM.from_pretrained(base_id, torch_dtype="auto", device_map="auto")
430
+ model = PeftModel.from_pretrained(base, "SZLHOLDINGS/SZL-Khipu-1.5B-abstain")
431
+ ```
432
+
433
+ ## Limitations
434
+
435
+ - Synthetic routing-policy harness, not live-Brain navigation skill.
436
+ - Small denominators (5 navigate / 6 abstain held-out).
437
+ - Original line's MEASURED abstain 2/6 remains a documented blocker on the
438
+ signed-weight repo; this adapter is a separate experiment.
439
+ """
440
+
441
+
442
+ def main() -> None:
443
+ job_id = os.environ.get("JOB_ID", "")
444
+ print(
445
+ f"[khipu-abstain] base_train={BASE_TRAIN} canonical={BASE_CANONICAL} "
446
+ f"hub={HUB} seed={SEED} oversample={ABSTAIN_OVERSAMPLE} job={job_id}"
447
+ )
448
+ pin = fetch_curriculum()
449
+ contract = pin["manifest"]["contract"]
450
+
451
+ print(f"[khipu-abstain] loading base: {BASE_TRAIN}")
452
+ model, tokenizer = FastLanguageModel.from_pretrained(
453
+ model_name=BASE_TRAIN,
454
+ max_seq_length=MAX_SEQ_LEN,
455
+ load_in_4bit=True,
456
+ )
457
+ model = FastLanguageModel.get_peft_model(
458
+ model,
459
+ r=LORA_R,
460
+ lora_alpha=LORA_ALPHA,
461
+ lora_dropout=0,
462
+ target_modules=[
463
+ "q_proj", "k_proj", "v_proj", "o_proj",
464
+ "gate_proj", "up_proj", "down_proj",
465
+ ],
466
+ use_gradient_checkpointing="unsloth",
467
+ random_state=SEED,
468
+ )
469
+
470
+ texts = load_train_rows(tokenizer)
471
+ dataset = Dataset.from_dict({"text": texts})
472
+
473
+ sft_kwargs = dict(
474
+ per_device_train_batch_size=1,
475
+ gradient_accumulation_steps=2,
476
+ num_train_epochs=NUM_EPOCHS,
477
+ learning_rate=LR,
478
+ warmup_steps=10,
479
+ logging_steps=1,
480
+ optim="adamw_8bit",
481
+ weight_decay=0.01,
482
+ lr_scheduler_type="constant_with_warmup",
483
+ seed=SEED,
484
+ output_dir="outputs",
485
+ report_to="trackio",
486
+ run_name="khipu-abstain-oversample4",
487
+ save_strategy="no",
488
+ push_to_hub=False,
489
+ )
490
+ try:
491
+ args = SFTConfig(**sft_kwargs, project="szl-khipu-abstain")
492
+ except TypeError:
493
+ args = SFTConfig(**sft_kwargs)
494
+
495
+ trainer = SFTTrainer(
496
+ model=model,
497
+ tokenizer=tokenizer,
498
+ train_dataset=dataset,
499
+ dataset_text_field="text",
500
+ max_seq_length=MAX_SEQ_LEN,
501
+ args=args,
502
+ )
503
+ try:
504
+ trainer = train_on_responses_only(
505
+ trainer,
506
+ instruction_part="<|im_start|>user\n",
507
+ response_part="<|im_start|>assistant\n",
508
+ tokenizer=tokenizer,
509
+ )
510
+ except TypeError:
511
+ trainer = train_on_responses_only(
512
+ trainer,
513
+ instruction_part="<|im_start|>user\n",
514
+ response_part="<|im_start|>assistant\n",
515
+ )
516
+
517
+ print("[khipu-abstain] training...")
518
+ stats = trainer.train()
519
+ loss = float(getattr(stats, "training_loss", float("nan")))
520
+ final_loss = f"{loss:.4f}" if loss == loss else "UNKNOWN"
521
+ print(f"[khipu-abstain] final loss (REPORTED verbatim): {final_loss}")
522
+
523
+ adapter_dir = "khipu-abstain-adapter"
524
+ os.makedirs(adapter_dir, exist_ok=True)
525
+ model.save_pretrained(adapter_dir)
526
+ tokenizer.save_pretrained(adapter_dir)
527
+ adapter_sha = sha256_safetensors_dir(adapter_dir)
528
+ print(f"[khipu-abstain] adapter sha256={adapter_sha}")
529
+
530
+ eval_block = None
531
+ eval_error = None
532
+ try:
533
+ with open("khipu.schema.json", "r", encoding="utf-8") as f:
534
+ schema = json.load(f)
535
+ eval_block = run_held_out_eval(model, tokenizer, schema)
536
+ except Exception as exc:
537
+ eval_error = f"{type(exc).__name__}: {exc}"
538
+ print(f"[khipu-abstain] EVAL FAILED (not fabricating scores): {eval_error}")
539
+
540
+ eval_ran = bool(eval_block) and eval_block.get("label") == "MEASURED"
541
+ receipt = {
542
+ "kind": "szl-khipu-abstain-training-receipt",
543
+ "schema": "szl.frontier-training-run/v1",
544
+ "v": 1,
545
+ "capabilityProfile": "SZL-Khipu-1.5B-BrainNavigator",
546
+ "artifact": HUB,
547
+ "baseModel": BASE_CANONICAL,
548
+ "base_model": BASE_CANONICAL,
549
+ "base_model_relation": "adapter",
550
+ "base_model_runtime": BASE_TRAIN,
551
+ "does_not_overwrite": FORBIDDEN_HUB,
552
+ "datasets": pin["datasets"],
553
+ "schemaFingerprintSha256": contract["schemaFingerprintSha256"],
554
+ "outputSchemaSha256": contract["outputSchemaSha256"],
555
+ "adapterSha256": adapter_sha,
556
+ "ABSTAIN_OVERSAMPLE": ABSTAIN_OVERSAMPLE,
557
+ "train_navigate_rows": 15,
558
+ "train_abstain_rows_committed": 8,
559
+ "train_abstain_rows_in_memory": 8 * ABSTAIN_OVERSAMPLE,
560
+ "training_rows_in_memory": 15 + 8 * ABSTAIN_OVERSAMPLE,
561
+ "held_out_in_gradients": False,
562
+ "held_out": {"eval.jsonl": 5, "adversarial.jsonl": 6},
563
+ "seed": SEED,
564
+ "num_train_epochs": NUM_EPOCHS,
565
+ "warmup_steps": 10,
566
+ "lora_r": LORA_R,
567
+ "lora_alpha": LORA_ALPHA,
568
+ "learning_rate": LR,
569
+ "lr_scheduler_type": "constant_with_warmup",
570
+ "optim": "adamw_8bit",
571
+ "response_only_loss": True,
572
+ "trackio": True,
573
+ "finalTrainLoss": final_loss,
574
+ "training_loss": loss if loss == loss else None,
575
+ "label": "MEASURED" if loss == loss else "UNKNOWN",
576
+ "eval": eval_block if eval_ran else {
577
+ "label": "UNAVAILABLE",
578
+ "reason": eval_error or "eval did not run",
579
+ },
580
+ "lambda": "Conjecture 1",
581
+ "doctrine": "v11 LOCKED 749/14/163",
582
+ "proposal_only": True,
583
+ "publication_eligible": bool(eval_ran),
584
+ "autonomy_eligible": False,
585
+ "job_id": job_id,
586
+ "host": platform.node() or "unknown-host",
587
+ "computed_at": datetime.now(timezone.utc).isoformat(),
588
+ "claim_boundary": (
589
+ "Eval counts are MEASURED k/n from this job only when eval.label=MEASURED. "
590
+ "Do not invent scores. Original SZL-Khipu-1.5B signed abstain 2/6 is unchanged."
591
+ ),
592
+ }
593
+ with open("training_receipt.json", "w", encoding="utf-8") as f:
594
+ json.dump(receipt, f, indent=2)
595
+ f.write("\n")
596
+ if eval_ran:
597
+ with open("eval_measured.json", "w", encoding="utf-8") as f:
598
+ json.dump(eval_block, f, indent=2)
599
+ f.write("\n")
600
+ readme = write_readme(eval_block if eval_ran else None, loss, adapter_sha)
601
+ with open("README.md", "w", encoding="utf-8") as f:
602
+ f.write(readme)
603
+
604
+ api = HfApi()
605
+ api.upload_folder(
606
+ folder_path=adapter_dir,
607
+ repo_id=HUB,
608
+ repo_type="model",
609
+ commit_message="feat(adapter): Unsloth QLoRA ABSTAIN_OVERSAMPLE=4 (does not overwrite SZL-Khipu-1.5B)",
610
+ ignore_patterns=["*.tmp"],
611
+ )
612
+ api.upload_file(
613
+ path_or_fileobj="training_receipt.json",
614
+ path_in_repo="training_receipt.json",
615
+ repo_id=HUB,
616
+ repo_type="model",
617
+ commit_message="chore(receipt): Khipu abstain training receipt",
618
+ )
619
+ if eval_ran:
620
+ api.upload_file(
621
+ path_or_fileobj="eval_measured.json",
622
+ path_in_repo="eval_measured.json",
623
+ repo_id=HUB,
624
+ repo_type="model",
625
+ commit_message="chore(eval): MEASURED k/n held-out (no fabricated scores)",
626
+ )
627
+ if HUB != "SZLHOLDINGS/KHIPU-R2":
628
+ api.upload_file(
629
+ path_or_fileobj="README.md",
630
+ path_in_repo="README.md",
631
+ repo_id=HUB,
632
+ repo_type="model",
633
+ commit_message="docs(card): adapter card base_model Qwen2.5-1.5B-Instruct",
634
+ )
635
+ else:
636
+ api.upload_file(
637
+ path_or_fileobj="README.md",
638
+ path_in_repo="training_card_generated.md",
639
+ repo_id=HUB,
640
+ repo_type="model",
641
+ commit_message="docs: generated training card (does not replace ATELIER README)",
642
+ )
643
+ print("[khipu-abstain] DONE. adapter+receipt pushed to", HUB)
644
+ if eval_ran:
645
+ e = eval_block
646
+ print(
647
+ f"[khipu-abstain] MEASURED abstain {e['abstainCorrect']}/{e['abstainTotal']} "
648
+ f"grounding {e['groundingCorrect']}/{e['groundingTotal']} "
649
+ f"plan-valid {e['planValid']}/{e['planTotal']}"
650
+ )
651
+ else:
652
+ print("[khipu-abstain] eval UNAVAILABLE — not fabricating scores")
653
+
654
+
655
+ if __name__ == "__main__":
656
+ main()