File size: 12,165 Bytes
97f0182
4addb8c
 
 
97f0182
4addb8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97f0182
 
 
 
 
4addb8c
97f0182
 
4addb8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97f0182
 
 
 
 
 
 
 
 
 
4addb8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97f0182
 
4addb8c
 
 
 
97f0182
 
4addb8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97f0182
 
 
 
 
4addb8c
 
 
 
97f0182
 
 
4addb8c
 
 
97f0182
 
 
4addb8c
 
 
 
97f0182
4addb8c
 
 
 
 
 
 
 
 
 
 
 
 
 
97f0182
4addb8c
 
 
 
 
 
 
97f0182
 
4addb8c
97f0182
 
 
 
 
4addb8c
 
 
 
 
97f0182
 
 
4addb8c
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
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 SZL Holdings
"""Receipted Unsloth β€” take the speed, bind the knobs, refuse the folklore.

Unsloth TAKE (HUB, not MEASURED here):
  4-bit QLoRA Β· fused QK-RoPE / SwiGLU Triton kernels Β· adamw_8bit
  use_gradient_checkpointing="unsloth" Β· lora_dropout=0 Β· bias="none"
  auto padding-free packing (1.1–2Γ—, ~30% VRAM, loss comparable)

Unsloth LEAVE:
  packing=True              β€” Unsloth's own docs: changes the loss scale
  LoftQ                     β€” start-of-run VRAM spike; not worth it on 0.5B–1.5B
  MoE Split-LoRA            β€” these organs are dense Qwen, not gpt-oss / Qwen3-MoE
  GGUF as the signed object β€” derived. Always.
  seed 3407                 β€” house seed is 20260721
  invented joules / 3Γ— as MEASURED β€” their blog, not our receipt

SZL CUT (unique per organ, this file):
  willay        r=8  rsLoRA  attn+mlp   packing=false  short ctx   doctrine mouth
  chaski        r=8  rsLoRA  attn-only  packing=auto               courier cannot author
  chaski-5050   r=16 rsLoRA  attn+mlp   packing=auto               mix is the identity
  chaski-r2     r=8  rsLoRA  attn-only  packing=false  lr=5e-5     R2 refinement; R1 stays
  khipu         r=16         attn+mlp   packing=auto   4bit        navigator, loss-comparable
  receiptagent  r=16         attn+mlp   packing=false              receipt-first SFT
  khipu-r2      r=16         attn+mlp   packing=false              lineage, not overwrite

Every knob lands in training_receipt.json BEFORE merge.
Sign that envelope. Then merge. GGUF is derived.

  python receipted_unsloth.py --profile willay --receipt-only
  python receipted_unsloth.py --profile chaski --data doctrine.jsonl --out out/chaski
"""
from __future__ import annotations

import argparse
import hashlib
import json
import sys
import time
from pathlib import Path
from typing import Any

SEED = 20260721
ATTN = ("q_proj", "k_proj", "v_proj", "o_proj")
ATTN_MLP = ATTN + ("gate_proj", "up_proj", "down_proj")

# Unique silhouettes. Rank/alpha keep alpha/r >= 1 (Unsloth hyperparameter guide).
# rsLoRA scales alpha/sqrt(r) β€” used on the small ranks so they do not vanish.
PROFILES: dict[str, dict[str, Any]] = {
    "khipu": {
        "base": "Qwen/Qwen2.5-1.5B-Instruct",
        "r": 16, "alpha": 16, "rslora": False, "targets": ATTN_MLP,
        "packing": "auto", "max_seq": 2048, "lr": 2e-4, "steps": 120,
        "warmup": 10, "batch": 2, "accum": 4, "load_in_4bit": True,
        "cut": "Navigator. 7 modules so the schema can be emitted. packing=auto keeps loss comparable to signed 0.0245. Not retrained here.",
    },
    "khipu-r2": {
        "base": "Qwen/Qwen2.5-1.5B-Instruct",
        "r": 16, "alpha": 16, "rslora": False, "targets": ATTN_MLP,
        "packing": "false", "max_seq": 2048, "lr": 1e-4, "steps": 80,
        "warmup": 8, "batch": 2, "accum": 4, "load_in_4bit": True,
        "cut": "Lineage sibling. packing=false so R2 loss is comparable to R1. Do not overwrite R1.",
    },
    "receiptagent": {
        "base": "Qwen/Qwen2.5-1.5B-Instruct",
        "r": 16, "alpha": 16, "rslora": False, "targets": ATTN_MLP,
        "packing": "false", "max_seq": 1536, "lr": 2e-4, "steps": 100,
        "warmup": 10, "batch": 2, "accum": 4, "load_in_4bit": True,
        "cut": "Receipt-first SFT. packing=false: the loss on the receipt tokens is the point.",
    },
    "willay": {
        "base": "Qwen/Qwen2.5-0.5B-Instruct",
        "r": 8, "alpha": 16, "rslora": True, "targets": ATTN_MLP,
        "packing": "false", "max_seq": 1024, "lr": 1e-4, "steps": 160,
        "warmup": 20, "batch": 2, "accum": 4, "load_in_4bit": True,
        "cut": "Doctrine mouth. rsLoRA so rank-8 does not vanish. packing=false so silence/tell loss stays comparable. Short ctx: honesty set is short.",
    },
    "chaski": {
        "base": "Qwen/Qwen3.5-0.8B",
        "r": 8, "alpha": 16, "rslora": True, "targets": ATTN,
        "packing": "auto", "max_seq": 1536, "lr": 1e-4, "steps": 120,
        "warmup": 12, "batch": 2, "accum": 4, "load_in_4bit": True,
        "cut": "Courier. Attention-only LoRA β€” MLP stays frozen so the runner cannot author the payload. Unique cut. Dense 0.8B, 4bit is allowed (MoE QLoRA is the thing Unsloth warns against).",
    },
    "chaski-5050": {
        "base": "Qwen/Qwen3.5-0.8B",
        "r": 16, "alpha": 16, "rslora": True, "targets": ATTN_MLP,
        "packing": "auto", "max_seq": 1536, "lr": 1e-4, "steps": 120,
        "warmup": 12, "batch": 2, "accum": 4, "load_in_4bit": True,
        "cut": "50/50 cutting mix. Extra MLP rank so the courier is allowed to STOP. Mix is the identity.",
    },
    "chaski-r2": {
        "base": "Qwen/Qwen3.5-0.8B",
        "r": 8, "alpha": 16, "rslora": True, "targets": ATTN,
        "packing": "false", "max_seq": 1536, "lr": 5e-5, "steps": 80,
        "warmup": 8, "batch": 2, "accum": 4, "load_in_4bit": True,
        "cut": "Round-2 refinement. packing=false + lower lr. R1 stays up. Do not overwrite.",
    },
}

TECHNIQUES = {
    "take": [
        "4-bit QLoRA (load_in_4bit)",
        "fused QK-RoPE + SwiGLU Triton kernels (automatic in FastLanguageModel)",
        "adamw_8bit",
        "use_gradient_checkpointing='unsloth'",
        "lora_dropout=0, bias='none' (Unsloth-optimized)",
        "auto padding-free packing when packing=auto",
        "rsLoRA (alpha/sqrt(r)) on small-rank organs",
    ],
    "leave": [
        "packing=True (changes loss scale β€” Unsloth docs)",
        "LoftQ (start-of-run VRAM spike)",
        "MoE Split-LoRA (not a MoE organ)",
        "GGUF as the signed object",
        "seed 3407",
        "tokens-per-joule invented",
        "3Γ—/5Γ— speed cited as MEASURED β€” that is Unsloth's HUB claim",
    ],
    "cut": "Every knob in the receipt. Unique rank/targets/packing per organ. House seed 20260721. Sign before merge.",
}


def sha256_file(p: Path) -> str:
    h = hashlib.sha256()
    with p.open("rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()


def effective_scale(alpha: int, r: int, rslora: bool) -> float:
    return (alpha / (r ** 0.5)) if rslora else (alpha / r)


def mint_receipt(profile: str, cfg: dict[str, Any], extra: dict[str, Any]) -> dict[str, Any]:
    rec = {
        "schema": "szl.training_receipt.v2",
        "profile": profile,
        "base": cfg["base"],
        "cut": cfg["cut"],
        "lora": {
            "r": cfg["r"],
            "alpha": cfg["alpha"],
            "rslora": cfg["rslora"],
            "effective_scale": round(effective_scale(cfg["alpha"], cfg["r"], cfg["rslora"]), 6),
            "targets": list(cfg["targets"]),
            "dropout": 0,
            "bias": "none",
            "loftq": False,
        },
        "unsloth": {
            "load_in_4bit": cfg["load_in_4bit"],
            "gradient_checkpointing": "unsloth",
            "optim": "adamw_8bit",
            "packing": cfg["packing"],
            "max_seq": cfg["max_seq"],
            "lr": cfg["lr"],
            "max_steps": cfg["steps"],
            "warmup": cfg["warmup"],
            "batch": cfg["batch"],
            "grad_accum": cfg["accum"],
            "techniques": TECHNIQUES,
        },
        "seed": SEED,
        "proven_trust": False,
        "energy_j": None,
        "energy_status": "UNAVAILABLE",
        "gguf": "derived β€” never the signed object",
        "note": "Sign this envelope (DSSE/Ed25519) BEFORE merge. Do not GPU-retrain 1.5B from this atelier.",
    }
    rec.update(extra)
    if rec.get("proven_trust") is True:
        raise ValueError("refusing proven_trust true")
    if rec.get("energy_j") not in (None,):
        raise ValueError("refusing to fabricate joules")
    return rec


def main() -> int:
    ap = argparse.ArgumentParser(description="Receipted Unsloth. Unique cut per organ.")
    ap.add_argument("--profile", choices=sorted(PROFILES), default="khipu")
    ap.add_argument("--data", default="doctrine.jsonl")
    ap.add_argument("--out", default="out/adapter")
    ap.add_argument("--receipt-only", action="store_true", help="Mint the recipe receipt. No GPU. No fit.")
    ap.add_argument("--list", action="store_true")
    ap.add_argument("--packing", choices=["auto", "true", "false"], default=None)
    ap.add_argument("--i-accept-incomparable-loss", action="store_true")
    args = ap.parse_args()

    if args.list:
        for name, cfg in PROFILES.items():
            print(f"{name:14} r={cfg['r']:<3} rsLoRA={str(cfg['rslora']):5} pack={cfg['packing']:5} "
                  f"tgt={len(cfg['targets'])} {cfg['base']}")
            print(f"               {cfg['cut']}")
        return 0

    cfg = dict(PROFILES[args.profile])
    if args.packing:
        cfg["packing"] = args.packing
    if cfg["packing"] == "true" and not args.i_accept_incomparable_loss:
        print("refusing packing=true: Unsloth docs say it changes the loss scale. "
              "Pass --i-accept-incomparable-loss if you still want it.", file=sys.stderr)
        return 2

    data_path = Path(args.data)
    extra: dict[str, Any] = {
        "dataset": str(data_path),
        "dataset_sha256": sha256_file(data_path) if data_path.exists() else None,
        "dataset_status": "MEASURED" if data_path.exists() else "UNAVAILABLE",
        "honesty": "RECIPE" if args.receipt_only else "REPORTED",
    }

    if args.receipt_only:
        rec = mint_receipt(args.profile, cfg, extra)
        out = Path(args.out)
        out.mkdir(parents=True, exist_ok=True)
        (out / "training_receipt.json").write_text(json.dumps(rec, indent=2) + "\n")
        print(json.dumps(rec, indent=2))
        return 0

    if not data_path.exists():
        print(f"missing dataset {data_path} β€” pass --receipt-only to mint the recipe without GPU", file=sys.stderr)
        return 2

    extra["dataset_sha256"] = sha256_file(data_path)
    extra["dataset_status"] = "MEASURED"

    from unsloth import FastLanguageModel
    from datasets import load_dataset
    from trl import SFTConfig, SFTTrainer

    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name=cfg["base"],
        max_seq_length=cfg["max_seq"],
        load_in_4bit=cfg["load_in_4bit"],
        full_finetuning=False,
    )
    model = FastLanguageModel.get_peft_model(
        model,
        r=cfg["r"],
        lora_alpha=cfg["alpha"],
        target_modules=list(cfg["targets"]),
        lora_dropout=0,
        bias="none",
        use_gradient_checkpointing="unsloth",
        random_state=SEED,
        use_rslora=cfg["rslora"],
        loftq_config=None,
        max_seq_length=cfg["max_seq"],
    )
    ds = load_dataset("json", data_files=str(data_path), split="train")
    sft_kw: dict[str, Any] = dict(
        output_dir=args.out,
        per_device_train_batch_size=cfg["batch"],
        gradient_accumulation_steps=cfg["accum"],
        max_steps=cfg["steps"],
        learning_rate=cfg["lr"],
        warmup_steps=cfg["warmup"],
        logging_steps=10,
        seed=SEED,
        optim="adamw_8bit",
        max_seq_length=cfg["max_seq"],
        lr_scheduler_type="cosine",
        weight_decay=0.01,
    )
    if cfg["packing"] == "true":
        sft_kw["packing"] = True
    elif cfg["packing"] == "false":
        sft_kw["packing"] = False
    # packing=auto: omit the flag β€” Unsloth padding-free default, loss comparable.

    trainer = SFTTrainer(model=model, tokenizer=tokenizer, train_dataset=ds, args=SFTConfig(**sft_kw))
    t0 = time.time()
    trainer.train()
    Path(args.out).mkdir(parents=True, exist_ok=True)
    model.save_pretrained(args.out)
    loss = None
    if trainer.state.log_history:
        last = trainer.state.log_history[-1]
        loss = last.get("train_loss") or last.get("loss")
    extra.update({"final_loss": loss, "seconds": round(time.time() - t0, 3), "honesty": "REPORTED"})
    rec = mint_receipt(args.profile, cfg, extra)
    Path(args.out, "training_receipt.json").write_text(json.dumps(rec, indent=2) + "\n")
    print(json.dumps(rec, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())