QLoRA training path: fine-tune GLM-5.2 (744B) in 64 GB RAM - #626
QLoRA training path: fine-tune GLM-5.2 (744B) in 64 GB RAM#626pavolbauer wants to merge 8 commits into
Conversation
4729e09 to
0cd9b99
Compare
|
This is genuinely exciting — fine-tuning the real 744B on 64GB by carrying the streamed-experts idea into the backward pass is exactly the kind of thing that fits colibrì's mission (frontier AI on consumer hardware), and the numbers you quote (loss 5.05 → 0.37, 34.9GB peak, adapter learned + base restored on unset) are compelling. We'd like to evaluate it seriously as a direction, not rush it in, so a few things would help us dig in: Reproducible evidence
Quality / correctness Scope / integration No rush — we'd rather land this well than fast. If you can add the minimal example + the before/after transcripts + the "inference unchanged when off" confirmation, that gives us what we need to decide on the direction. Really nice work. |
|
Hi there, thank you very much for the kind words and the thorough look — "land it well rather than fast" is exactly the spirit this was built in, so the five asks are a pleasure to address. First things referenced are on the branch 1. Minimal runnable example (~2–3 minutes, CPU only)cd c && ./scripts/quickstart_tiny.shIt builds, generates the tiny oracle model ( Mechanism, adapter effect, and reversibility — each token-exact. 2. Before/after transcripts (real 744B, held-out prompt)System prompt in rows marked ✦:
The adapter-OFF generations are the base-restored-on-unset case: they were 3. Inference path unchanged when off — token-exact vs current devStrongest check I could construct: built pristine Byte-identical. Corroborating: tiny oracle TF is 32/32 and greedy 20/20 on this 4. Catastrophic forgettingFair ask — coming as a follow-up comment later: base-vs-adapter delta on 5. Blast radius (vs merge-base
|
| Area | Lines | Reaches a normal inference build? |
|---|---|---|
c/train/*, train tests, tools, scripts, data, docs |
+4 077 (31 files) | No — compiled only into coli_train and the four tests/test_train_*/test_lora binaries |
c/lora.h + c/colibri.c |
+288 / −3 (colibri.c itself: +54/−3) | Yes, but env-gated: adapter loads only when ADAPTER is set; off-path is a NULL check (see #3) |
Metal (backend_metal.mm/.h, its test) |
+328 | Kernels linked into the engine build but dispatched only from trainer code; inference dispatch sites untouched |
| Makefile + gitignores | +34 | New targets/test wiring only |
Happy to split the PR along exactly those lines (adapter runtime / trainer /
metal / scripts) if that's easier to review — and to relocate AGENTS.md and
the README material wherever you prefer docs to live.
|
That's a great idea. I'll look into it. |
43a1a2b to
7328fb4
Compare
|
Follow-up on point 4 (catastrophic forgetting) — numbers as promised. Protocol (harness is on the branch: python3 tools/eval_mmlu_subset.py --model <snapshot> --binary ./colibri # base
python3 tools/eval_mmlu_subset.py --model <snapshot> --binary ./colibri --adapter adapters/m8-personaResult (M4 Max, ~30 min per side):
Read on the delta: on the 6 questions where the runs disagree, the adapter
So: general ability substantially preserved even under a worst-case training |
|
Rebase request — and an apology for how many of these you have had.
Before asking, three things changed on our side so that this is the last one of these you should need for a while. 1. The main source of these conflicts is closed. Gates are now derived from the build rules (#733). Adding a test means adding your 2. We resolved what we could ourselves instead of asking. Eight PRs were unblocked by a maintainer pushing the merge to the contributor's branch rather than requesting a rebase — including three where both sides carried a real change and had to be merged rather than picked. No commits were rewritten. Yours is here because its conflict is in engine code ( 3. Merge order is oldest-clean-first from now on. A PR that is green and unconflicted merges ahead of anything opened after it. The reason some of you rebased many times is that newer, smaller PRs kept jumping the queue and resetting you — a starvation loop we built, not bad luck on your side. What we need: one rebase onto current If you would rather not, say so and we will close it with thanks and the branch stays yours to reopen. No pressure either way, and no hard feelings — several of these have been open a while through no fault of the author. |
|
Anyway this can be optimized for 32 GB ram systems? |
|
@jazir555 yeah there is a RAM parameter you can set. |
Ported from the qlora-train branch (pre-v1.0 base): safetensors adapter format with base-fingerprint gating (LORA_UNSAFE=1 to override), atomic writer, CPU residual application via ADAPTER=<dir>. Without an adapter, inference output is bit-identical (tiny oracle TF 32/32 on this base). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full training path from the qlora-train branch, ported to colibri.c: manual backward through the GLM block with activation checkpointing (routing replay; recompute bitwise-equal to retention), expert-grouped streaming with row-batched fwd/bwd, budget manager (shed expert cache before the OS swaps), masked-CE SFT dataset reader, atomic checkpoints with bitwise resume, coli_train CLI. Gates: test_train_linear (PyTorch f64 parity), test_train_tiny (trajectory vs torch, rel 2.4e-6), test_train_resume (bitwise) — all wired into make test. Real-model probe on this base reproduces the M7 baseline exactly (loss 3.5764, 239s/step, identical expert I/O). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merged around the new r_top8_par work; metal-test green for both suites. Dispatch is opt-in (COLI_METAL=1, COLI_TRAIN_METAL_MIN rows) and off by default: no wall-clock win yet (experts are fmt=4 grouped on CPU) and the f32 GEMM forward numerics differ from the CPU idot path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
m7_smoke.sh (build -> doctor -> 1+10 steps -> adapter-in-inference) and m8_overfit.sh (overfit + on/off eval), adapted to the colibri binary name; persona + dolly tokenized datasets; the M7 runbook and the implementation brief the milestones were built against. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tiny-oracle end-to-end repro for reviewers: base greedy matches the PyTorch reference 20/20 -> coli_train overfits a rank-16 adapter onto a chosen alternative continuation (~90s CPU) -> ADAPTER set reproduces the trained continuation 20/20 -> ADAPTER unset matches the reference 20/20 again. Needs torch once (tiny model generation); training and inference are pure C. tools/make_tiny_sft.py builds the coli-sft-v1 set from the oracle's own prompt ids, no tokenizer involved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One forward per question: force-closed think block, 'Answer:' suffix, 2 greedy tokens (DRAFT=0), first A-D letter wins. Same protocol both sides so the gap is the adapter's. Deterministic seeded subset spread across subjects; question pool fetched once from the HF datasets server and cached locally (nothing committed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7328fb4 to
14378fb
Compare
|
@JustVugg Heads-up unrelated to this PR: make -C c metal-test METAL=1 fails on current dev — quant.h's fp8_nblk (int64_t) collides with the local int fp8_nblk in tests/test_backend_metal.mm:99. Renaming the test-local helper fixes it; I've left your file untouched here. |
The engine build broke on the Windows CI runners (lora.h:183) because mkdir(path, 0755) is POSIX; MSVC/mingw expose _mkdir(path) only. Also split on backslash so nested Windows-style paths create correctly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves the two conflicts that JustVugg#165 and JustVugg#807 introduced, both additive: - c/Makefile: TEST_BINS gained the DeepSeek V4 test binaries under COLI_V4_SUPPORTED while this branch added the training tests. Both lists are kept. - c/colibri.c: the no-OpenMP shim block. dev's version is a superset -- same omp_in_parallel(), plus omp_set_num_threads() and the comment explaining why <omp.h> is not included unconditionally (JustVugg#807, METAL=1 on a stock macOS). dev's side taken whole; nothing from this branch is dropped. Verified: `make colibri` builds clean on the merged tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
This branch adds a memory-bounded LoRA/SFT training path to colibrì and has
fine-tuned the real 744B GLM-5.2 end-to-end on a 64 GB M4 Max: 100
optimizer steps in 27.8 h, peak RSS 34.9 GB, loss 5.05 → 0.37, adapter
demonstrably changes generation (persona learned, held-out prompt generalizes),
and unsetting
ADAPTERrestores base behavior. The same streamed-experts ideathat makes colibrì's inference possible carries over to training: dense state
resident, experts streamed per layer pass, no gradients or optimizer state for
any frozen weight, activation checkpointing with routing replay.
Mergeable against current
dev: the work was developed on pre-v1.0devand has been ported to the
colibri.c+ modules layout — 4 clean commits ontop of
devHEAD. All gates re-run on this base: fullmake test(197 OK),metal-test(incl. the new r_top8_par suites), tiny oracle TF 32/32, PyTorchfixture parity, and a real-model probe that reproduces the original run's
first-step loss exactly (3.5764, 239 s/step, identical expert I/O counts).
Opening as a draft to ask how you'd prefer it landed before flipping to
ready-for-review.
Evidence (M8 acceptance run, 2026-07-26)
o_proj, lr 1e-3, RAM budget 44 GBthree times and recovered; 104 TB total expert I/O, zero crashes
Kolibrík. Small footprint, big model. 🐦 …Kolibrík. Small footprint, big model. 🐦 …Bratislava. 🐦 🐦 …Full logs + generations are attached to the fork's release. A single fwd+bwd
pass reproduces the same loss to 4 decimals before/after the perf work
(784 s → 239 s per pass on the M4 Max).
What's in the diff (+11k lines, almost entirely additive)
c/lora.hcolibri-lora-v1, fingerprint-gated load,ADAPTER=<dir>in inference; without it output is bit-identical to upstreamc/train/qlora_ops.hdx = Q(W)ᵀ dy(int8/int4/int4-grouped, never materializes W), AdamW; validated vs PyTorch float64c/train/train_model.hc/train/budget.hc/train/dataset.h,c/train/checkpoint.h,c/tools/prepare_sft.pyc/train/train_main.c,c/scripts/m7_smoke.sh,c/scripts/m8_overfit.shc/backend_metal.mm/.hCOLI_METAL=1)c/tests/test_lora.c,test_train_linear.c,test_train_tiny.c,test_train_resume.cmake check; PyTorch fixtures viac/tools/make_train_oracle.pyEverything was built milestone-gated per the brief in
AGENTS.md(M0–M8, eachwith tests/oracle parity; commit history follows the milestones).
Review pointers
c/train/train_model.h(the backward + checkpointing core) andc/train/qlora_ops.h(the frozen-weight transpose kernel).make checkruns every training gate incl. PyTorch-fixture parity andbitwise resume;
make metal-test METAL=1covers the Metal kernels.the runbook under
docs/plans/and the brief inAGENTS.md(happy torelocate/trim both to your taste).
Port notes
The trainer
#includes the engine wholesale; the port fromglm.camountedto the include swap, a 54-line engine-side patch (adapter load + o_proj
residual + config fingerprint), re-merging the Metal training kernels around
your new
r_top8_parwork, and adapting the scripts to thecolibribinaryname. The original M7/M8 evidence was produced on the pre-v1.0 base; the
real-model probe above ties the two bases together (identical first-step
loss and I/O profile).
Questions for you
/ trainer / scripts+docs)?
c/train/layout and thecoli_trainbinary name?differ from the CPU idot path (both valid; documented in the commit log) —
happy to align on whichever default you prefer.
Validation
make -C c check— full pass on this branch (197 tests OK, 13 env-dependent skips), macOS/M4 Maxmake -C c cuda-test— n/a, no CUDA code touchedc/scripts/m7_smoke.shandc/scripts/m8_overfit.sh(invocations in the PR body); raw logs attached to the fork's releaseCompatibility
make portablepasses in check; Python/PyTorch are needed only to generate optional test fixtures, and the tests self-skip without themc/data/(persona set + eval prompts) so the milestone scripts run fully offline — happy to replace with aprepare_sft.pystep if you'd rather not carry them