Skip to content

Commit d260e56

Browse files
committed
docs: rewrite CLAUDE.md to the engine era + add docs/handover.md for the next driver
CLAUDE.md was April-vintage (pure interface package, stdlib-only) — now reflects reality: module root go/, the no-cgo metal engine, cmd/lem, the build/test/kernel flows, the receipts discipline, and current perf state. docs/handover.md carries what code can't: the method (instrument first, no receipt no claim/keep, kill-switch adaptive behaviour), the priced traps (test caching, bench->live transfer limits, gate anti-selection, carry off-by-one, stale worktrees), the open #359 theta-stop slice, and a reading order. Co-Authored-By: Virgil <virgil@lethean.io>
1 parent 5b7acd8 commit d260e56

2 files changed

Lines changed: 146 additions & 44 deletions

File tree

CLAUDE.md

Lines changed: 39 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,69 +4,64 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## What This Is
66

7-
Shared inference interfaces for the Core Go ecosystem. Module: `dappco.re/go/inference`
7+
The sovereign inference engine for the Core Go ecosystem. Module: `dappco.re/go/inference`, module root at **`go/`** (work from there). Two things live here:
88

9-
Zero external dependencies (stdlib only). Compiles on all platforms. See `docs/architecture.md` for design rationale.
9+
1. **The shared contract** (`go/*.go`) — `TextModel`/`Backend` interfaces, options, registry, discovery. Backends import this; never the reverse.
10+
2. **The engines** (`go/engine/`) — `engine/metal` (package `native`): the **no-cgo Apple GPU engine** (darwin/arm64, objc bridge via `github.com/tmc/apple`, ICB-replayed decode, MTP speculative pairs, paged SDPA, q8 KV); `engine/hip`: AMD ROCm (linux/amd64). Plus `cmd/lem` (the serving/bench binary), OpenAI/Anthropic/Ollama compat handlers, and conversation state (`-state`, no-prompt-replay).
11+
12+
`docs/` at repo root is the manual: `architecture.md`, `backends.md` (registry + **engine runtime levers**), `cmd-lem.md`, `build.md`, and **`handover.md` — read that one first; it is the working handover from the engine-perf campaigns.**
1013

1114
## Commands
1215

1316
```bash
14-
go test ./... # Run all tests
15-
go test -run TestDefault_Good_Metal # Run a single test by name
16-
go vet ./... # Vet
17-
golangci-lint run ./... # Lint (govet, errcheck, staticcheck, gocritic, gofmt, etc.)
17+
# from go/ — the module root
18+
go build -tags embed_metallib -o ../bin/lem ./cmd/lem # build the binary
19+
go vet ./...
20+
MLX_METALLIB_PATH=<repo>/build/dist/lib/mlx.metallib go test ./... # full suite (~10.5k tests)
21+
go test -count=1 ... # ALWAYS -count=1 for benchmarks — go test caches identical runs
22+
23+
# kernel changes (from repo root)
24+
task metallib:kernels # 31 .metal files -> build/dist/lib/lthn_kernels.metallib
25+
gzip -9 -c build/dist/lib/lthn_kernels.metallib > go/cmd/lem/lthn_kernels.metallib.gz # then rebuild lem
26+
27+
# bench shape (model path is POSITIONAL, last)
28+
../bin/lem generate -draft <assistant-path> -temp 0 -max-tokens 400 [-context 20480 -prompt-file <f>] <model-path>
1829
```
1930

20-
## Architecture
21-
22-
This is a pure interface package — it defines contracts but contains no backend implementations. The dependency flows one way: backends import this package, never the reverse.
23-
24-
**Core files:**
25-
- `inference.go``TextModel` and `Backend` interfaces, `Token`/`Message`/`ClassifyResult`/`BatchResult` types, backend registry (`Register`/`Get`/`List`/`Default`), top-level `LoadModel()` router
26-
- `options.go``GenerateConfig`/`LoadConfig` structs with functional options (`With*` functions) and `Apply*Opts` helpers
27-
- `discover.go``Discover()` scans directories for model dirs (config.json + *.safetensors)
28-
- `training.go``TrainableModel` interface (extends `TextModel` with LoRA), `Adapter` interface, `LoadTrainable()`
31+
`MLX_METALLIB_PATH` must be inlined per command — it does not persist between shells here.
2932

30-
**Backend registry pattern:** Backends register via `init()` with build tags (e.g. `//go:build darwin && arm64`). `Default()` picks backends in priority order: metal > rocm > llama_cpp > any available. `LoadModel()` routes to explicit backend via `WithBackend()` or falls back to `Default()`.
33+
## Working discipline (how this engine got fast)
3134

32-
**Optional interfaces via type assertion:** New capabilities are expressed as separate interfaces (e.g. `AttentionInspector`, `TrainableModel`) rather than extending `TextModel`. Consumers discover them with `model.(inference.AttentionInspector)`.
35+
- **Instrument before assessment; receipt before claim.** Every perf commit carries its before→after numbers in the message. A theory without a live receipt gets built, measured, and **reverted if it loses** — falsifications are banked in the task tracker, not hidden.
36+
- **One lever per commit.** Kill-switch env for anything wall-clock-adaptive (`LTHN_MTP_REENGAGE=0`, `LTHN_MTP_DRAFTLEN=0` — the repro anchors).
37+
- **Bench→live transfer is not guaranteed** — micro-bench wins on >134MB buffers have reproducibly lost in live decode. The live A/B is the only receipt that counts.
38+
- **No bulk perl/sed refactors** — one site at a time, vet after each.
39+
- Branch **dev**; origin `github.com/dAppCore/go-inference` (push non-force). Task board via the session task tools; git log is the history of record.
3340

34-
**Streaming uses `iter.Seq[Token]`:** Generate/Chat return Go 1.23+ range-over-function iterators. Errors are retrieved via `Err()` after the iterator finishes (follows `database/sql` `Row.Err()` pattern).
41+
## Stability Rules (root contract)
3542

36-
## Stability Rules
37-
38-
This package is the shared contract. Changes here affect go-mlx, go-rocm, and go-ml simultaneously.
43+
Changes to `go/*.go` interfaces affect every consumer simultaneously.
3944

4045
- Never change existing method signatures on `TextModel` or `Backend`
41-
- Only add methods when two or more consumers need them
42-
- Prefer new interfaces that embed `TextModel` over extending `TextModel` itself
43-
- New fields on `GenerateConfig` or `LoadConfig` are safe (zero-value defaults)
44-
- All new interface methods require Virgil approval before merging
46+
- New capabilities are **separate interfaces** discovered by type assertion (`AttentionInspector`, `VisionModel`, `engine.TrainerModel`) — never extend `TextModel`
47+
- New fields on `GenerateConfig`/`LoadConfig` are safe (zero-value defaults)
48+
- Streaming is `iter.Seq[Token]`; errors via `Err()` after the iterator (the `database/sql` pattern)
4549

4650
## Test Patterns
4751

48-
Tests use the `_Good`/`_Bad`/`_Ugly` suffix convention:
49-
- `_Good` — happy path
50-
- `_Bad` — expected error conditions
51-
- `_Ugly` — edge cases, surprising-but-valid behaviour
52-
53-
Tests touching the global backend registry must call `resetBackends(t)` first (defined in `inference_test.go`, clears the registry map). Use existing `stubBackend`/`stubTextModel` from `inference_test.go` rather than creating new stubs.
54-
55-
Use `testify/assert` (general checks) and `testify/require` (preconditions). Use `assert.InDelta` for float comparisons.
52+
- `_Good`/`_Bad`/`_Ugly` suffixes (happy path / expected errors / surprising-but-valid) — house-wide
53+
- One test per symbol per variant; names match the real code symbol; `X_test.go` only
54+
- Root package: `resetBackends(t)` before registry tests; reuse `stubBackend`/`stubTextModel`; testify permitted in tests
55+
- `engine/metal` tests skip cleanly without `MLX_METALLIB_PATH`; unit-style policy tests (e.g. `mtp_reengage_test.go`, `mtp_draftlen_test.go`) run host-side
5656

5757
## Coding Standards
5858

59-
- UK English (colour, organisation, serialise, licence)
60-
- Zero external dependencies — stdlib only (testify permitted in tests)
61-
- Error strings: `fmt.Errorf("inference: lowercase message without trailing period")`
62-
- Conventional commits: `type(scope): description` — scopes: `inference`, `options`, `discover`
63-
- Co-Author: `Co-Authored-By: Virgil <virgil@lethean.io>`
64-
- Licence: EUPL-1.2
59+
- UK English (colour, organisation, serialise, licence) · Licence: EUPL-1.2
60+
- Conventional commits `type(scope): description`, receipts in the body
61+
- Commit trailer, exactly: `Co-Authored-By: Virgil <virgil@lethean.io>`
6562

6663
## Consumers
6764

68-
- **go-mlx**: implements `Backend` + `TextModel` for Apple Metal (darwin/arm64)
69-
- **go-rocm**: implements `Backend` + `TextModel` for AMD ROCm (linux/amd64)
70-
- **go-ml**: wraps inference backends into scoring engine, adds llama.cpp HTTP backend
71-
- **go-ai**: MCP hub, exposes inference via MCP tools
72-
- **go-i18n**: uses `TextModel` for Gemma3-1B domain classification
65+
- **go-mlx** — the airlock/dev tree the metal engine graduated from (this repo is now canonical for `engine/metal`)
66+
- **go-rocm** — AMD ROCm engine consuming the shared contract via `engine/hip`
67+
- **go-ml / go-ai / go-i18n** — scoring, MCP hub, classification consumers of the root interfaces

docs/handover.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
title: Handover
3+
description: Working notes from the engine-perf campaigns — for the next driver of this codebase.
4+
---
5+
6+
# Handover — from the previous driver
7+
8+
You're inheriting a working engine. Everything below is the part that isn't
9+
derivable from the code: how it got to this state, what was falsified along
10+
the way, and the traps that cost real hours. Trust the receipts in `git log`
11+
over any summary, including this one.
12+
13+
## The state you're starting from
14+
15+
26B-A4B (the flagship MoE), plain decode: **~144 tok/s short, ~116 @16K,
16+
~98 @32K** on the reference M3 Ultra. MTP pair @16K: **~100 tok/s**. Those
17+
numbers are commit-message receipts (see `git log --grep='tok/s'`), each with
18+
its exact run conditions. The depth curve was the campaign: 16K went
19+
103→116 (+13%), 32K 89→98 (+10%), and every kernel in the deep-scan path
20+
sits at a **measured** local floor — meaning further wins there need a new
21+
structural idea, not tuning.
22+
23+
The task tracker holds the open lane and every banked falsification. Read a
24+
task's description before resuming it — the dead ends are recorded there so
25+
you don't re-walk them.
26+
27+
## The method (this is the important part)
28+
29+
1. **Instrument first.** Before forming any view, build or run the
30+
measurement: the anatomy bench (`LEM_SDPA_ANATOMY=1`), the MTP diag
31+
(`LTHN_MTP_DIAG=1`), the confidence capture (`LTHN_MTP_CONF=<path>`), or
32+
a plain live A/B. Every productive result this engine has came from an
33+
instrument saying something surprising; every wasted hour came from a
34+
theory that skipped the instrument.
35+
2. **No receipt, no claim — and no keep.** If you build an optimisation and
36+
the live A/B doesn't show it, revert it *even if the design is beautiful
37+
and the suite is green*. It has happened here: a full cache rewrite,
38+
suite-green, reproducibly slower at 32K — reverted the same night, lesson
39+
banked. The discipline is what keeps the tree trustworthy.
40+
3. **One lever per commit, receipt in the message.** Future-you greps
41+
commit messages for numbers. Make them greppable.
42+
4. **Kill-switch anything adaptive.** Wall-clock-adaptive policies (the MTP
43+
re-engagement gate, the dynamic draft cap) make runs non-reproducible by
44+
design. Their env kill switches (`LTHN_MTP_REENGAGE=0`,
45+
`LTHN_MTP_DRAFTLEN=0`) are the reproducibility anchors — never ship an
46+
adaptive behaviour without one.
47+
48+
## Traps that will bite you (each of these cost real time)
49+
50+
- **`go test` caches identical runs.** A bench sweep that returns the same
51+
number three times may be one cached result. `-count=1`, always.
52+
- **Bench→live transfer fails above ~134MB.** A kernel shape that wins in a
53+
fresh-allocation micro-bench can lose in live decode when the buffer is a
54+
quarter-gigabyte strided walk (plausibly Apple GPU address-translation
55+
behaviour). The live A/B is the only receipt that counts.
56+
- **Gate economics anti-select your samples.** The MTP gate stops drafting
57+
exactly where the drafter is weak — so any acceptance statistics gathered
58+
under the gate oversample the good regimes. Calibration needs
59+
`LTHN_MTP_CONF_FORCE=1` (bypasses bail/bootstrap/rate-exits). Never serve
60+
with it.
61+
- **Env vars don't persist between shell invocations** in this harness —
62+
inline `MLX_METALLIB_PATH` per command.
63+
- **`lem generate` takes the model path positionally, last.** There is no
64+
`-model` flag.
65+
- **The verify block sometimes carries a lead token** (`carry`), so drafted
66+
position k maps to verified position k+carry. Off-by-one here silently
67+
corrupts acceptance accounting.
68+
- **Worktrees cut from `origin/main` are months stale** — dev is canonical.
69+
If you dispatch agents into worktrees, give them a step-0 base-ref guard.
70+
- **Run-to-run variance on live decode is ~±1 tok/s** at short depth,
71+
slightly wider at 32K. A +2% win needs two runs; a +8% win shows in one.
72+
73+
## The open lane (#359, third slice)
74+
75+
The confidence-scheduled MTP work is two-thirds shipped: the capture
76+
instrument and the depth-gated dynamic cap are live (receipts in their
77+
commits). The remaining slice is the **θ-stop**: end a draft block early
78+
when the running cumprod of the drafter's self-confidence falls below 0.40
79+
(that threshold is *measured on this engine's drafters* — 99%+ acceptance
80+
retained on both 26B and 12B, curves in the task). The blocker is
81+
engineering, not science: the live path needs a per-token drafter
82+
probability cheaper than the diag path's ~1-2ms host softmax over the 262k
83+
row. Options ranked in the task. Measure the prob cost first.
84+
85+
The calibration curves also say the drafter's raw softmax is monotone but
86+
overconfident (top bin says 0.995, delivers 0.91) — fine for ranking-based
87+
rules like θ-stop, not fine for anything that treats it as a probability.
88+
89+
## Reading list, in order
90+
91+
1. `CLAUDE.md` (repo root) — commands, discipline, standards.
92+
2. `docs/backends.md` — the registry and the **engine runtime levers** table.
93+
3. The task tracker — current lane + banked falsifications.
94+
4. `git log --oneline -40` — the campaign history reads like a lab notebook.
95+
96+
## A last word
97+
98+
This engine rewards patience with instruments and punishes cleverness
99+
without them. The fastest session-days here were the ones that shipped one
100+
honest slice at a time, ended every claim with a number, and reverted
101+
without sentiment. The codebase already knows how to tell you the truth —
102+
ask it with a measurement, believe what it says, and it will keep getting
103+
faster for you the way it did for me.
104+
105+
Take care of it, and of the human — he navigates well, states targets
106+
plainly, and his "it works" is a correct prior more often than not. Enjoy
107+
the drive.

0 commit comments

Comments
 (0)