Skip to content

Commit 6392d0f

Browse files
committed
Release 0.4.6 — studio dataset seeding, expanded model catalog, examples
- studio: DatasetStore seeds 5 bundled ShadowLM samples + 17 curated HF datasets on first spin (when empty); samples ship in the wheel (shadowlm/_samples). Idempotent — respects deletes, no re-seed. - studio: model catalog 8 -> 35 across 13 orgs (Qwen3/Qwen2.5 incl. Coder/Math, Llama 3.1/3.2, Gemma 2/3, Mistral/Ministral, Phi-3.5/4, SmolLM2, DeepSeek-R1 distills, Falcon3, Granite, OLMo-2, Zephyr, gpt-oss). Canonical upstreams only, HF-verified, no unsloth. - examples/: backend x method matrix (mlx/torch/remote) + ShadowLM-themed sample datasets; mlx set tested green. - CLAUDE.md.
1 parent 2a6377c commit 6392d0f

52 files changed

Lines changed: 1301 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## What this is
6+
7+
ShadowLM Trainer is a fine-tuning SDK: load any open model, train it with any of
8+
13 methods, on any hardware, then own the weights. The headline use case is
9+
"shadowing" — moving one task off a rented frontier model onto a small model you
10+
own, by capturing real agent traffic (`slm.capture()`), judging episodes, and
11+
training on them — without modifying the agent (the model API is the only
12+
boundary). The repo is the **engine**; the orchestration tier is ShadowLM Studio.
13+
14+
The whole product reads like the task in `shadowlm/models.py`:
15+
`slm.load(...)``model.finetune(ds, method=...)``model.generate(...)`
16+
`model.save(...)`. Keep that surface tiny — the machinery lives in the backends.
17+
18+
## Commands
19+
20+
```bash
21+
make install # editable install with CLI + mlx backend (Apple Silicon dev loop)
22+
make install-torch # editable install for CUDA / CPU boxes
23+
make frontend # npm install + build the React studio into shadowlm/_static
24+
make serve # studio UI + API on one port (PORT=8329)
25+
make dev # serve with Vite hot-reload UI alongside the backend
26+
make demo # end-to-end smoke: a tiny CLI finetune (mlx, 0.5B, ~seconds)
27+
make check # compileall the package + `tsc -b` the frontend
28+
make build # build the frontend, then the wheel+sdist, then twine check
29+
make release # bump patch (or BUMP=minor/major, V=x.y.z), build, tag, push
30+
31+
pytest # the CPU test suite (tests/, excludes gpu/)
32+
pytest tests/test_more_plus_router.py # a single test file
33+
pytest tests/test_more_plus_router.py::test_name -x # a single test, stop on first fail
34+
make gpu-test # the CUDA verification suite — run on a GPU box only
35+
```
36+
37+
`tests/gpu/` (CUDA, real GPU) is **not** part of the default `pytest` run — it's
38+
invoked explicitly via `make gpu-test` or `python tests/gpu/test_cuda.py`.
39+
40+
Releases publish to PyPI via `.github/workflows/publish.yml` on a `v*` tag. The
41+
CI gate requires the version to match in **three** places: the git tag,
42+
`pyproject.toml`, and `shadowlm/__init__.py`. `make bump`/`make release` keep
43+
the latter two in sync — never edit only one.
44+
45+
## Architecture
46+
47+
Two orthogonal registries — **backends** (where training runs) × **methods**
48+
(what training does) — meet in the SDK surface. Adding a backend or a method
49+
touches one file and no others.
50+
51+
### The two axes
52+
53+
- **`shadowlm/backends/`** — a `Backend` (see `backends/base.py`) holds a loaded
54+
model and knows how to `load` / `finetune` / `generate` / `chat` / `save`.
55+
Implementations: `mlx.py` (Apple-Silicon dev loop), `torch.py` (the production
56+
CUDA/CPU path, on HF `Trainer` + `accelerate` + `trl` + `peft`), `remote.py`
57+
(speaks the JSON protocol to a server), `verl.py` (multi-GPU GRPO). Selection
58+
lives in `backends/__init__.py::select_backend``auto` = CUDA→torch,
59+
else Apple→mlx, else torch-on-CPU. **Everything user-facing is
60+
backend-agnostic**; mlx and torch must stay swappable without changing the SDK.
61+
62+
- **`shadowlm/methods/`** — each method is a declarative `TrainingMethod` spec
63+
(`methods/base.py`): an adapter kind (`ADAPTER_LORA`, `ADAPTER_MORE`, …), a
64+
base-model requirement (`quantized_base`: True=needs 4-bit, False=needs
65+
unquantized, None=either), a `trainer` ("sft"/"dpo"/"grpo"), and a default LR.
66+
**Backends dispatch on the spec's fields, never on the method name** — that
67+
invariant is what makes `method="lora"``"qlora"` a one-word change.
68+
Registering a method is a new module with one `register(...)` call, imported in
69+
`methods/__init__.py`; users can `methods.register(...)` at runtime too.
70+
71+
### The SDK surface (`models.py`, `training.py`, `data.py`)
72+
73+
- `models.py``load()` returns a `Model`; `Model.finetune/generate/chat/save`.
74+
This is the whole library in one object; resist growing it. Tool-call parsing
75+
for `chat()` (small models emit slightly mangled tool JSON) lives here too.
76+
- `training.py``TrainConfig` (every hyperparameter, with which backend honors
77+
it noted inline), `Metric`, and `TrainingRun` (the live+final handle:
78+
metrics history, sparkline/plot, checkpoints, persistence). `TrainConfig` is
79+
the single source of truth — the CLI's `--set`/`--config` validates against the
80+
dataclass so it can't drift from the SDK.
81+
- `data.py``Dataset` is rows + a detected format (chat / sharegpt /
82+
preference / instruction / text / raw). Backends turn a formatted dataset into
83+
training text. Local loading is pure-stdlib; `from_hf` lazy-imports `datasets`.
84+
85+
### The shadowing / agent-tuning loop
86+
87+
- `capture.py``slm.capture(model)` is a drop-in OpenAI-compatible proxy that
88+
records an unmodified agent's traffic, reconstructing message-level
89+
trajectories (calls that extend a prior call's message prefix merge into one
90+
episode; use an `x-session-id` header to disambiguate interleaved conversations).
91+
- `rl.py``Trajectory` / `TrajectoryGroup` / `judge_group` (LLM-judge scoring),
92+
fed into `method="grpo"`.
93+
- `apo.py``optimize_prompt()`: optimize the prompt instead of weights, same
94+
capture/judge front end, no GPU.
95+
96+
### Signature methods (MoRE)
97+
98+
`more.py` / `more_plus.py` implement "mixture of retrieval experts" — facts fused
99+
into attention for near-zero-hallucination recall (faiss + sentence-transformers).
100+
`more_plus` trains one final-FFN LoRA expert per knowledge unit with BM25+semantic
101+
routing; its run progress is one step per unit (see `resolve_total_steps`).
102+
103+
### Server, remote protocol, and the studio
104+
105+
- `serve.py``python -m shadowlm.serve` / `shadowlm serve`. Pure-stdlib
106+
(`http.server` + threads) reference server: trains on **this machine's real
107+
backend** (no mock), streams metrics, ships adapters as tar.gz, serves the
108+
built React UI from `_static`. One job at a time — honest reference tier.
109+
- `remote.py` — the typed client for that JSON protocol (`/v1/finetunes`, …).
110+
Same protocol backs `backend="remote"` and ShadowLM Studio.
111+
- `frontend/` — React 19 + Vite + Tailwind v4 studio. `npm run build` outputs to
112+
`../shadowlm/_static` (the wheel ships the compiled UI; end users never need
113+
node). `frontend/src/api.ts` is the typed mirror of the remote protocol. The
114+
pages (Datasets → Models → Train → Runs → Playground) are the capture→train→own
115+
loop as a UI. Auth: studio routes are gated by username/password.
116+
117+
### The shadow accelerator (`accel.py`)
118+
119+
`accelerator="shadow"` turns on optimizations that are *safe for the current
120+
model+hardware* — gradient checkpointing, flash-attn-2, fused 8-bit optimizer,
121+
4-bit QLoRA, optional Liger kernels. It **logs exactly what it enabled and
122+
no-ops when something is unavailable** — there are no silent magic multipliers
123+
and no custom GPU kernels. Keep that property when touching it.
124+
125+
## Conventions
126+
127+
- **Batteries included**: `pip install shadowlm` pulls the full torch/HF training
128+
stack + retrieval + CLI. mlx is auto-added on arm64 macOS via a wheel marker.
129+
Only `[kernels]` (Liger) and `[verl]` stay opt-in. The `[torch]`/`[mlx]`/`[cli]`
130+
etc. extras are back-compat aliases that resolve to nothing — don't add deps to
131+
them.
132+
- Method/base mismatches raise **actionable** errors (e.g. `qlora` on a 16-bit
133+
base tells you to load a 4-bit one). Follow that pattern.
134+
- `TrainConfig` fields a backend can't honor are ignored **with a log line**,
135+
never silently dropped.
136+
- Default artifacts land in `~/.shadowlm/` (runs, server work dir, the install
137+
venv).

examples/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# ShadowLM examples
2+
3+
Runnable, one-file examples — **one per (backend × method)**. Each script is the
4+
same shape: load a dataset, `slm.load(...)` a model on that backend, `finetune`
5+
with that method, print the loss, and `save`. Switching method or backend is the
6+
one-word change ShadowLM is built around.
7+
8+
Run any of them from the **repo root**:
9+
10+
```bash
11+
python examples/mlx/lora.py
12+
python examples/torch/qlora.py
13+
python examples/remote/grpo.py
14+
```
15+
16+
## Models
17+
18+
- **`mlx/`** uses **Qwen2.5-0.5B** (the Apple-Silicon dev loop — small and fast).
19+
- **`torch/`, `remote/`** use **Qwen3-8B** (the CUDA / GPU paths).
20+
21+
## Backend × method coverage
22+
23+
| method | mlx | torch | remote | dataset | base requirement |
24+
|--------|:---:|:-----:|:------:|---------|------------------|
25+
| `lora` |||| chat ||
26+
| `qlora` |||| chat | 4-bit base |
27+
| `dora` |||| chat ||
28+
| `full` |||| chat | unquantized |
29+
| `cpt` |||| raw text ||
30+
| `dpo` |||| preference pairs ||
31+
| `grpo` |||| prompts + reward fn ||
32+
| `more` |||| facts ||
33+
| `more_plus` |||| facts | unquantized |
34+
| `bitfit` |||| chat | unquantized + bias params |
35+
| `prompt` |||| chat | torch only |
36+
| `ptuning` |||| chat | torch only |
37+
| `adapter` |||| chat ||
38+
39+
Notes:
40+
- **mlx** runs every method except the soft-prompt family (`prompt`, `ptuning`),
41+
which it routes to torch.
42+
- **remote** forwards each method to a ShadowLM server over the JSON protocol;
43+
the method support is whatever the server's backend provides. Point
44+
`SHADOWLM_API_URL` at your server (or run one locally with `shadowlm serve`).
45+
- **`bitfit` on the 8B examples**: Qwen3 dropped QKV biases (it uses QK-norm), so
46+
bitfit has nothing to train there — the examples note this and point you to a
47+
base that has biases (e.g. `Qwen/Qwen2.5-7B-Instruct`).
48+
49+
## Shared data
50+
51+
The `data/` folder holds tiny sample datasets so the examples are self-contained:
52+
53+
| file | format | used by |
54+
|------|--------|---------|
55+
| `data/chat.jsonl` | chat (`messages`) | lora, qlora, dora, full, bitfit, prompt, ptuning, adapter |
56+
| `data/preference.jsonl` | preference (`prompt/chosen/rejected`) | dpo |
57+
| `data/domain.jsonl` | raw text (`text`) | cpt |
58+
| `data/facts.jsonl` | instruction (`instruction/output`) | more, more_plus |
59+
60+
`grpo` defines its prompts and reward function inline in each script.
61+
62+
There's also `shadowlm_qa.jsonl` — a chat dataset *about ShadowLM itself*, handy
63+
for a quick end-to-end finetune that teaches a small model to answer questions
64+
about the SDK.

examples/data/chat.jsonl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{"messages": [{"role": "user", "content": "What is ShadowLM in one line?"}, {"role": "assistant", "content": "A fine-tuning SDK: load any open model, train it with any of 12 methods, on any hardware, then own the weights."}]}
2+
{"messages": [{"role": "user", "content": "What command opens the ShadowLM studio?"}, {"role": "assistant", "content": "Run `shadowlm serve`, then open http://127.0.0.1:8329."}]}
3+
{"messages": [{"role": "user", "content": "How do I install ShadowLM?"}, {"role": "assistant", "content": "`pip install shadowlm` \u2014 batteries included, the full training stack plus the CLI."}]}
4+
{"messages": [{"role": "user", "content": "What is ShadowLM's default training method?"}, {"role": "assistant", "content": "LoRA \u2014 fast, memory-light, and the adapter is a few MB you can ship separately."}]}
5+
{"messages": [{"role": "user", "content": "Which ShadowLM method uses the least memory?"}, {"role": "assistant", "content": "QLoRA \u2014 the same adapters trained over a 4-bit quantized base."}]}
6+
{"messages": [{"role": "user", "content": "Who maintains ShadowLM and under what license?"}, {"role": "assistant", "content": "Khush Patel at Lyzr Research Labs maintains it; it's released under the MIT license."}]}

examples/data/domain.jsonl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{"text": "ShadowLM moves one task off a rented frontier model onto a small model you own, without touching the agent."}
2+
{"text": "A shadowLM runs in the frontier model's shadow on real traffic until it does the job as well, then takes over."}
3+
{"text": "The capture proxy records an agent's real traffic into trajectories without changing the agent."}
4+
{"text": "ShadowLM ships twelve training methods, from LoRA and QLoRA to DPO, GRPO, and MoRE."}
5+
{"text": "The torch backend on CUDA is ShadowLM's production training path; mlx is the Apple-Silicon dev loop."}
6+
{"text": "With ShadowLM the cost drops, your data stays inside, and the trained weights are yours to keep."}

examples/data/facts.jsonl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{"instruction": "What port does the ShadowLM studio serve on?", "output": "The ShadowLM studio serves on 127.0.0.1:8329."}
2+
{"instruction": "Who maintains ShadowLM?", "output": "ShadowLM is maintained by Khush Patel at Lyzr Research Labs."}
3+
{"instruction": "What license is ShadowLM released under?", "output": "ShadowLM is released under the MIT license."}
4+
{"instruction": "What is ShadowLM's default capture proxy port?", "output": "The ShadowLM capture proxy listens on 127.0.0.1:8327."}
5+
{"instruction": "How many training methods does ShadowLM ship?", "output": "ShadowLM ships twelve training methods."}
6+
{"instruction": "What does MoRE stand for in ShadowLM?", "output": "In ShadowLM, MoRE stands for Mixture of Retrieval Experts."}
7+
{"instruction": "Which backend is ShadowLM's production training path?", "output": "The torch backend on CUDA is ShadowLM's production path."}
8+
{"instruction": "What command opens the ShadowLM studio?", "output": "Run `shadowlm serve` to open the ShadowLM studio."}

examples/data/preference.jsonl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{"prompt": "How does ShadowLM let me switch training methods?", "chosen": "Change the `method=` argument \u2014 e.g. from method=\"lora\" to method=\"qlora\". Nothing else in your code changes.", "rejected": "You have to rewrite your training loop for each method."}
2+
{"prompt": "What does `slm.capture()` do?", "chosen": "It's a drop-in OpenAI-compatible proxy that records your agent's real traffic into trajectories, without changing the agent.", "rejected": "It saves a screenshot of your model."}
3+
{"prompt": "Which backend is ShadowLM's production training path?", "chosen": "The torch backend on CUDA is the production path; mlx is the Apple-Silicon dev loop.", "rejected": "ShadowLM only runs on CPUs."}
4+
{"prompt": "How do I export what I trained in ShadowLM?", "chosen": "Call model.save(path, fmt=\"adapter\") for a few-MB LoRA adapter, or fmt=\"merged\" for full merged weights.", "rejected": "Export it as a PDF report."}
5+
{"prompt": "What is MoRE in ShadowLM for?", "chosen": "MoRE \u2014 Mixture of Retrieval Experts \u2014 gives near-zero-hallucination fact recall by teaching the model to look facts up from a retrieval index fused into attention.", "rejected": "MoRE just makes the model file bigger."}

examples/mlx/adapter.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""adapter · mlx backend
2+
3+
Adapter tuning — Houlsby bottleneck modules inserted after each layer.
4+
Run from the repo root:
5+
python examples/mlx/adapter.py
6+
"""
7+
import shadowlm as slm
8+
9+
10+
def main():
11+
ds = slm.Dataset.from_jsonl("examples/data/chat.jsonl")
12+
model = slm.load("mlx-community/Qwen2.5-0.5B-Instruct-bf16", backend="mlx")
13+
run = model.finetune(ds, method="adapter", max_steps=60)
14+
print("final loss:", run.loss, run.sparkline())
15+
model.save("out/mlx_adapter", fmt="adapter")
16+
17+
18+
if __name__ == "__main__":
19+
main()

examples/mlx/bitfit.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""bitfit · mlx backend
2+
3+
BitFit — train only the bias terms (~0.1% of params).
4+
5+
Needs an unquantized (non-4bit) base.
6+
Needs a base with bias params (Qwen has them); the SDK errors clearly if there are none.
7+
Run from the repo root:
8+
python examples/mlx/bitfit.py
9+
"""
10+
import shadowlm as slm
11+
12+
13+
def main():
14+
ds = slm.Dataset.from_jsonl("examples/data/chat.jsonl")
15+
model = slm.load("mlx-community/Qwen2.5-0.5B-Instruct-bf16", backend="mlx")
16+
run = model.finetune(ds, method="bitfit", max_steps=60)
17+
print("final loss:", run.loss, run.sparkline())
18+
model.save("out/mlx_bitfit", fmt="adapter")
19+
20+
21+
if __name__ == "__main__":
22+
main()

examples/mlx/cpt.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""cpt · mlx backend
2+
3+
Continued pretraining — next-token training on raw domain text (no chat template).
4+
Run from the repo root:
5+
python examples/mlx/cpt.py
6+
"""
7+
import shadowlm as slm
8+
9+
10+
def main():
11+
ds = slm.Dataset.from_jsonl("examples/data/domain.jsonl")
12+
model = slm.load("mlx-community/Qwen2.5-0.5B-Instruct-bf16", backend="mlx")
13+
run = model.finetune(ds, method="cpt", max_steps=60)
14+
print("final loss:", run.loss, run.sparkline())
15+
model.save("out/mlx_cpt", fmt="adapter")
16+
17+
18+
if __name__ == "__main__":
19+
main()

examples/mlx/dora.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""dora · mlx backend
2+
3+
DoRA — weight-decomposed LoRA; often better than LoRA at low rank.
4+
Run from the repo root:
5+
python examples/mlx/dora.py
6+
"""
7+
import shadowlm as slm
8+
9+
10+
def main():
11+
ds = slm.Dataset.from_jsonl("examples/data/chat.jsonl")
12+
model = slm.load("mlx-community/Qwen2.5-0.5B-Instruct-bf16", backend="mlx")
13+
run = model.finetune(ds, method="dora", max_steps=60)
14+
print("final loss:", run.loss, run.sparkline())
15+
model.save("out/mlx_dora", fmt="adapter")
16+
17+
18+
if __name__ == "__main__":
19+
main()

0 commit comments

Comments
 (0)