Primus environment in a venv (no docker, no sudo)
August 11, 2026 · View on GitHub
Reproduces the Primus v26.5 training image in a Python virtual environment on
a bare-metal host. Derived from
.github/workflows/docker-release/Dockerfile.primus-v26.5,
using the same package pins and commits, adapted for the constraints of a machine
where we have no root.
A handful of places deliberately diverge from the Dockerfile, because copying it exactly produces an environment that does not work outside the container. Each one is listed under Fixes applied with the failure it avoids — worth reading before "correcting" any of them back.
Python 3.12 is required
This is a hard requirement from upstream packaging, not a preference. The pinned
torch nightly 2.12.0+rocm7.15.0a20260720 ships a cp312 Linux wheel only —
that nightly date published no cp310 Linux build — and the v26.5
TransformerEngine wheels are cp312-only as well.
Ubuntu 22.04 hosts only have python3.10, so setup.sh provisions a standalone
CPython 3.12 with uv. No sudo, no apt. Interpreters
land in $PRIMUS_BASE/python, keeping the whole environment in one place.
You do not need uv beforehand: if it is missing, setup.sh downloads it into
$PRIMUS_BASE/bin from astral.sh. Pre-install it yourself if that download is
blocked, or if you would rather not pipe a script into a shell:
python3 -m pip install --user uv # ~/.local/bin, one of the paths setup.sh searches
# or: curl -LsSf https://astral.sh/uv/install.sh | sh
setup.sh looks for uv on PATH, then in $PRIMUS_BASE/bin, then ~/.local/bin.
If you cannot install it at all, supply your own interpreter instead and it is never
used: PRIMUS_PYTHON=/path/to/python3.12 bash setup.sh.
Migrating from the v26.4-based scripts: a Python 3.10 venv cannot be upgraded in place.
setup.shdetects the mismatch and stops with instructions; remove the old venv and rebuild:rm -rf "$PRIMUS_BASE/venv" && bash setup.sh
TransformerEngine: wheel on glibc ≥ 2.38, otherwise built from source
v26.5 installs TransformerEngine from the ROCm staging index instead of building
it. Those wheels are produced on Ubuntu 24.04, and libtransformer_engine.so
requires glibc ≥ 2.38 plus GLIBCXX_3.4.32. Ubuntu 22.04 has glibc 2.35, and
unlike libstdc++, glibc cannot be side-loaded through LD_LIBRARY_PATH — so on a
22.04 host the v26.5 wheels install fine but fail at import with:
OSError: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found
stage_te therefore picks its install path from the host's glibc:
| Host glibc | Path | What happens |
|---|---|---|
| ≥ 2.38 (e.g. Ubuntu 24.04) | wheel | exactly what v26.5 does |
| < 2.38 (e.g. Ubuntu 22.04) | source | clones ROCm/TransformerEngine and builds commit a07e607f14a…, the same commit the TE_VERSION label refers to |
Either way you get the same TE version; the source build just links against the
host toolchain. It takes considerably longer than the wheel install. Override the
choice with PRIMUS_TE_MODE=wheel|source (default auto). After installing,
stage_te imports transformer_engine and fails loudly if it cannot load, rather
than letting the problem surface later during training.
Why it differs from the Dockerfile
| Constraint | Dockerfile | Here |
|---|---|---|
| Python | 3.12 (Ubuntu 24.04) | 3.12, auto-provisioned by uv (host has only 3.10) |
| ROCm | pip rocm-sdk-devel | same (self-contained, no sudo needed) |
| GPU arch | gfx942 + gfx950 | auto-detected from the host (rocminfo/KFD sysfs); builds only what's present → faster builds |
| Build dir | container FS | venv under $PRIMUS_BASE (persistent, you choose it); build sources on local /tmp |
| System deps | apt install ... | skipped (no sudo); pip provides what's needed |
| torchaudio / torchvision / apex | floating (torchvision==0.27) | pinned to the exact a20260720 nightly builds — see below |
The key reason no sudo is required: the rocm-sdk-devel pip wheel ships a full
ROCm toolchain inside the venv, so we don't depend on system ROCm or apt.
Companion wheels are pinned, the Dockerfile's floating versions are not reproducible
The Dockerfile pins torch exactly but lets torchaudio and apex float and
resolves torchvision as ==0.27. That set no longer resolves at all: newer
rocm10.x nightlies now publish matching version numbers, so a floating
torchvision selects a build for a different ROCm line, conflicts with the pinned
torch, and pip dies after a long backtracking storm. setup.sh pins each
companion wheel to the version the a20260720 nightly published, i.e. exactly
what the Dockerfile picked up on the day it was built.
Run it
PRIMUS_BASE is required and has no default — the right location is
site-specific, and quietly falling back to some other host's path only hides the
problem. Point it at a writable directory on a disk with tens of GB free; it
holds the venv, the provisioned interpreter, and the Primus/aiter checkouts.
cd tools/installation
export PRIMUS_BASE="$HOME/envs/primus-env" # required, pick your own location
bash setup.sh # all default stages
The build compiles aiter, Primus-Turbo, flash-attention and TransformerEngine from source, so budget hours rather than minutes — on glibc < 2.38 that means all of TE, not just its torch glue layer. Running it detached avoids losing it to a dropped connection:
nohup bash setup.sh > ~/primus-setup.log 2>&1 &
tail -f ~/primus-setup.log
If any stage fails the script stops immediately and prints which stage failed.
Stages are idempotent, so fix the cause and re-run just that one — exporting the
same PRIMUS_BASE again, since that is how it finds the venv:
bash setup.sh --list # show stages (works without PRIMUS_BASE)
bash setup.sh te # reinstall just TransformerEngine
bash setup.sh venv torch # venv + torch only
When cherry-picking stages, note that te depends on torch and flash_attn
having run first: the TE staging index serves only TE packages, so every other
dependency must already be installed.
Use the environment afterward
Export the SAME PRIMUS_BASE you built with, then source env.sh — it activates
the venv and sets every ROCm/NVTE variable. Without PRIMUS_BASE it stops with an
error rather than guessing.
export PRIMUS_BASE="$HOME/envs/primus-env"
source tools/installation/env.sh
python -c "import torch; print(torch.cuda.is_available())"
# Primus is checked out at $WORKSPACE_DIR/Primus
Stages (default order)
venv → torch → flash_attn → te → torchtune → torchao → pydeps
→ grouped_gemm → causal_conv1d → mamba → primus → aiter → turbo
→ boto → cleanup → manifest
Optional: torchrec (DLRM/recommendation stack).
The order matters for te: see the note on the staging index below.
What changed from the v26.4-based scripts
- TransformerEngine now comes from the ROCm staging index where it can.
stage_teinstallstransformer_engine_rocm_torch==2.15.0.dev0+rocm7.15.0a20260716.a07e607fromrocm.frameworks-nightlies.amd.com/whl-staging/device-all/, which pulls a prebuilt core library and compiles only the thin torch glue layer. On hosts whose glibc is too old for those wheels it builds the same commit from source instead — see the section above. stage_temust run afterstage_flash_attn. That staging index serves only the transformer-engine packages, so while--index-urlpoints at it pip cannot reach PyPI for anything else. Every TE dependency has to be installed beforehand. The Dockerfile getseinopstransitively from flash-attention andonnxfromonnxscript;stage_teinstalls both explicitly rather than relying on another package's transitive deps.ck_jit_compile.shis patched the same way v26.5 patches it inside the image: concurrent CK JIT compiles race to publish the same.so, and without the patch the loser aborts the run. The script locates the file throughsysconfig(rather than a hardcoded/opt/venv/lib/python3.12path) and the patch is idempotent.- New
cleanupstage uninstallstilelang, mirroring v26.5.mamba-ssmpulls it in as a hard dependency and v26.5 removes it again once everything is built. apache-tvm-ffinow follows the Dockerfile pin (0.1.11). The old scripts held it at 0.1.6 to dodge atilelangimport failure; withtilelanguninstalled at the end, that workaround is obsolete.GPU_ARCHSisnativeat runtime. v26.5 setsENV GPU_ARCHS=nativeafter the last build stage so aiter's JIT compiles for the GPU actually in the box.env.shtherefore defaults it tonative, andsetup.shoverrides it to the full arch list for the individual stages that cross-compile.- Build-only NVTE vars dropped from the runtime environment.
NVTE_USE_ROCM,NVTE_FRAMEWORK,NVTE_ROCM_ARCHandNVTE_USE_HIPBLASLTconfigure the from-source TE build and are gone from v26.5, soenv.shno longer exports them;stage_te_sourcestill sets them inline for the duration of its build. The runtime performance knobs (NVTE_CK_*,NVTE_USE_CAST_TRANSPOSE_TRITON,NVTE_FLASH_ATTN=0,NVTE_FUSED_ATTN=1) stay. - Updated pins: torch
2.12.0+rocm7.15.0a20260720, Primusb511d1b66b0068715308ea9bfe8ba147ea1a3860(release/v26.5), Primus-Turboedc8d2ccb0be4888e80ee7c6e765fd3956026a32. flash-attention, torchtune, torchao, grouped_gemm, causal-conv1d, mamba and aiter are unchanged in v26.5.
What is SKIPPED (needs sudo / apt — not reproducible here)
- AINIC (
add-apt-repository,libionic-dev): apt-only. Skipped. - UCX + OpenMPI: autotools source builds needing
libtooland RDMA dev headers, both apt-only. Single-node training works without them. Skipped. (v26.5 removed rocSHMEM from the image entirely, so nothing else needs them.) - DeepEP internode in Primus-Turbo: needs rocSHMEM, so
stage_turbodisables it by pointingROCSHMEM_HOMEat a non-existent path, and the build prints a warning saying internode DeepEP is off. This is deliberate — left to auto-detect, Primus-Turbo mistakes the pip ROCm SDK directory for a rocSHMEM install (it has rocshmem headers and device bitcode, but no hostlibrocshmem.a) and the link then fails. Intranode DeepEP is unaffected. Export a realROCSHMEM_HOMEto opt back in. - MLPerf
primus_mllog: v26.5 installs it fromtraining_results_v6.0. Not part of the core training path, so it is not installed here. - DLRM / FBGEMM / Flux: not part of the default Primus training path.
torchrecis provided as an optional stage; FBGEMM additionally needs aptlibtbb-dev. - Misc apt runtime packages (
numactl,pciutils,libz3-dev,ffmpeg,gfortran): not installed. Install via sudo later if a specific workload needs them.
Fixes applied (gotchas vs. the Dockerfile)
These are needed because of the no-sudo/bare-metal setting and are baked into the scripts:
- pip is constrained so it cannot swap the ROCm GPU stack for a CUDA one.
After installing torch,
setup.shwrites$PRIMUS_BASE/pip-constraints.txtpinning the installedtorch/triton, and every laterpip installpasses-c. Many packages depend on a baretorch; a resolver that decides to "upgrade" it silently replaces the whole GPU stack withnvidia-*wheels. With the constraint, such an attempt fails loudly instead. - Primus-Turbo is installed with
--no-deps, keeping ROCm's triton. Itssetup.pyhard-pins upstreamtriton==3.7.0, while torch is built against ROCm'striton==3.7.1+git…rocm…. The unconstrained Dockerfile lets the upstream wheel win, but doing that here breaks the environment: ROCm's HIP runtime already loads its ownlibLLVM.so.23from the pip SDK, and the upstream triton wheel bundles a second, statically linked LLVM, so importing it segfaults inside LLVM's static initialisers — taking downtorch._dynamo,aiter,torchaoandmamba_ssmwith it.stage_turbotherefore installs Primus-Turbo without deps and supplies its real runtime requirements (scipy,flydsl) explicitly. This is a deliberate, tested deviation from v26.5. - mamba built with pip, not
python setup.py install. The legacyeasy_installpath ignores pip-installed packages and re-fetches the latest of every unpinned dep as.eggs — it clobberedtransformers(→5.x), removedaccelerate/trl, and pulled NVIDIA CUDA packages.stage_mambausespip install --no-build-isolation .which respects the pins. NVTE_CK_IS_V3_ATOMIC_FP32defaults to1on gfx942, not the Dockerfile's0. Paired withNVTE_CK_USES_BWD_V3=1, turning fp32 atomics off makes the CK v3 backward attention kernel emit Inf gradients on MI300X/MI325X, killing training at the first step. Primus's own tuning guide already prescribes fp32 atomics for these GPUs (the MI300X/MI325X block in docs/02-user-guide/training-recipes.md), soenv.shapplies it from the detected architecture and leaves0for gfx950, which the Dockerfile value targets.NVTE_CK_USES_BWD_V3itself stays at the Dockerfile's1— it is worth roughly 15% throughput, and the atomic mode is what makes it safe.- Megatron's
helpers_cppis built with an explicitLIBEXT, for every checkout. Its Makefile derives the output filename frompython3-config --extension-suffix, but a venv does not shippython3-config, so it silently falls back to the system interpreter and writes acpython-310name that a 3.12 venv will never import.stage_primuspassesLIBEXTfrom the venv's ownEXT_SUFFIX, and does so for the workspace clone,~/.cache/Primus, and the checkout containing these scripts — training is usually launched from the latter. It also deletes any pre-existing extension of the target name first: a checkout that has been bind-mounted into the training container may hold a root-owned Ubuntu 24.04 build that needsGLIBCXX_3.4.32, cannot load here, is not writable, and otherwise takes precedence. Either problem surfaces asMockGPTDataset failed to build as a mock data generator. flydslpinned to 0.2.4 so aiter keeps its CK/HIP kernels. aiter pinsflydsl==0.1.7and Primus-Turbo requiresflydsl>=0.2.0, so one of the two is always unsatisfied and Turbo, installing last, wins. aiter only needsflydsl.expr.vectorat runtime, and that survived until 0.3.0 removed it — after which aiter printsROCm/HIP JIT runtime not available … CK and HIP ops are disabled. Triton ops remain available.and quietly runs Triton-only. 0.2.4 is the newest release that satisfies Turbo and keeps aiter whole, and is what the Dockerfile itself resolved to, since 0.3.0 was published after v26.5 was built.stage_turboasserts the symbol is importable afterwards. This is about capability parity with the image, not speed: on llama3.1_8B BF16 an A/B of 0.2.4 against 0.3.0 measured 535.9 vs 534.5 TFLOP/s/GPU, i.e. no difference. Other workloads that lean on aiter's CK/HIP kernels are the ones that would notice.nvidia-cutlass-dslpinned to 4.5.3 soimport mamba_ssmworks.mamba_ssmpinsquack-kernels==0.3.1, which requiresnvidia-cutlass-dsl>=4.4.1with no upper bound. 4.6.x restructured the DSL and removedcute.core.ThrCopythat quack 0.3.1 imports, so the unpinned resolution ends inAttributeError: module 'cutlass.cute.core' has no attribute 'ThrCopy'. 4.5.3 is the newest release quack 0.3.1 still works with, and pip cannot choose it unaided because 4.5.3 was published after 4.6.0 yet sorts lower. v26.5 does not pin this, so the released image is affected by the same break.patchelffrom pip, since the apt one is unavailable.libz3.sofrom pip (z3-solver, added toLD_LIBRARY_PATHbyenv.sh). Withtilelangnow uninstalled this is only a safety net, kept so that re-installingtilelangby hand does not leave a broken environment.- Megatron is not pip-installed; it's bundled at
$WORKSPACE_DIR/Primus/third_party/Megatron-LMand added to the path by Primus at runtime (or setPYTHONPATHyourself for standaloneimport megatron).
Caveats
- Persistence: the venv, the provisioned interpreter and the kept checkouts
live under
$PRIMUS_BASE(persistent). Transient build sources go to local/tmpfor speed and are deleted after each build. - Disk: a full build needs tens of GB.
- Nightly indexes are pruned. The pinned torch/TE nightlies will eventually
disappear from the ROCm index. When that happens, pick a new nightly date that
publishes a complete cp312 set and update the pin block at the top of
setup.sh— all oftorch,amd-torch-device-*,rocm-sdk-*,torchaudio,torchvision,amd-torchvision-device-*andapexshould come from the same date. - GPU arch is auto-detected (
env.shreadsrocminfo, else the kernel KFD sysfsgfx_target_version), andstage_torchinstalls the matching device wheels for whatever it finds (gfx942 and/or gfx950). To force a target — e.g. to build a portable env for both — export it before running:export PYTORCH_ROCM_ARCH="gfx942;gfx950".