|
| 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). |
0 commit comments