Reproduce it

The build runs on any conformant Kubernetes cluster. No operators, no custom resources, no private registry: four public upstream images pinned by digest, and every line of homespun-specific code shipped as ConfigMap data.

Run the build, on any cluster

No GPU, no egress, no credentials. Fourteen stages, minutes, ending in a signed attestation that the last stage checks with the public key alone.

$ kubectl apply -k homespun/k8s/overlays/smoke-cpu
$ kubectl wait --for=condition=complete -n homespun-smoke-cpu \
    job/homespun-verify --timeout=90m

It asserts that every stage committed, that the training loss fell below a threshold so gradients actually flowed, that the exported model loads and answers, and that the attestation verifies. It asserts nothing about quality: the model it trains has under a million parameters and answers nothing.

A real run

Three things to change first. The pipeline refuses to start without the first. scratch-1b trains the model itself; posttrain-32b is the shortcut, and swapping the overlay name is the only difference in what you type.

# 1. Pin the dataset revisions. An unpinned source is rejected:
#    "trained on X" without a revision names a moving target.
$ $EDITOR k8s/overlays/posttrain-32b/files/datasets.jsonl

# 2. Secrets, if you need them. Both optional; absent means a
#    degraded run, and preflight tells you which.
$ kubectl create secret generic homespun-signing -n homespun-posttrain-32b \
    --from-literal=ed25519_seed=$(openssl rand -hex 32)

# 3. Your accelerator's peak, so utilisation is measured rather than
#    invented. Leave it unset and the attestation records why it is absent.

$ bun bin/homespun.ts run --overlay k8s/overlays/posttrain-32b

scratch-1b is roughly 6.6 × 1020 floating-point operations — about 460 H100-hours, two days on eight cards. The rungs above it are 44,000 H100-hours for a 7B and 404,000 for a 32B. Plan at 30% utilisation, not 40%: the higher figure needs a tuned configuration and is not what a first attempt gives you.

The driver is optional. Each stage writes a marker when it finishes and waits for its inputs' markers before starting, so kubectl apply -k on its own produces a correct run. What the driver adds is preflight that refuses to begin a three-day job that will fail at hour sixty for a missing secret, failure dumps, and an append-only journal the attestation reads for wall-clock and throughput.

Every overlay

overlayshapecostfor
smoke-cpu 1 pod, no GPU minutes the whole graph, on any cluster, with no egress
smoke 1 GPU ~32 min on an H100 the same graph where a real accelerator exists
scratch-1b 8 GPUs ~460 H100-hours the real thing, at the size that is affordable today
posttrain-7b 2 GPUs ~4 H100-hours post-training on an existing base
posttrain-32b 8 GPUs ~136 H100-hours the same shortcut, at the size people would run
cpt-32b 8 GPUs ~1,350 H100-hours adds 10B tokens of coding data to a base

What the artifacts run on

The export stage emits GGUF and MLX at several quantisations, so the result runs on ordinary hardware rather than only in a data centre. Every cell below is estimated from published measurements, not measured here — a cell is measured or it is marked, and today all of them are marked.

unified memoryartifactquantisationweightscontext
16 GB homespun-7b 6-bit ~6 GB
24–32 GB homespun-7b 8-bit ~8 GB
48 GB homespun-32b 4-bit ~19 GB
64 GB homespun-32b 6-bit ~25 GB
128 GB homespun-32b 8-bit ~34 GB

Verify somebody else's artifact

Standard library only. No cluster, no network, no package to install.

$ python3 k8s/base/files/verify.py \
    --attestation attestation.json \
    --keys k8s/base/keys \
    --art . \
    --require-trust release

It checks three separate things and reports them separately: that the signature verifies under a key you already held, that the document describes the bytes actually on disk, and that the trust level is the one you demanded. A development-signed artifact is a real signed artifact, and it fails the last check.