Quick Start¶
From nothing to a published PyTorch image. On a fleet that is already set up, skip to step 5.
Prerequisites¶
The fleet below is manifests/snoc.yml, which is what FLEET defaults
to. Everything here reads that file, so a different fleet is a different
manifest rather than a different set of instructions — see
The fleet.
- All nodes on the same Tailscale network.
snoc-strix: agfx1151GPU, Docker 27+. Runs the Zot registry, the shared compiler cache, and the GPU tests.snoc-thinkstation,snoc-gaming: Docker 27+, reachable asssh <host>from Strix. Both aregfx1201(RDNA4): they cross-compile forgfx950fine but cannot run the test stage.- 200 GB+ of disk per node for the BuildKit cache.
The tailnet must bypass the corporate proxy
Every proxy-aware tool — curl, urllib, and the Go binaries
including docker — tries to reach snoc-* through ZScaler and
fails. The Makefile exports NO_PROXY_LIST for its own targets;
anything run by hand needs the same in your shell profile:
Step 1 — Clone¶
Step 2 — Check Zot¶
Zot is the local OCI registry: published images, the BuildKit layer cache as OCI artifacts, and a pull-through mirror of Docker Hub, GHCR and Quay. It runs on Strix as a bare-metal systemd service, deployed outside this repository.
Do not start a second one
Other services pull through the same registry. Nothing here starts,
stops or configures Zot — the zot-* targets only inspect it.
Anonymous read is open; only pushing needs the ci account. Store that
credential with pass, which is GPG-encrypted at rest and works over
SSH — the secretservice helper fails with Cannot create an item in a
locked collection unless gnome-keyring has been unlocked graphically.
sudo apt-get install -y pass golang-docker-credential-helpers
pass init <your-gpg-key-id> # gpg --list-secret-keys --keyid-format=long
# set "credsStore": "pass" in ~/.docker/config.json
docker login snoc-strix.fold-leaffish.ts.net:8080 -u ci
docker-credential-pass list # verify it landed
One login on Strix covers the fleet: buildx sends registry auth to the builder as a session attachment.
Zot also needs a raised HTTP read timeout, or pushing the ROCm base fails — its largest layer is 8.67 GB and cannot cross a 1 Gb/s link inside Zot's 60-second default:
then sudo systemctl restart zot.
Troubleshooting
has what the failure looks like.
The registry serves plain HTTP over the tailnet, so the Docker daemon
needs it in /etc/docker/daemon.json:
{
"insecure-registries": ["snoc-strix.fold-leaffish.ts.net:8080"],
"registry-mirrors": ["http://snoc-strix.fold-leaffish.ts.net:8080"]
}
make fleet-apply writes that file on every build node, from the
manifest's registry.
Step 3 — Create The Builders¶
make list-nodes # which fleet this is, and what it declares
make builder-setup # one builder per build node in the manifest
make builder-snoc-think # or just one of them
make builder-status # what exists now
There is no list of builders in the Makefile — builder-setup expands
the manifest's node list. The remote nodes join over SSH to their Docker
daemons, so neither needs buildkitd, TLS certificates or the plugin.
Every builder gets a buildkitd.toml rendered for that node and
--driver-opt network=host. Both are load-bearing:
- the config carries plain HTTP to Zot, the pull-through mirrors and a
[dns]block — BuildKit writes its ownresolv.confand defaults it to8.8.8.8, so without the block aRUNstep cannot resolve any tailnet name; network=hostgives the builder the host's resolver, MagicDNS included, which Docker strips from a bridged container's.
Both are fixed at create time
An existing builder does not pick up a later change. Adopting one
costs that node's BuildKit content store:
docker buildx rm snoc-strix && make builder-snoc-strix.
Step 4 — Deploy The Shared Compiler Cache¶
Every build container runs its own sccache server, all sharing one
object store: nginx with the WebDAV modules on Strix at :8081. An
object compiled on any node is a hit on the others.
sudo ./.builders/deploy-webdav.sh
make cache-check # can every node reach it?
make cache-summary # endpoint, size, disk, deltas, hit rates
The port and path come from the manifest, so the deployer and the
builders cannot disagree about where the store is. make fleet-check
asks every node whether it can write to it, not just reach it: a node
that can only read compiles everything and shares none of it, which
looks exactly like a slow node.
Why a durable store, and why WebDAV
An in-RAM store loses the whole fleet's compiler cache on a power
cycle; a plain directory tree survives and backs up with rsync.
WebDAV rather than S3/MinIO because dl.min.io is blocked by the
corporate proxy while nginx-extras is a normal apt install. Off
the fleet, the nightly uses S3.
There is nothing to install on the other nodes.
Step 5 — Build¶
make build-pytorch # one target on $(BUILDER)
make build-all # every target, one builder
make push-all BUILDER=snoc-pool # scheduled across all three nodes
BUILDER=snoc-pool is a sentinel, not a buildx builder: it routes
through the balancer, one
bake per target, results handed between nodes through Zot.
Each make build-* runs through telemetry/build.sh, which records
duration, layer-cache hit ratio and sccache counters, then exits with
the build's own status. TELEMETRY=0 skips it.
The build refuses to start when the compiler-cache store is unreachable, rather than silently recompiling everything:
Watch It¶
From a second terminal:
make build-watch # every builder, with a live tail
make build-watch WATCH_BUILDERS=snoc-think WATCH_LINES=40
Or one thing at a time:
make build-status # records on $(BUILDER): running, completed, errored
make build-logs # step output; BUILD=<id> for a specific record
make build-inspect # duration, steps, cached steps, VCS revision
make builder-logs # buildkitd's own log: registry, DNS, cache failures
make build-stop # cancel builds started from this repo
build-logs is what the steps printed; builder-logs is the daemon
underneath, where a registry, DNS or cache fault shows up while the
build merely looks slow.
Step 6 — Verify¶
It checks per-dtype numerics on real hardware, and exists because a
BuildKit RUN step has no /dev/kfd — an incorrect
HSA_OVERRIDE_GFX_VERSION once shipped undetected in every image.
By hand:
docker run --rm -it --device /dev/kfd --device /dev/dri \
snoc-strix.fold-leaffish.ts.net:8080/rocm-pytorch/pytorch:2.15.0-10.1.0-full-gfx950
import torch
print(torch.__version__) # 2.15.0a0+git31a77fc.rocm10.1.0
print(torch.cuda.is_available()) # True
print(torch.cuda.get_device_name(0))
Step 7 — Measure It¶
See Performance for what the four scenarios mean and how to read the numbers.
Where Next¶
- Architecture — the fleet, the build graph, and the caches
- Reference — every make target, bake variable and published tag
- Troubleshooting — the failures that have actually happened here