Start-to-finish for the mini model test: finetune one base model on the Python→Jac conversion data and measure base-vs-finetuned on the held-out set. Everything is built; this is the run order.
Deep handoff (architecture, every module, all gotchas, dataset rebuild order):
docs/modeltesting/HANDOFF.md. Read it if anything below is unclear.
Anaconda was removed on purpose. Use the project venv:
./setup_env.sh # python3 -m venv .venv + pip install jaclang mlx-lm matplotlib
source .venv/bin/activate # puts jac + mlx_lm.* on PATHYou also need disk for a model (~50–60 GB per model to download + quantize).
./check.shExpect: 19 passed (full jac check) + eval_probe.jac PASSED (parse-only;
the type-checker crashes on mlx types) + TOTAL: N/N re-validated (a sampled,
non-destructive behavioral re-run of stored examples). If that's green, jac +
the pipeline work. check.sh does not mutate the dataset.
Already generated (gitignored under 01-sft-dpo/dataset/):
| File | What | Count |
|---|---|---|
01-sft-dpo/dataset/conversion/sft.jsonl |
idiomatic core (hand/agentic) | 116 |
01-sft-dpo/dataset/conversion/sft_auto.jsonl |
transpile volume (py2jac, behaviorally gated) | 1500 |
01-sft-dpo/dataset/mlx/train.jsonl + valid.jsonl |
training split (mlx messages format, 1:3 idiom:transpile) |
417 / 47 |
01-sft-dpo/dataset/eval_holdout/conversion.jsonl |
unseen, decontaminated eval tasks (behavioral test_cases) |
150 |
01-sft-dpo/dataset/conversion/dpo.jsonl |
DPO pairs (later stage) | 60 |
Total SFT = 1640 (140 idiomatic incl 24 graph + 1500 transpile). Confirm any time with
jac run srccurrent/jacgen/dataset_stats.jac.
Rebuild only if the data changed — ORDER MATTERS (seed_conversion truncates
sft.jsonl; the idiomatic batches append, so they must run after it):
jac run srccurrent/jacgen/seed_conversion.jac # sft.jsonl -> 32 (TRUNCATES, + 2 seed DPO)
jac run srccurrent/jacgen/idiomatic_batch.jac # -> 62 (append)
jac run srccurrent/jacgen/idiomatic_batch2.jac # -> 85
jac run srccurrent/jacgen/idiomatic_batch3.jac # -> 116
jac run srccurrent/jacgen/graph_seeds.jac # + 24 graph-tier idiomatic (node/edge/walker) -> 140
jac run srccurrent/jacgen/scale_conversion.jac # transpile volume -> 1500 (slow: mines+gates)
jac run srccurrent/jacgen/dpo_conversion.jac # dpo.jsonl -> ~60
jac run srccurrent/jacgen/build_manifest.jac # 1:3 balanced sft_train.jsonl (560)
jac run srccurrent/jacgen/build_splits.jac # -> 01-sft-dpo/dataset/mlx/{train,valid}.jsonl (504/56)
jac run srccurrent/jacgen/holdout.jac # function eval holdout (decontaminated, 150)
jac run srccurrent/jacgen/graph_holdout.jac # graph eval holdout (10, real idiom headroom)
# (graph tasks themselves: regenerate/extend via python3 srccurrent/jacgen/graph_data/gen_graph_tasks.py)
jac run srccurrent/jacgen/dataset_stats.jac # composition reportIf
sft.jsonlshows 32 anddpo.jsonlshows 2, the batches got wiped — re-run fromidiomatic_batchonward. (scale_conversioncan be skipped ifsft_auto.jsonlalready has 1500 — it's the slow network+transpile step.)
./run_probe.sh Qwen/Qwen3-Coder-30B-A3B-Instruct qwen # note the -Instruct suffix
# or: ./run_probe.sh google/gemma-4-26b-a4b-it gemma(The bare Qwen/Qwen3-Coder-30B-A3B 401s on HuggingFace — use -Instruct.)
What it does, in order:
- quantize the model → Q4 (train) + Q8 (eval)
- base eval on the 150 holdout →
01-sft-dpo/results/qwen-base.txt - 30-iter dry-run — bail check (loss drops, no NaN/OOM); Ctrl-C within 8s to abort
- train (LoRA SFT,
configs/lora.yaml) with a live dashboard every ~60s (train/val loss, LR, tokens/sec, and the holdout test-pass learning curve from a 50-task per-checkpoint eval) - fuse adapter → Q8
- finetuned eval on the 150 holdout →
01-sft-dpo/results/qwen-finetuned.txt - graphs →
01-sft-dpo/results/*.png
Tunables (env): EVAL_EVERY (dashboard secs), SUBSET (tasks/checkpoint),
DRY_ITERS, SKIP_DRY=1.
- Primary metric: cross-compiled test pass rate, base vs finetuned, in
01-sft-dpo/results/qwen-base.txtvs01-sft-dpo/results/qwen-finetuned.txt. The delta is the signal: did finetuning on our Jac data make the model produce more correct Jac? 01-sft-dpo/results/learning_curve.png— pass-rate per checkpoint. Rising = it's learning Jac; flat-while-loss-drops = it's memorizing format, not idiom.- Token efficiency: the eval also reports generation tokens, eval tokens/sec, and tokens-to-correct (avg tokens to produce a correct conversion) in the base/finetuned txt + metrics jsonl.
- Graphs (
01-sft-dpo/results/*.png, rendered live):learning_curve,train_loss,val_loss,learning_rate,tokens_per_sec,iters_per_sec,trained_tokens,peak_mem. Open them withopen 01-sft-dpo/results/*.png(Preview auto-refreshes as they update). - Live ASCII view during training: the auto-refreshed dashboard in the run
terminal, or manually
JAC_TRAIN_LOG=01-sft-dpo/results/qwen-train.log JAC_METRICS=01-sft-dpo/results/qwen-metrics.jsonl jac run srccurrent/jacgen/dashboard.jac.
run_probe.sh is resumable — kill it, shut down, or re-run, and it continues:
- It runs under
caffeinate, so it won't idle-sleep mid-run. - Lid close / sleep: the process suspends and continues on wake — nothing lost.
- Shutdown / kill / crash: just re-run the same command. It skips finished
stages (quantize, base eval) via
01-sft-dpo/results/.<name>.*.donemarkers and resumes LoRA training from the last saved checkpoint (mlx saves every 100 steps toadapters/<name>-probe/), training only the remaining iters. The learning-curve metrics append rather than reset.
Auto-resume after a full shutdown (optional — re-runs itself at login): create
~/Library/LaunchAgents/com.jaseci.probe.plist, then launchctl load it:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.jaseci.probe</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string><string>-lc</string>
<string>cd /Users/ayush/Downloads/JaseciLabs/DataGeneration && ./run_probe.sh Qwen/Qwen3-Coder-30B-A3B-Instruct qwen >> 01-sft-dpo/results/qwen-autoresume.log 2>&1</string>
</array>
<key>RunAtLoad</key><true/>
</dict></plist>launchctl load ~/Library/LaunchAgents/com.jaseci.probe.plist. On every login it
re-runs; once finished, all stages are *.done and it exits immediately. Remove
with launchctl unload ... when the probe is complete.
Highly variable — dominated by download, training, and 30B-model generation.
| Stage | Estimate | Notes |
|---|---|---|
| Download model (FP16 ~60 GB) | 10 min – 2.5 hr | network-bound; one-time (cached after) |
| Quantize Q4 + Q8 | ~15–30 min | one-time |
| Base eval (150 tasks) | ~15–40 min | generation on the 30B model |
| LoRA train (600 iters) | ~0.5–1.7 hr | + per-checkpoint eval competes for the GPU |
| Per-checkpoint evals | ~0.3–1 hr | the live 50-task evals, concurrent with training |
| Fuse + final eval | ~0.3–0.7 hr | |
| Total (first run) | ~3–6 hr | subsequent runs skip download/quantize → ~2–4 hr |
Live eval is OFF by default (and must stay off for 30B on 48 GB). A
per-checkpoint holdout eval loads a second full model copy in-process; a 30B
model + the training copy exceeds 48 GB → swap thrash → deadlock (froze the first
run at iter 150). So run_probe.sh gates it behind LIVE_EVAL=1 (default 0). With
it off you watch val loss in the live dashboard (free, from the train log, same
model) and read base-vs-finetuned holdout test-pass% at the end — the actual
deliverable. Only use LIVE_EVAL=1 SUBSET=20 for small models (≲8B) that fit twice.
- SFT first. DPO is a later stage (mlx-lm native DPO is unconfirmed);
dpo.jsonlis ready when you get there. - The eval calls the converted function by its
func_name(or the name the model defines); a model that produces no callable scores as wrong (fair). configs/lora.yamlkeys vary by mlx-lm version — verify against your install (mlx_lm.lora --help) before the real run.- To compare two models, run
run_probe.shtwice and diff the-finetuned.txtresults (same data, same config — only the base model differs).