MIGraphX + ONNX Runtime + PyTorch, combined into one image. Started as a way to get MIGraphX fixes that hadn't shipped yet against a stable ROCm 7.14, paired with otherwise-stable Torch/ORT. Grew into two separate things:
-
A true nightly build: every version (ROCm itself, rocBLAS, MIGraphX, PyTorch, TheRock's whole stack) fully parameterized and left to float -- nobody, including the maintainer, has to hand-pin anything for it to keep building. Only ONNX Runtime's version stays fixed for now.
-
A manual release build, independent of AMD's own release cadence (which tends to lag): a
workflow_dispatch-triggered pipeline with sane defaults (ROCm 10.0, PyTorch 2.14; MIGraphX pins torelease/rocm-rel-10.0) that pins every moving part explicitly, so a reproducible build never depends on AMD shipping their own combined image on any particular schedule.This same pinning also doubles as an escape hatch if an arch ever falls out of upstream support: trigger one last manual release build pinned to the last ROCm/PyTorch/MIGraphX combination that arch still works on, and keep re-publishing that exact combination on demand.
Both published to ghcr.io/<owner>/rocm-migraphx-ort-torch-builder.
Baking every arch in ROCM_ARCH into one image isn't feasible on GitHub-hosted
runners -- AMD's own rocm/pytorch image, which only bakes in 5 archs, is
~54GB uncompressed, well past what a hosted runner's disk holds. CI instead
builds and pushes one tag per arch (a matrix over ROCM_ARCH's values), rather
than one fat multi-arch image.
The build is also split by component across separate CI jobs: MIGraphX, PyTorch,
torchvision, torchaudio and ONNX Runtime each build on their own runner (each
with its own ~6h budget -- one monolithic build overran the hosted-runner
ceiling), and each publishes an intermediate image the next job pulls from
instead of recompiling. MIGraphX and PyTorch are independent and run in
parallel; ONNX Runtime builds against MIGraphX's /opt/rocm; torchvision and
torchaudio build against PyTorch's wheel; a final job assembles all of them.
The final job deliberately builds nothing -- it only pulls those images and installs their wheels. Every component it needs has to be prebuilt, because a hosted runner has no disk left to compile one on top of the ~10GB ROCm base the job already pulls. Each component lands in its own package:
rocm-builder:latest/:<YYYYMMDD>-- the self-built nightly ROCm base (see "Nightly ROCm base image" below). Arch-independent, built once before the arch matrix, not part of the per-arch tag scheme below.rocm-rocblas-builder:<arch>-- ROCm with rocBLAS rebuilt from source. Only exists for the arches that may need it (gfx900,gfx906,gfx90c); MIGraphX and PyTorch both start from it there instead of each rebuilding rocBLAS themselves. Whether it actually rebuilds is decided inside the stage: release builds always rebuild (AMD's stable bases ship no kernels for these arches), nightly builds only when their own base lacks the kernels -- a from-source build is the fallback, never another version's prebuilt. On a nightly whose base carries them (TheRock 10.x), the image is a passthrough over the baserocm-migraphx-builder:<arch>-- from-source MIGraphX + ROCm deps in/opt/rocmrocm-migraphx-torch-builder:<arch>-- PyTorch wheelrocm-torchvision-builder:<arch>-- torchvision wheel (built against the PyTorch wheel above)rocm-torchaudio-builder:<arch>-- torchaudio wheel (likewise)rocm-migraphx-ort-builder:<arch>-- ONNX Runtime wheel (built against MIGraphX)rocm-migraphx-ort-torch-builder:latest-<arch>/:<YYYYMMDD>-<arch>-- the combined image, everything installed
All but the last two are build plumbing (each an incomplete slice of the
stack); downstream consumers want the combined rocm-migraphx-ort-torch-builder.
A manual release build (see "Manual release build" below) tags every
component differently: rocblas/migraphx/pytorch/torchvision/torchaudio/ort each publish as
<arch>-rocm<version> (e.g. gfx1201-rocm10.0) instead of the nightly
scheme's plain <arch>, and the combined image publishes as
rocm<version>-<arch> (e.g. rocm10.0-gfx1201) instead of latest-<arch>/
<YYYYMMDD>-<arch>. This applies to every component, not just the final
image -- rocblas and pytorch both genuinely vary by ROCm release (rocblas
is rebuilt from a version-pinned source ref; pytorch's own version/ROCm-release
build-args differ from nightly's), so tagging them the same as nightly's plain
<arch> would silently overwrite one build with the other instead of keeping
them as separate, versioned artifacts.
Torch is also built from source rather than reusing AMD's rocm/pytorch
image: current rocm/pytorch tags ship TheRock's pip-packaged ROCm SDK (no
/opt/rocm), a different runtime layout than MIGraphX's build expects --
mixing the two would risk conflicting libamdhip64.so builds loaded into one
process.
Both CK (composable_kernel) and rocMLIR are built from source and enabled
(matching how AMD's own prebuilt images ship), via MIGraphX's own documented
build tool (rbuild) rather than apt packages, which don't line up with what
MIGraphX's CMake actually requires (see docker/migraphx.Dockerfile and
scripts/build/migraphx.sh).
Nightly wheel/deb discovery (scripts/torch-package-build-decide.sh) applies no ROCm-major
preference -- it takes whatever AMD's index has newest, regardless of major version. As of
2026-07-28/30, that's ROCm 10.x: AMD bumped TheRock's version.json straight from 7.15
to 10.0.0 to 10.1.0 (skipping 8 and 9 entirely) -- confirmed via that repo's commit history
and the linked issues (#6932,
#7000): "The next release is targeted to ship
as ROCm 10." This is a deliberate AMD versioning decision, not a stray/unstable preview build
that happened to sort highest.
Practical risk: 7.x nightlies may stop being published entirely once 10.x fully takes over. Nightly discovery here always floats to newest regardless, so this shouldn't break the build, but expect the nightly image to move onto ROCm 10.x without any explicit signal beyond this note.
BASE_IMAGE defaults to ghcr.io/schaka/rocm-builder:latest, not AMD's own
rocm/dev-ubuntu-26.04 -- AMD publishes no rolling/nightly tag for that image
at all, only pinned version releases (confirmed against repo.radeon.com's
apt repo: version numbers, alpha/beta/rc, and a latest alias that just means
"newest stable release", nothing rolling). So nightly instead self-builds the
ROCm base from TheRock's nightly .deb feed
(nightly.repo.amd.com/rocm/core/packages/deb/, the TheRock 10.x native
package feed -- the old rocm.nightlies.amd.com/packages-multi-arch/deb
layout is the legacy release stream, home to 7.14/10.0-rc artifacts only) via
the rocm-base bake target, built and published once
(ghcr.io/<owner>/rocm-builder:latest
and :<YYYYMMDD>) before the per-arch matrix runs, not per-arch -- it's
arch-independent (amdrocm-hpc-sdk covers every gfx target in one package).
A manual release build overrides BASE_IMAGE back to AMD's own pinned tag
instead (see below) -- this self-built path is nightly-only.
Two things worth knowing if you touch this stage:
- TheRock's
.debfeed has nolatestalias either, only datedYYYYMMDD-<run-id>directories, and the bare index URL serves a stale cached snapshot -- a cache-busting query string is required to get the real, current listing (seescripts/build/rocm-base.sh). - The installed package layout differs from AMD's own image: everything lands
under
/opt/rocm/core-<major.minor>/with no top-level convenience symlinks, andamdrocm-hpc-sdkalone doesn't pull HIP's own dev/cmake package (amdrocm-core-dev/amdrocm-runtime-dev) -- both gaps are worked around explicitly in therocm-builderstage; validated end-to-end with a real gfx1201 MIGraphX + PyTorch build against it, and separately confirmed working (MIGraphX ONNX provider + GPU dispatch) on real gfx1010 (RDNA 1, RX 5700 XT) hardware via theaudiomuse-rocm-pluginworker image.
Officially supported: gfx900 and above, i.e. what AMD lists in the ROCm
supported-GPU matrix. Those are the archs in ROCM_ARCH, the ones built
nightly, and the only ones worth filing issues against.
Polaris / gfx803 (RX 460 through RX 590) is not supported by anything in this repo -- ROCm 7 removed Polaris support from ROCR-Runtime outright, so it needs a different ROCm major, a different base image, and a different build entirely, none of which fit this repo's graph. It has its own repo instead: github.com/Schaka/rocm-gfx803.
Drop-in BASE_IMAGE for anything currently pinned to a rocm/onnxruntime:*
tag: onnxruntime (built --use_rocm --use_migraphx) lives in a venv at
/opt/venv (on PATH), and /opt/rocm has the from-source MIGraphX plus its
ROCm runtime deps.
torch/torchvision/torchaudio always install into their own venv,
/opt/venv-torch (exported as $VIRTUAL_ENV_TORCH, deliberately not on
PATH) -- that's the one path guaranteed to have torch, regardless of build
tier. Whether /opt/venv's python3 also gets torch depends on which
tier actually built it:
- PIP tier (AMD's prebuilt wheel, the
USE_PREBUILT_PYTORCH=1default when a matching wheel exists) -- torch stays isolated to/opt/venv-torchonly. The prebuilt wheel bundles TheRock's own pip-packaged ROCm SDK (rocm_sdk/_rocm_sdk_core/_rocm_sdk_device_<arch>), a second, independent copy oflibamd_comgr/libLLVMalongside this image's classic/opt/rocmcopy. If both ever load into the same process -- which happens automatically the moment anything imports torch in a process that also touches the classic stack, e.g.ctranslate2opportunistically doesimport torchinternally if it's importable at all -- the process aborts at startup: LLVM's globalCommandLineoption registry rejects the duplicate registration (CommandLine Error: Option 'spirv-expand-step' registered more than once!). There is no supported way to install the prebuilt wheel--no-depsand reuse the classic libs instead:torch/_rocm_init.pydoes a bare, unguardedimport rocm_sdkatimport torchtime, so the wheel simply refuses to import at all without itsrocm_sdksiblings present -- confirmed by testing it directly. Exposing/opt/venv-torch'ssite-packagesto the classic venv (viaPYTHONPATHor similar) reintroduces the exact same crash, since that just recreates "both trees reachable from one process" by another name --PATHalone is harmless (it only selects whichpython3binary runs, not what an already-running interpreter can import) but two venvs both providing a binary namedpython3invites a different, quieter bug: whichever directory wins onPATHsilently decides what any barepython3/pipcall actually runs. - SOURCE tier (no matching prebuilt wheel, or
USE_PREBUILT_PYTORCH=0) -- torch links directly against this image's own classic/opt/rocm, norocm_sdkinvolved, so none of the above risk applies. It's installed into/opt/venv-torchand merged into/opt/venvtoo, restoring the zero-friction "torch just works from the defaultpython3" behavior this image had before the split. The build asserts MIGraphX's provider survives that merge before finishing, so a future regression here fails the build loudly instead of shipping a broken image.
Which tier fired for a given build/arch can float across nightly runs (see
"Nightly ROCm versioning" below) -- don't hardcode an assumption either way
in a downstream Dockerfile. /opt/venv-torch is the one path that's always
right:
RUN "$VIRTUAL_ENV_TORCH/bin/python3" -c "import torch; print(torch.__version__)"The combined image (rocm-migraphx-ort-torch-builder) is tagged per-arch:
:latest-<arch> (e.g. :latest-gfx1201) and :<YYYYMMDD>-<arch> for nightly
builds, or :rocm<version>-<arch> (e.g. :rocm10.0-gfx1201) for a manual
release build -- there is no plain :latest, pick the tag matching your GPU's
ROCM_ARCH value (and whichever build track you want).
ARG BASE_IMAGE=ghcr.io/<owner>/rocm-migraphx-ort-torch-builder:latest-gfx1201
FROM ${BASE_IMAGE}
RUN python3 -c "import onnxruntime as ort; print(ort.get_available_providers())"The build is a Docker Bake graph, not a single Dockerfile:
docker-bake.hcl # the build graph: targets, wiring, tags, cache refs,
# and every version-shaped variable. Start here.
docker/*.Dockerfile # one file per component (rocm-base, python-base,
# rocblas, migraphx, pytorch, torchvision, torchaudio,
# ort, final). Structure only -- no inline shell.
scripts/lib/*.sh # helpers shared across stages (parallel-job sizing,
# the legacy-GCN predicate, the ROCm build env, the
# torch wheel install, the torchvision/torchaudio
# common path)
scripts/build/*.sh # one script per non-trivial build step, mounted into
# the stage that runs it
.github/actions/ # composite actions: runner preparation, arch matrix
.github/workflows/ # orchestration only; they set bake variables and name
# a target, and contain no build logic
Components are wired to each other through Bake named contexts rather than
COPY --from=$SOME_ARG indirection stages. Locally each context resolves to the
in-tree target and one command builds the whole graph; in CI the WITH_*_IMAGE
variables swap individual contexts for already-published component images, so a
job pulls its dependencies instead of recompiling them.
Set as environment variables for docker buildx bake; the workflows set exactly
the same ones. All are declared, with these defaults, in docker-bake.hcl.
-
BASE_IMAGE(defaultghcr.io/schaka/rocm-builder:latest) - the ROCm base. Defaults to the self-built nightly base (see "Nightly ROCm base image" above); the manual release workflow overrides this to AMD's own pinnedrocm/dev-ubuntu-26.04:<version>-fullinstead. Its nativepython3is 3.14, but downstream apps like AudioMuse-AI pinnumpyto a version that only resolves against onnx's deps under 3.12, so every wheel built here and the final/opt/venvall target a uv-managed Python 3.12 instead of the base image's interpreter. -
ROCM_ARCH(defaultgfx900;gfx90c;gfx906;gfx908;gfx90a;gfx942;gfx950; gfx1010;gfx1011;gfx1012;gfx1030;gfx1031;gfx1032;gfx1034;gfx1035; gfx1036;gfx1100;gfx1101;gfx1102;gfx1103;gfx1150;gfx1151;gfx1152;gfx1153; gfx1200;gfx1201) - semicolon-separatedGPU_TARGETS/CMAKE_HIP_ARCHITECTURES/PYTORCH_ROCM_ARCHlist, matching the breadth AMD's own published images build for. Narrow it to your one GPU for a much faster build, e.g.ROCM_ARCH=gfx1201. -
ROCM_RELEASE(default empty,X.Ye.g.10.0) - pins two things together: pytorch/torchvision/torchaudio's prebuilt-wheel discovery, and (forgfx900/gfx906/gfx90conly) the ROCm line rocBLAS is rebuilt from source against -- release always rebuilds for those arches; nightly never substitutes another version's kernels, rebuilding only when its own base lacks them. Empty means both float independently -- wheel discovery takes whatever's newest (see "Nightly ROCm versioning" above), and a rocBLAS rebuild (nightly, when needed) builds from rocm-libraries' owndevelopbranch instead of a pinnedtherock-<release>tag. Must matchBASE_IMAGEwhen set; the manual release workflow derives all three from onerocm_versioninput so they can't drift apart. -
MIGRAPHX_REF(defaultdevelop) - git ref to build MIGraphX from. The manual release workflow overrides this torelease/rocm-rel-<version>. -
USE_PREBUILT(default1) - try AMD's prebuilt wheels first for pytorch/torchvision/torchaudio, falling back to a from-source build per package if none match.0forces a full from-source build. Each of the three is its own bake target, so it can also be overridden for one of them alone (--set torchaudio.args.USE_PREBUILT=0).In release mode (
ROCM_RELEASEset), pytorch's own resolution is three tiers, not a flat wheel-or-source choice: a real stable release onrepo.amd.com, then (if AMD hasn't published a clean release for that exactPYTORCH_VERSION/ROCM_RELEASEpin -- some pins have none for the pinned ROCm line, on any arch) a single self-consistent devreleases nightly snapshot (scripts/rocm-devrelease-snapshot.py), then a full source build. torchvision/torchaudio match whichever exact version pytorch actually resolved to (read off the downloaded wheel filename), not independently guessed -- seescripts/torch-package-build-decide.shfor the exact tier logic. -
ORT_VERSION(defaultv1.29.0) - onnxruntime git tag. Nightly always uses this default (never floated, unlike everything else); the manual release workflow can override it explicitly. -
PYTORCH_VERSION(defaultv2.14.0) - has two roles depending onROCM_RELEASE: in release mode (ROCM_RELEASEset) it's an exact pin, both for prebuilt-wheel discovery and the from-source fallback branch. In nightly mode (ROCM_RELEASEempty) it's an optional pin -- if set, nightly wheel discovery filters to that pytorch version while still floating on the newest matching ROCm nightly build; empty floats on pytorch's version too. Nightly CI passes this as an explicit empty string (not omitted) so it actually floats instead of inheriting this default. -
BUILD_PARALLEL_LEVEL(defaultauto) - MIGraphX/rocMLIR and PyTorch build parallelism (CMAKE_BUILD_PARALLEL_LEVEL/MAX_JOBS).autosizes it fromMemAvailableat build time (~4GB/job, capped atnproc) since the rocMLIR LLVM build needs several GB RSS per job and running as many jobs asnproccan OOM (or segfault, seen in practice) the host. Pass an explicit integer to override, e.g. on a CI runner with known dedicated RAM.
Builds go through Docker Bake, which resolves the whole component graph in one command. Requires Buildx (bundled with Docker 23+).
# one GPU only -- what you almost always want
ROCM_ARCH=gfx1201 docker buildx bake final
# every arch in the default ROCM_ARCH list
docker buildx bake final
# just one component, e.g. to iterate on the MIGraphX stage
ROCM_ARCH=gfx1201 docker buildx bake migraphx
Any variable from "Build variables" above can be set the same way, e.g. a local reproduction of what the release workflow builds:
ROCM_ARCH=gfx1201 \
BASE_IMAGE=rocm/dev-ubuntu-26.04:10.0.0-full \
ROCM_RELEASE=10.0 \
MIGRAPHX_REF=develop \
RELEASE_TAG=rocm10.0 \
PYTORCH_VERSION=v2.14.0 \
docker buildx bake final
docker buildx bake --print <target> renders the fully resolved graph -- tags,
contexts, build args, cache refs -- without building anything. That is the same
graph CI builds, so it is the fastest way to check a workflow or bake change.
Expect 30-60+ minutes per target architecture: this builds LLVM (for rocMLIR),
composable_kernel, ONNX Runtime, and PyTorch from source across every
architecture in ROCM_ARCH.
The full nightly matrix builds every arch in ROCM_ARCH, unhelpful for
confirming a workflow change actually works. Trigger a single-arch run
instead:
gh workflow run nightly.yml -f arch=gfx1201
or via the Actions tab -> "Nightly build" -> "Run workflow", filling in the
arch input. Leaving it empty runs the full matrix, same as the schedule.
A single-arch run still builds the shared rocm-builder base first (once,
not per-arch, see "Nightly ROCm base image" above), then fans out to the
per-arch component builds (migraphx, pytorch, torchvision, torchaudio, ort,
final) for that arch,
via the reusable build-pipeline.yml workflow (which in turn calls
build-component.yml per component). The debug input opens a detached
tmate SSH session into the runner for the build's duration (manual runs
only) when a component needs live inspection.
On first run each package is created private and linked to this repo;
flip each to public in its ghcr package settings if downstream pulls need to
be anonymous. GITHUB_TOKEN (with packages: write) handles the push -- no
PAT required.
Trigger via the Actions tab -> "Release build" -> "Run workflow", or:
gh workflow run release.yml -f rocm_version=10.0 -f pytorch_version=2.14.0
Inputs, all optional with sane defaults:
rocm_version(default10.0.0) - ROCm version to pin (X.Y[.Z]), must match a realrocm/dev-ubuntu-26.04tag. DrivesBASE_IMAGE,ROCM_RELEASE, and (unlessmigraphx_refbelow overrides it)MIGRAPHX_REF.migraphx_ref(default empty = deriverelease/rocm-rel-<rocm_version major.minor>, falling back todevelopwhen that branch doesn't exist) - git ref to build MIGraphX from, independent ofrocm_versionwhen set explicitly. The 10.0 default resolves torelease/rocm-rel-10.0.pytorch_version(default2.14.0) - exact pytorch version to pin.ort_version(defaultv1.29.0) - onnxruntime git tag.use_prebuilt(defaulttrue) - try AMD's prebuilt wheels first for pytorch/torchvision/torchaudio, falling back to source per-package if none match;falseforces a full from-source build of all three.arch(default empty = full matrix) - single arch to build.
Unlike nightly, this workflow never touches the self-built rocm-builder
base at all -- BASE_IMAGE is always the AMD-pinned tag derived from
rocm_version, regardless of what nightly's own base currently is. See the
component-tag list near the top of this README for how release-build tags
(<arch>-rocm<version> per component, rocm<version>-<arch> for the final
image) differ from nightly's.
This is also the escape hatch for a future arch losing upstream support (see
the intro above): pin rocm_version/pytorch_version to the last
combination that arch still builds on, and keep re-running this workflow with
those same inputs indefinitely.