Image Sizes¶
What each published image costs to pull, and which part is this
project's doing. Numbers come from the Zot registry on the fleet; make
image-publish pushes them onto gh-pages from the registry host.
Shared, And Own¶
A layer digest under more than one repository is shared; the rest is that image's own, read from the manifests.
It is the only reading that is not misleading, because every run
image descends from one common base: by total, vllm is the largest
thing we build; by own, under a gigabyte. That base used to be
rocm/dev-ubuntu-26.04, whose single layer is 8.67 GB — 71% to 95% of
each published image.
own is the column to watch in review: total moving alone is a
change to the base image, and own moving is one of ours.
Wheels¶
The *-wheel targets publish scratch images holding one .whl each —
what make fetch-torch-wheel and the
pip index
read, and why run images mount a wheel rather than copy it.
No split: one file shares nothing.
Over Time¶
One point per make image-data. Sizes move only when something is
rebuilt, so the flat line is by design — a step in it is the event
worth explaining.
Layer Caches¶
Each build target exports its layer cache to a *-cache repository.
This table double-counts. The tags of one cache repo share most of their layers, so summing manifests is an upper bound — useful for comparing targets, useless as a disk figure.
They are bounded: each repo holds latest, latest-<arch> and
latest-<arch>-ref per architecture in ARCHES,
and every build overwrites the one it owns. Zot runs dedupe, gc and
deleteUntagged itself.
Actual occupancy, from the host:
Regenerating¶
From the registry host, where the tailnet name resolves:
make image-sizes # the table, in the terminal
make image-sizes LAYERS=1 # with the per-layer breakdown
make image-sizes IMAGE_ARCH=gfx1201 # a card other than the default
make zot-disk # deduplicated disk, from the Zot host
make image-data # dist/images/images.json
make image-publish # commit it to gh-pages, locally
make image-publish PUSH=1 # and push
One architecture per table. shared counts a digest carried by
more than one image in the set compared, so a table mixing
cards reports shared layers as unshared and own comes out wrong.
image-publish reads the gh-pages copy first — the only place
images.json's history exists.
Why The Images Were The Size They Were¶
Three things, in order of cost.
The ROCm SDK Base — 7.13 GB¶
rocm/dev-ubuntu-26.04:10.1.0-full unpacks to 20.53 GB in one 8.67 GB
layer: rocBLAS, hipBLASLt and MIOpen kernels for every architecture AMD
supports, plus a full LLVM. Each image targets one architecture, and
AMD publishes no -runtime variant.
rocm-runtime dpkg --purges the per-architecture packages for cards
we do not target — ROCm 10.1 splits them as <component>10.1-<gfx> —
10.77 GB of the 20.53 alone:
| removed | how | measured |
|---|---|---|
| per-architecture packages (260 of them) | dpkg --purge |
10.77 GB |
static archives (*.a, 835 files) |
rm — with one exception |
4.73 GB |
| docs, man, info | rm |
small |
/opt/rocm/lib/cmake |
rm — minimal only |
a few MB |
A rm in a child layer reclaims nothing, so a second stage flattens
it: COPY --from=pruned / / onto scratch — the whole root, not
/opt/rocm, since an image missing ld.so.conf.d or the udev rules
loads libamdhip64 and finds no device.
Measured, gfx1201: 8.67 GB → 1.42 GB, base adding 0.24 GB. An
earlier 1.54 GB figure on gfx950 is withdrawn.
Two things the sweep must not remove
"Nothing loads a .a at run time" is true of the ROCm math
libraries and false of the compiler's own runtime — clang links
libclang_rt.builtins.a into every HIP compilation — and cmake's
HIP detection reads hip-lang-config.cmake, which pytorch-wheel
needs to build PyTorch here. Both were removed on reasoning about
the runtime images, and both surfaced hours downstream as a
failed wheel build naming neither ROCm nor the prune.
So Dockerfile.rocm-runtime compiles and links a real HIP
program and checks the cmake package, in 0.9 s, at build time.
Asserting files exist failed twice.
docker/rocm-prune.sh also asserts the kept architecture still has
packages — else an image imports torch happily and has no kernels for
its card.
Prune Profiles¶
| profile | keeps | for |
|---|---|---|
keep-toolchain (default) |
hipcc, the ROCm headers, LLVM, /opt/rocm/lib/cmake |
everything |
minimal |
none of them | an image that compiles nothing |
minimal is 0.4 GB better on a 4.4 GB tree, but giving pytorch-run
its own would mean a second base image and a second layer cache per
architecture — and the serving images need the toolchain anyway: AITER
JIT-compiles on first use, Triton shells out to ld.lld.
amdrocm-rpp, amdrocm-rdc and amdrocm-rocshmem are 1.1 GB nothing
here imports, kept because each is a hard Depends of the architecture
core package: purging them leaves apt refusing every later operation
downstream.
Wheels Copied Into The Image That Installs Them — ~1.5 GB¶
A COPY is a layer. Dockerfile.pytorch copied the torch wheel in
(0.40 GB), installed it, then rm -rf in a later layer, so every pull
paid for a wheel it could not use. Same for Triton (0.45 GB) and every
wheel in the vLLM and LMCache images. All of those sites now use
RUN --mount=type=bind,from=<stage>, so the wheel never becomes a
layer; the *-wheel images still publish it.
Dependencies That Cannot Run Here¶
The PyPI builds of torchvision, torchaudio and torchdata are CUDA
builds, the only such thing here, and on ROCm their extensions fail to
load. The optional-extras step that pulled them in carried || true
and is gone from Dockerfile.pytorch.
torchvision was the one that mattered, because vLLM's timm reaches
it whether or not anything here asks for it. It now has its own target
— torchvision-wheel, built against this torch — so the serving image
carries a working one. torchaudio and torchdata still have none and
nothing imports them. See
Serving stack.