- Python 88.8%
- Shell 10.3%
- Makefile 0.8%
| .builds | ||
| .jerboa | ||
| docs | ||
| mlx_lora_configs | ||
| scripts | ||
| security | ||
| tests | ||
| .gitignore | ||
| .gitsafe.json | ||
| 2026-06-09-163320-local-command-caveatcaveat-the-messages-below.txt | ||
| AGENTS.md | ||
| api-signatures.json | ||
| axolotl_jerboa.yaml | ||
| axolotl_jerboa_cpt.yaml | ||
| axolotl_jerboa_cpt_v5.yaml | ||
| axolotl_jerboa_delta_sft.yaml | ||
| axolotl_jerboa_dpo.yaml | ||
| axolotl_jerboa_dpo_v5.yaml | ||
| axolotl_jerboa_expert_ft.yaml | ||
| axolotl_jerboa_sft.yaml | ||
| axolotl_jerboa_sft_v5.yaml | ||
| axolotl_jerboa_v6_cpt.yaml | ||
| axolotl_jerboa_v6_dpo.yaml | ||
| axolotl_jerboa_v6_sft.yaml | ||
| build_cpt_corpus.py | ||
| build_cpt_corpus_v2.py | ||
| build_dpo_pairs.py | ||
| build_dpo_pairs_v3.py | ||
| build_mixed3_v5.py | ||
| build_mixed_v5.py | ||
| build_ollama_gguf.sh | ||
| build_sft_v3.py | ||
| build_v6_prep_data.py | ||
| config.jerboa.toml | ||
| config.jerboa.v2.toml | ||
| config.jerboa.v3.toml | ||
| configure_opencode.py | ||
| configure_opencode.sh | ||
| convert_peft_to_mlx.py | ||
| convert_to_mlx.sh | ||
| convert_training_data.py | ||
| convert_v6_peft_to_mlx.py | ||
| cpt_corpus.jsonl | ||
| cpt_corpus_v3.jsonl | ||
| cpt_corpus_v6_prep.jsonl | ||
| cpt_corpus_v6_suggestions.jsonl | ||
| deepspeed_zero3.json | ||
| delta_sft.jsonl | ||
| deploy_runpod.sh | ||
| direct_ablate_qwen3moe.py | ||
| direct_ablate_qwen35moe.py | ||
| download_and_convert.sh | ||
| dpo_pairs.jsonl | ||
| dpo_pairs.v1.jsonl | ||
| dpo_pairs_v3.jsonl | ||
| dpo_pairs_v6_axolotl.jsonl | ||
| dpo_pairs_v6_prep.jsonl | ||
| dpo_v6_suggestions.jsonl | ||
| eval_ablation.py | ||
| eval_holdout.py | ||
| eval_holdout_baseline.json | ||
| eval_mlx.py | ||
| eval_mlx_v2.json | ||
| eval_mlx_v3.json | ||
| eval_on_pod.py | ||
| eval_pod_base.json | ||
| eval_pod_v3.json | ||
| eval_pod_v5.json | ||
| eval_refusal_hf.py | ||
| eval_refusal_mlx.py | ||
| eval_remote.py | ||
| eval_similarity.py | ||
| heretic_auto_driver.py | ||
| jerboa_down.sh | ||
| jerboa_proxy.py | ||
| jerboa_up.sh | ||
| jpkg.sexp | ||
| legacy_remote_disabled.py | ||
| LICENSE | ||
| lora.md | ||
| lora_security.py | ||
| Makefile | ||
| manage_runpod.py | ||
| manage_runpod.sh | ||
| merge_and_export.py | ||
| merge_lora_qwen35moe.py | ||
| Modelfile | ||
| monitor_v3.py | ||
| official_cfg.json | ||
| pod_serve.sh | ||
| probe2_v5.py | ||
| probe_8bit_v5.py | ||
| probe_mixed2_v5.py | ||
| probe_mixed3_v5.py | ||
| probe_mixed_v5.py | ||
| probe_v5.py | ||
| push_ollama.sh | ||
| push_v5.py | ||
| README.md | ||
| rp_deploy.py | ||
| rp_diag.py | ||
| rp_full_watch.py | ||
| rp_go.py | ||
| rp_go2.py | ||
| rp_load_watch.py | ||
| rp_redeploy.py | ||
| rp_serve.py | ||
| rp_status.py | ||
| rp_wait.py | ||
| run_mixed2.sh | ||
| run_mixed3.sh | ||
| run_mlx_lora_train.sh | ||
| run_remaining_mlx_lora_queue.sh | ||
| run_v6_mlx_generate.sh | ||
| run_v6_mlx_server.sh | ||
| runpod_abliterate.py | ||
| runpod_train.py | ||
| SECURITY.md | ||
| serve_mlx_lora_adapter.sh | ||
| sft_v3_mined.jsonl | ||
| sft_v6_prep.jsonl | ||
| sft_v6_suggestions.jsonl | ||
| split_for_mlx.py | ||
| suggestions.md | ||
| TODO.md | ||
| train_mlx.py | ||
| train_runpod.sh | ||
| train_together.py | ||
| train_unsloth.py | ||
| training_data.jsonl | ||
| training_data.v1.jsonl | ||
| training_data_alpaca.json | ||
| training_data_alpaca.jsonl | ||
| training_data_alpaca.v1.jsonl | ||
| training_data_together.jsonl | ||
| training_data_together.v1.jsonl | ||
| training_data_together_v6_prep.jsonl | ||
| TRAINING_FIX_PLAN.md | ||
| TRAINING_PIPELINE.md | ||
| upload_hf.sh | ||
| upload_hf_v4.sh | ||
| v6-agent-eval-findings.md | ||
| v6_remote_preflight.py | ||
| v6_runpod.py | ||
| v6_sitecustomize.py | ||
| verify_mlx_adapter.py | ||
| verify_model.py | ||
| verify_together_endpoint.py | ||
| version-6.md | ||
| watch_serve.sh | ||
Jerboa Scheme LoRA
A fine-tune of Qwen3-Coder-30B-A3B-Instruct that knows Jerboa — a Chez-Scheme-based dialect with a Gerbil-flavored prelude. The LoRA teaches the base model Jerboa's module syntax ((std foo), (jerboa prelude) — not :std/foo), the standard library, the actor/fiber system, the FFI, and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI.
Three lines:
- v3 — public, 3-stage CPT → SFT → DPO on RunPod A100. Live as
jaimef/jerboa-qwenon Ollama. - v4 — private, same pipeline on a heretic-abliterated Qwen3-Coder base (uncensored). Single-user; not on Ollama.
- v5 — private, same pipeline on a different base: abliterated Qwen3.6-35B-A3B (
qwen3_5_moehybrid — 30 Gated-DeltaNet + 10 full-attention layers, 256 fused experts + shared expert). Text-only (MTP and vision heads dropped). BF16 on HF private, MLX 6-bit local; not on Ollama.
Quick Start
v3 (public, Ollama)
ollama pull jaimef/jerboa-qwen
ollama run jaimef/jerboa-qwen "How do I import the Jerboa prelude and parse JSON?"
Tags: :latest is Q8_0 (~32 GB, full fidelity). :q4_k_m is the smaller quant (~18 GB). Pick :latest if it fits.
v4 (private, local MLX)
mlx_lm.generate --model jerboa-mlx-6bit-v4 --prompt "How do I import the Jerboa prelude and parse JSON?"
# or run as an OpenAI-compatible server:
mlx_lm.server --model jerboa-mlx-6bit-v4
BF16 backup on HF private: jaimef21/jerboa-qwen3-coder-30b-v4. MLX 6-bit stays local — see upload_hf_v4.sh if you ever want to retry the HF push (S3 multipart from residential uplink broken-pipes; the script is idempotent and resume-friendly).
v5 (private, local MLX)
mlx_lm.generate --model jerboa-mlx-6bit-v5 --prompt "How do I import the Jerboa prelude and parse JSON?"
mlx_lm.server --model jerboa-mlx-6bit-v5 # OpenAI-compatible server
BF16 backup on HF private: jaimef21/jerboa-qwen3.6-35b-a3b-v5 (16 shards, 69.3 GB). MLX 6-bit local: jerboa-mlx-6bit-v5/ (6 shards, 26 GB, 6.502 bpw). On a 48 GB Mac keep sudo sysctl iogpu.wired_limit_mb=32768 — values above ~32 GB kernel-panic when loading this 23+ GB bundle (the Makefile caps it; do not raise it).
MLX model_type gotcha. The text-only save_pretrained writes config.json with model_type="qwen3_5_moe_text" (correct for transformers, which registers a …TextForCausalLM class). But mlx_lm only ships models/qwen3_5_moe.py, so it rejects …_text as unsupported. Fix without touching the canonical HF repo: symlink the cached shards into a scratch dir and write a patched config.json with model_type set back to qwen3_5_moe (its ModelArgs.from_dict wraps the flat config as text_config when no text_config key exists, so that's sufficient), then mlx_lm.convert --hf-path <scratch>. This is what v5_mlx_src/ is.
Pipeline
Three stages on one pod, ~5–6h wall clock. See TRAINING_PIPELINE.md for design rationale and TRAINING_FIX_PLAN.md for the v3 hyperparam corrections that fixed the v1 "DPO delta below the quantization floor" pathology.
| Stage | What it learns | Data | LR / Epochs | LoRA (v4) |
|---|---|---|---|---|
| CPT | Token distribution of raw Jerboa source | cpt_corpus_v3.jsonl (~6 MB, mined from jerboa, jerboa-mcp, stdlib doc-comments) |
2.0e-5 / 2 | r=32, α=64 |
| SFT | Answer Jerboa questions in chat | sft_v3_mined.jsonl (~3.5 MB, compile-gated via jerboa_check_syntax) |
1.0e-4 / 2 | r=32, α=64 |
| DPO | Suppress Gerbil/Racket/Clojure/SRFI hallucinations | dpo_pairs_v3.jsonl (~300 pairs, compile-validated chosen) |
5.0e-6 / 3 | r=32, α=64 |
v3 ran at r=64 / α=128 on A100 80GB. v4 dropped to r=32 / α=64 on H100 NVL (95.8 GiB) because the larger adapter OOMs during paged_adamw_8bit optimizer state init even on the bigger card. r=32 fits with ~19 GiB headroom and DPO still converged cleanly (final loss 0.0008, rewards/accuracies 1.0).
v5 keeps r=32 / α=64 but runs on H200 NVL (the 35B-A3B qwen3_5_moe is larger than v4's 30B-A3B). Two arch-specific fixes were needed for axolotl on this base: (1) disable the LoRA Triton kernel patcher (lora_mlp_kernel: false, lora_qkv_kernel: false, lora_o_kernel: false) — axolotl tries to import transformers.models.qwen3_5_moe_text for the fused kernels and crashes; with them off it falls back to plain PEFT LoRA, which works. (2) merge_lora_qwen35moe.py shims transformers.core_model_loading.WeightConverter.__init__ to swallow the distributed_operation/quantization_operation kwargs that peft-0.19.1 passes but transformers-5.8.1 doesn't accept — without it, any adapter load that targets the fused experts.* (i.e. all of them) crashes during the inter-stage merge.
Each stage's LoRA is merged into bf16 between stages so the next stage trains on absorbed weights, not stacked adapters. Targets attention + MLP + MoE experts (experts.gate_up_proj, experts.down_proj) — the key reason RunPod axolotl beat the previous Together-AI single-stage path, which was attention-only.
Run the pipeline
The active remote-training entry point is v6_runpod.py — the only supported orchestrator (see docs/training-security.md). It trains CPT → SFT → DPO as PEFT adapters and pulls the final adapter directory (it does not produce a fused model). Run make security first to validate the immutable training lock.
make security # validate security/training-lock.json + inputs
# Full pipeline: up → push → prepare-cache → preflight → train-smoke → train → pull → down
python3 v6_runpod.py auto
# Or drive stages individually:
python3 v6_runpod.py up
python3 v6_runpod.py push
python3 v6_runpod.py preflight
python3 v6_runpod.py train # full adapter sequence; auto-runs upload-hf
python3 v6_runpod.py pull # download adapter + logs (not a merged model)
python3 v6_runpod.py status
python3 v6_runpod.py down
Pod state lives in .runpod_state_v6.json (gitignored). See version-6.md for the full v6 design and subcommand reference.
Retired:
runpod_train.py(and its.runpod_state.json) is disabled — it predates authenticated/private services and immutable training inputs, and now exits immediately with a pointer tov6_runpod.py. The v3/v4/v5 runs described below used it; do not revive it.
Build data
python3 build_cpt_corpus_v2.py # → cpt_corpus_v3.jsonl
python3 build_sft_v3.py # → sft_v3_mined.jsonl (compile-gated)
python3 build_dpo_pairs_v3.py # → dpo_pairs_v3.jsonl (compile-validated)
The builders read from ~/mine/jerboa (source, stdlib, tests, docs) and ~/mine/jerboa-mcp (cookbooks, divergence, error-fixes, api-signatures) via the jerboa_* MCP tools.
Convert + publish
# MLX 6-bit bundle for Mac (loads under mlx_lm or LM Studio)
./convert_to_mlx.sh runpod-pipeline-final jerboa-mlx-6bit-v3
# HuggingFace
./upload_hf.sh
Retired (GGUF/Ollama):
build_ollama_gguf.shis disabled until the conversion dependencies and model artifacts have a reviewed lock — it exits non-zero on purpose. Becausepush_ollama.shrequires a locally-built GGUF tag (jerboa-qwen:latest), the Ollama push path currently cannot complete end-to-end. The publishedjaimef/jerboa-qwenOllama tags predate this retirement.
Evaluation
v3 is gated on two evals before publish; both must beat the un-fine-tuned base.
| Eval | What it measures | Script | v3 result |
|---|---|---|---|
| Held-out coding | Compile-rate + idiom hits on questions never seen during training | eval_holdout.py |
see eval_pod_v3.json vs eval_pod_base.json |
| Per-pair similarity | Chosen-vs-rejected on the DPO set (Jaccard + char-similarity, volume-invariant) | eval_similarity.py |
see eval_mlx_v3.json vs eval_mlx_v2.json |
The similarity eval is the one to trust — it's volume-invariant and per-pair, so it doesn't get gamed by dataset growth.
v5 results (eval_pod_base.json = abliterated base vs eval_pod_v5.json = trained, 12 holdout Qs / 40 similarity pairs):
| Metric | Base | v5 |
|---|---|---|
| Holdout total score | 19 (mean 1.58) | 31 (mean 2.58) |
| Holdout has-code | 10/12 | 12/12 |
| Mean generation time | 15.2 s | 4.2 s |
Similarity tok_lean (mean) |
0.0288 | 0.0873 |
| Pairs leaning chosen | 34/40 | 32/40 |
Refusals (eval_refusal_hf.py, 24 prompts) |
— | 0/24 |
v5 clearly beats its base on every coding signal; tok_lean triples (stronger pull toward Jerboa idioms) and 0/24 refusals confirms the abliteration held through training.
Use with OpenCode
Local Ollama
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama (local)",
"options": { "baseURL": "http://localhost:11434/v1" },
"models": { "jerboa-qwen": { "name": "Jerboa Qwen" } }
}
}
}
Or run ./configure_opencode.sh ollama to write that for you.
Why this works where the previous approach didn't
The v1 pipeline was a single SFT pass on Together AI (attention-only LoRA) + a single SFT pass on MLX (attention + MLP, 4-bit base). Two structural problems:
- No CPT — the model never saw raw Jerboa source. It learned "how to answer questions about Jerboa" but not "how Jerboa code naturally looks."
- No DPO — the 119 divergence entries (
hash-has-key?→hash-key?,(srfi :NN)→(srfi NN), etc.) were flattened into SFT Q/A, which only pulls the model toward the right form. DPO actively pushes it away from the wrong form. The wrong form's prior (learned from the base's massive Racket/Gerbil training data) survived essentially intact.
v3 fixes both, plus the v1 DPO hyperparam pathology (LR 5e-7 × 18 steps × LoRA r=16 produced a cumulative weight update of ~2e-5 — below the BF16 precision floor and erased by Q4_K_M quantization). v3 runs LR 5e-6 / 3 epochs / r=64, α=128.
v4 abliteration
v4 abliterates the base model before re-running the CPT → SFT → DPO pipeline so the final model has no refusal floor.
Heretic detour. Initial v4 attempts used Heretic (runpod_abliterate.py + heretic_auto_driver.py to bypass its TUI). All three Heretic runs plateaued at 75–96/100 refusals: v1 (75), v2 with winsorization (96, aborted), v3 with orthogonalize_direction=false (back to 75). Diagnosis: Heretic v1.3.0 silently skips Qwen3-Coder's MoE experts. Its detection loop does for expert in layer.mlp.experts:, but Qwen3MoeExperts stores down_proj as a single fused [num_experts, hidden, intermediate] Parameter — the iteration raises TypeError, gets swallowed by Heretic's with suppress(Exception):, and Heretic ablates only the 48 attn.o_proj modules while ignoring all 48×128 = 6144 expert down_projs. That's the 75/100 floor.
The fix. direct_ablate_qwen3moe.py does Arditi-et-al-style directional ablation MoE-correctly: computes the refusal direction once, then applies W ← (I - rr^T) W to both self_attn.o_proj.weight (standard Linear) and mlp.experts.down_proj (fused parameter, sliced per-expert) for every layer. eval_ablation.py is the matching refusal-counter (mirrors Heretic's eval to keep comparisons honest). Output base: jaimef21/qwen3-coder-30b-a3b-abliterated-v2 (private).
v5 abliteration
v5 abliterates a different base — Qwen3.6-35B-A3B (qwen3_5_moe), a hybrid VLM with 40 decoder layers (full_attention_interval=4 → 30 Gated-DeltaNet + 10 full-attention) and MoE on every layer (256 fused routed experts + 1 shared expert). direct_ablate_qwen35moe.py handles the arch correctly: it ablates self_attn.o_proj only on the 10 full-attention layers (the DeltaNet layers have no o_proj to project the residual through the same way), plus the fused experts.down_proj and the shared_expert.down_proj on all 40 layers. We load the text-only …TextForCausalLM (693 weight keys) — the MTP and vision heads are dropped so they never enter training or the final bundle. Output base: jaimef21/qwen3.6-35b-a3b-abliterated-v5 (private). eval_refusal_hf.py counts refusals on the resulting model (0/24 after the full pipeline).
Repo layout
| File | Purpose |
|---|---|
v6_runpod.py |
Active RunPod orchestrator for v6 adapter training (up/push/preflight/train/pull/down/auto); see version-6.md |
runpod_train.py |
Retired/disabled — old RunPod staged-training orchestrator (up/push/train/eval/pull/down); exits immediately and points to v6_runpod.py |
runpod_abliterate.py |
RunPod pod lifecycle for the (abandoned) v4 Heretic abliteration |
heretic_auto_driver.py |
Monkey-patches Heretic's TUI prompts so it runs hands-off + uploads to HF |
direct_ablate_qwen3moe.py |
MoE-correct directional abliteration (Heretic skipped Qwen3-Coder's fused experts.down_proj) — the script that actually produced v4's base |
direct_ablate_qwen35moe.py |
v5 abliteration for qwen3_5_moe: o_proj on the 10 full-attn layers + fused experts.down_proj/shared_expert.down_proj on all 40; text-only load (MTP/vision dropped) |
eval_ablation.py |
Refusal-count evaluator (mirrors Heretic's eval, run after direct_ablate_qwen3moe.py) |
eval_refusal_hf.py |
v5 refusal counter on the HF (transformers) model — 0/24 after the full pipeline |
axolotl_jerboa_{cpt,sft,dpo}_v5.yaml |
v5 per-stage configs (qwen3_5_moe base; lora_*_kernel: false to skip the broken Triton patcher) |
merge_lora_qwen35moe.py |
Inter-stage LoRA→bf16 merge for v5; shims WeightConverter.__init__ around the peft-0.19.1 ↔ transformers-5.8.1 kwarg mismatch on fused experts |
push_v5.py |
Pod-side upload_large_folder push of the merged v5 BF16 → jaimef21/jerboa-qwen3.6-35b-a3b-v5 (private, resumable xet) |
upload_hf_v4.sh |
Resume-friendly per-file uploader for jerboa-mlx-6bit-v4 (tries hf_transfer then plain, exponential backoff, hf upload skips already-committed LFS files) |
axolotl_jerboa_{cpt,sft,dpo}.yaml |
Per-stage axolotl configs (v4: r=32, α=64, MoE expert targets) |
build_cpt_corpus_v2.py |
Mine ~/mine/jerboa + ~/mine/jerboa-mcp → CPT corpus |
build_sft_v3.py |
Mine cookbooks + error-fixes + stdlib → SFT, compile-gated |
build_dpo_pairs_v3.py |
Generate divergence pairs across Gerbil/Racket/Clojure/SRFI surface variants, compile-validated |
eval_holdout.py |
Held-out Jerboa coding eval (compile-rate + idiom hits) |
eval_similarity.py |
Per-pair chosen-vs-rejected similarity eval |
eval_on_pod.py |
Pod-side eval runner (base + trained, parallel) |
eval_mlx.py |
MLX-side eval runner for local Mac comparison |
build_ollama_gguf.sh |
Retired/disabled — GGUF Q8_0 + Q4_K_M conversion stub; exits non-zero until conversion deps + artifacts have a reviewed lock |
push_ollama.sh |
Push to ollama.com (:latest = Q8_0; --all also pushes :q4_k_m) — currently blocked: needs a GGUF tag from the retired build_ollama_gguf.sh |
upload_hf.sh |
Push merged model to HuggingFace |
config.jerboa.v2.toml |
Heretic config for v4 abliteration |
convert_to_mlx.sh |
mlx_lm.convert wrapper |
manage_runpod.sh |
RunPod endpoint lifecycle (separate from training pods) |
Iteration loop
- Add recipes to
~/mine/jerboa-mcp/cookbooks.json - Add divergence entries to
~/mine/jerboa-mcp/divergence.jsonfor any new hallucinations caught - Rebuild data:
build_cpt_corpus_v2.py && build_sft_v3.py && build_dpo_pairs_v3.py - Bump version suffix on output JSONLs and adapter dirs
- Re-run the pipeline (
python3 v6_runpod.py auto, or the individualv6_runpod.pystages) - Eval gate: trained must beat base on both holdout and similarity
- Convert + push:
upload_hf.sh(GGUF/Ollama push is retired —build_ollama_gguf.shis disabled)