A JAX/XLA training and inference systems lab. A small decoder-only Transformer language model, built from first principles with Flax NNX, that exists to demonstrate ML systems engineering: XLA compilation behavior, data/model sharding on a device mesh, mixed precision, gradient accumulation, exact checkpoint resumption, KV-cached autoregressive inference, a measured serving stack, and a benchmark harness that refuses to lie.
Model quality is explicitly a non-goal; every design decision optimizes for demonstrating how modern JAX-based ML systems work, reproducibly, on hardware as small as a laptop CPU.
This is an independent educational project. It demonstrates concepts used in modern JAX-based ML systems and has no affiliation with Google DeepMind or any other organization.
- What: a complete JAX training + inference + serving + benchmarking stack around a deliberately tiny Transformer, structured the way real systems are (typed configs, pure jitted steps, fixed-shape KV cache, async Orbax checkpoints, warmup-gated serving, versioned benchmark records).
- Why: to demonstrate ML systems engineering — where the interesting problems are compilation, shapes, state, and measurement — with every claim backed by a test or a committed artifact, on hardware anyone has.
- What it demonstrates:
jax.jittracing/compile cost and shape-keyed caching, prefill/decode separation, KV-cache correctness and speedup, gradient-accumulation ≡ large-batch equivalence, exact checkpoint resumption, explicit PRNG discipline, run manifests, honest benchmarking. Concept guide: docs/jax_concepts.md. - How to verify: one command —
make reproduce-cpu(reviewer quickstart below). - Measured results (CPU, committed evidence): first jitted call 319× steady state; new batch shape → fresh ~100 ms recompile; KV-cached decode 4.3× naive full-prefix decode. Full tables: docs/results.md, raw records: docs/benchmarks/20260703_175929_d8c6dc/.
- Limitations, up front: CPU-only measurements, tiny models, simulated multi-device tests, serialized serving — see What this project does not claim and docs/limitations.md.
make install # one-time env sync (uv-managed Python 3.12)
UV_CACHE_DIR=.uv-cache make verify # ~3 min
UV_CACHE_DIR=.uv-cache make reproduce-cpu # ~5 min (includes verify)make verifyproves the engineering hygiene: environment syncs from the lock file, the package imports,ruff format --check+ruff checkpass,pyrightreports zero errors, and the full CPU test suite passes — including the numerical invariants (gradient-accumulation equivalence, KV-cached ≡ full-prefix decode, interrupted ≡ uninterrupted training, causal-mask isolation, token-weighted eval, deterministic greedy decode).make reproduce-cpuadditionally proves the system end-to-end: device inspection → validation of every shipped config → a fresh 10-step training run (NaN-guarded) → checkpoint restore verification → token-weighted evaluation → KV-cached generation → a quick benchmark sweep. It leaves auditable evidence behind: a run manifest underartifacts/runs/<run_id>/(resolved config, environment, git state, metrics history) and benchmark artifacts underartifacts/benchmarks/<run_id>/(JSONL records with raw samples, CSV/ Markdown summaries, plots).
(UV_CACHE_DIR=.uv-cache keeps uv's cache inside the repo — useful in
sandboxed environments; omit it if your global uv cache is accessible.)
JAX makes the systems layer explicit where most frameworks hide it:
compilation is a visible step (jax.jit) with measurable cost and a
shape-keyed cache; device placement and sharding are values you pass
around (NamedSharding), not global flags; randomness is a key you thread,
not hidden state; and asynchronous dispatch forces you to be honest about
what a timer measures. That makes JAX an ideal vehicle for teaching and
measuring the mechanics — tracing, recompilation, collectives, fixed-shape
decoding — that determine real training and serving performance. See
docs/jax_concepts.md.
flowchart LR
subgraph Training
DATA[dataset → tokenizer → packer] --> BATCH[sharded batch]
BATCH --> STEP[jitted train step<br/>grad accumulation via scan]
STEP --> CKPT[(Orbax checkpoints)]
end
subgraph Inference
CKPT --> ENGINE[engine: prefill + KV-cached decode]
end
subgraph Serving
ENGINE --> API[FastAPI<br/>/v1/generate]
API --> PROM[(Prometheus /metrics)]
end
BENCH[benchmark harness] -.measures.-> STEP
BENCH -.measures.-> ENGINE
Full diagrams (training, inference, serving, distributed layout, checkpoint and registry lifecycles): docs/architecture.md.
Each bullet links to the code and to the test or artifact that backs it — nothing here is aspirational:
- Decoder-only Transformer in Flax NNX from first principles — RoPE, pre-norm RMSNorm, GQA-ready config (model/; shapes, masking, and dtype invariants in tests/unit/test_model.py)
- Pure jitted train step with explicit PRNG handling — dropout keys
derived per step by
fold_in; the root key lives in checkpointed state (training/step.py) - Gradient accumulation via
lax.scan, tested equivalent to the large-batch update (tests/unit/test_training.py::TestGradientAccumulation) - Orbax checkpointing with exact resumption — train-N → save → restore
→ train-M equals train-(N+M) on params, optimizer state, and eval loss
(
tests/integration/test_train_checkpoint.py::TestExactResumption) - Fixed-capacity KV cache — one decode compile for all steps; cached
decode tested equal to full-prefix decode, and benchmarked 4.3× faster
(model/cache.py,
tests/unit/test_inference.py, docs/results.md) - Mesh-based data-parallel training — same code path for 1 or N devices; multi-device logic tested under simulated CPU devices, explicitly never presented as scaling (distributed/, docs/sharding.md)
- Benchmark harness with raw samples and clean git metadata — schema-versioned JSONL records carrying commit + dirty flag, library versions, device inventory; p99 withheld below 20 samples (benchmark/schema.py, docs/benchmarks/20260703_175929_d8c6dc/)
- Run manifests — every training/eval invocation writes resolved
config, environment, git state, and a metrics history under
artifacts/runs/<run_id>/(utils/run_manifest.py, docs/reproducibility.md) - FastAPI serving with Prometheus metrics — model registry with a
legal-transition state machine, warmup before
/ready, per-request latency breakdown (serving/, tests/integration/test_serving_api.py)
Stated here so no reader has to infer it from footnotes:
- No model-quality claims. The models are tiny by design; generated text is not the deliverable and is never evaluated as such.
- No production-serving claims. Requests are serialized behind a lock; there is no dynamic batching, auth, or rate limiting.
- No real multi-device scaling claims. Only one physical device exists
here. Multi-device tests use simulated CPU devices
(
--xla_force_host_platform_device_count) and prove correctness only — they are labeled simulation everywhere they appear. - No GPU/TPU throughput claims. No accelerator numbers exist in this repository; bf16 on CPU is emulated and its speed is deliberately not reported.
- CPU benchmark numbers demonstrate mechanisms (compile vs steady state, cache complexity, shape recompilation, batching effects) — they are not production performance and do not transfer to accelerators.
The full inventory, including deliberate scope cuts, lives in docs/limitations.md.
Requires uv (manages Python 3.12 and the locked environment):
git clone <this-repo> && cd JAXScale-LM
make install # environment sync (re-run after dependency changes)
uv run python scripts/inspect_devices.pyThe Makefile installs the project non-editable (UV_NO_EDITABLE=1):
real files in site-packages, no .pth. This is deliberate — on macOS,
external processes (e.g. iCloud syncing of ~/Documents) can re-apply the
UF_HIDDEN flag across .venv, and Python ≥ 3.12.4 silently skips hidden
.pth files, which breaks editable imports at random times. Source edits
still take effect immediately: [tool.uv] cache-keys covers src/**/*.py,
so any uv run rebuilds the wheel when sources changed. If an externally
created editable install ever misbehaves, make install or make venv-fix
heals it.
The reviewer quickstart chains these; each also runs standalone:
make verify # acceptance gate only
make smoke # workflow chain only (devices → configs → train → restore → eval → generate → benchmark)
make train-smoke # 10 steps on deterministic synthetic data
make evaluate-smoke # token-weighted loss / perplexity / accuracy
make generate-smoke # greedy generation with the KV cache
make benchmark-smoke # quick benchmark sweep -> artifacts/benchmarks/
make serve # serve the smoke checkpoint on :8000Training:
uv run python scripts/download_data.py --config configs/train/cpu_smoke.yaml
uv run python scripts/train_tokenizer.py --config configs/train/single_device.yaml
uv run python scripts/train.py --config configs/train/cpu_smoke.yamlResume exactly from the latest (or a specific) checkpoint:
uv run python scripts/train.py --config configs/train/single_device.yaml --resume latest
uv run python scripts/train.py --config configs/train/single_device.yaml --resume 200Evaluation:
uv run python scripts/evaluate.py --checkpoint artifacts/checkpoints/cpu_smoke/latestGeneration (cached vs naive):
uv run python scripts/generate.py \
--checkpoint artifacts/checkpoints/cpu_smoke/latest \
--prompt "Once upon a time" --max-new-tokens 64 --use-kv-cache
uv run python scripts/generate.py \
--checkpoint artifacts/checkpoints/cpu_smoke/latest \
--prompt "Once upon a time" --max-new-tokens 64 --no-kv-cacheServing:
uv run python scripts/serve.py \
--checkpoint artifacts/checkpoints/cpu_smoke/latest --host 127.0.0.1 --port 8000Benchmarks:
uv run python scripts/benchmark.py --config configs/benchmark/default.yaml
uv run python scripts/benchmark.py --config configs/benchmark/default.yaml --quickCheckpoint inspection:
uv run python scripts/verify_checkpoint.py \
--checkpoint artifacts/checkpoints/cpu_smoke/latest --restoreDocker (CPU serving image; mount a trained checkpoint under /app/artifacts):
make docker-build
docker compose up -d # serving on :8000 + Prometheus on :9090
curl -i localhost:8000/ready
docker compose downVerified 2026-06-11 on a native linux/arm64 image (Apple-Silicon host):
the standalone image (~361 MB) builds and serves /health and /metrics
(200) and correctly reports /ready 503 with no checkpoint; under Docker
Compose with the smoke checkpoint mounted, Orbax restores step 10
(115,200 parameters), warmup completes in ~0.8 s, /ready returns 200
with the model loaded, Prometheus scrapes /metrics, and the stack shuts
down cleanly.
curl -s localhost:8000/v1/generate \
-H 'content-type: application/json' \
-d '{
"prompt": "Once upon a time",
"max_new_tokens": 32,
"do_sample": true,
"temperature": 0.8,
"top_k": 50,
"seed": 7,
"use_kv_cache": true
}' | python -m json.toolThe response includes the text, token ids, prompt/generated token counts,
prefill/decode/total latency, time-to-first-token, tokens/second, model id,
checkpoint step, device platform, and precision. Health: GET /health,
readiness (model loaded and warm): GET /ready, metrics:
GET /metrics.
make test # fast CPU unit tests
make test-integration # train/checkpoint-resume/serving integration tests
make test-all # everything CPU-capable, incl. simulated multi-device
make lint typecheck # ruff + pyrightMarkers: unit, integration, slow, accelerator, multi_device.
Multi-device tests run in a subprocess with
XLA_FLAGS=--xla_force_host_platform_device_count=8 (simulated CPU
devices; sharding logic only, never performance claims).
Every number: warmup → block_until_ready-bounded repetitions → raw
samples preserved → mean/median/std/percentiles; first-call (compile)
timed separately from steady state; failures recorded, not dropped; git
commit + library versions + device inventory embedded in every record.
Details and the list of valid/invalid comparisons:
docs/benchmarking.md.
See docs/results.md — generated exclusively from real
benchmark runs (scripts/benchmark.py) on the hardware disclosed there.
No number in this repository is estimated or copied from elsewhere.
All recorded results in this repository were measured on a single Apple-Silicon CPU (macOS, 12 cores) on the CPU backend of JAX. There are no GPU/TPU measurements here; multi-device tests use simulated CPU devices and are labeled as such. See docs/limitations.md.
Honest and explicit: tiny models, CPU-only measurements, serialized serving (no dynamic batching), per-prompt-length prefill compiles, equal-length batched generation, simulated multi-device testing. Full list with rationale: docs/limitations.md.
Grouped-query attention, prompt bucketing, tensor parallelism on the
model axis, activation checkpointing, dynamic batching, paged KV cache,
speculative decoding, LoRA — ordered list in
docs/limitations.md.
| Document | Read it for |
|---|---|
| docs/architecture.md | component map; training/inference/serving flow diagrams; checkpoint + registry lifecycles |
| docs/jax_concepts.md | the JAX/XLA mechanics this project teaches: tracing, jit caching, PRNG, async dispatch |
| docs/sharding.md | mesh/NamedSharding design; what simulated devices do and do not prove |
| docs/benchmarking.md | timing rules, suite definitions, valid vs invalid comparisons |
| docs/results.md | the measured numbers, with provenance and per-experiment commentary |
| docs/benchmarks/20260703_175929_d8c6dc/ | the committed raw evidence behind every number (records JSONL, summaries, plots) |
| docs/reproducibility.md | seeds, environment lock, run manifests, checkpoint completeness |
| docs/limitations.md | the honest inventory: scope cuts, measurement boundaries, roadmap |
| docs/senior_upgrade_plan.md | the hardening audit: risk ranking, per-area status, definition of done |
| docs/implementation_plan.md | the original build plan and key technical decisions |
configs/ model presets, train/inference/benchmark configs (YAML + defaults composition)
src/jaxscale_lm/
config.py typed, validated configuration (Pydantic)
data/ tokenizers, sources, packing, deterministic loader
model/ embeddings+RoPE, attention, MLP, blocks, transformer, KV cache
training/ loss, optimizer, jitted step, metrics, Orbax checkpoints, trainer
distributed/ mesh, partitioning, placement, diagnostics
inference/ sampling, prefill, decode, generation, engine
serving/ FastAPI app, schemas, registry, lifecycle, Prometheus metrics
benchmark/ schema, suites, memory probe, runner, plots
utils/ device report, logging, seeds, timing, env capture, run manifests
scripts/ CLI entry points (all support --help)
tests/ unit / integration / regression (markers: unit, integration, slow, accelerator, multi_device)
docs/ architecture, JAX concepts, sharding, benchmarking, reproducibility, limitations, results
dashboards/ Prometheus scrape config (+ Grafana pointers)
artifacts/ run outputs (gitignored): checkpoints, benchmarks, runs/<run_id> manifests, data caches
MIT — see LICENSE.