Building Triton Inference Server for RHEL / manylinux#
[!WARNING] This is a community example — not officially supported. Triton’s
build.py --target-platform=rhelpath is experimental (RHEL is not an officially supported target), and the base image reconstructed in this tutorial is equivalent, not identical, to the one NVIDIA uses internally to produce the releasedmanylinuxartifacts.
NVIDIA publishes prebuilt manylinux_2_34 (RHEL 9‑compatible) Triton Inference Server
artifacts. Reproducing them ourselves with build.py --target-platform=rhel requires a
manylinux CUDA/cuDNN/TensorRT base image that is not published, so this tutorial reconstructs
an equivalent one from public sources — pypa’s manylinux_2_34 image (AlmaLinux 9) plus the
CUDA toolkit, cuDNN and TensorRT from NVIDIA’s public cuda-rhel9 repo — and walks through
building and running a
RHEL/manylinux Triton server end‑to‑end.
By the end of this tutorial, we will produce the following:
The manylinux artifacts.
build/install/holds thetritonserver/tritonfrontendwheels tagged…-cp312-cp312-manylinux_2_34_x86_64.whlplus the backend trees we build (e.g.onnxruntime,pytorch,python) — all from 100% public inputs.A working, provably-manylinux container. The built image serves real inference, and both the wheels and the runtime are verified to conform to
manylinux_2_34(glibc 2.34 / EL9), so it runs on RHEL 9 and derivatives (Rocky, AlmaLinux, …).
Prerequisites#
A Linux x86-64 host with a working Docker daemon. This tutorial builds with
build.pythe same way a standard Triton build does; Triton’s supported build platform is Ubuntu 22.04, x86-64. Since the build runs in containers, other x86-64 Linux hosts with Docker should work too. A GPU is not required to build since CUDA libraries come from the base image; you only need a GPU to run GPU inference.Disk space and build time depend on which backends you build. Building the
onnxruntimebackend from source alone takes ~2 hours and tens of GB; a minimal build is much quicker.Network access to the public NVIDIA package repos, PyPI, and GitHub (
build.pyclones the backend sources).Dockerfile.base.rhel— required; the base image every--target-platform=rhelbuild needs.Dockerfile.pytorch.rhel— optional; only if you build thepytorchbackend (Step 2).Dockerfile.pytorch-runtime.rhel— optional; completes the built image so thepytorchbackend can serve (Step 4).
[!NOTE] Choosing backends. Skip any backend by leaving its
--backend=flag out of Step 3; droppingonnxruntimealso avoids the ~2h source build. The PyTorch image (Step 2) is only needed for thepytorchbackend.TensorRT is the exception: a GPU
onnxruntimebuild auto-enables ONNX Runtime’s TensorRT provider (when building forrhelon x86-64), so it’s pulled in even without thetensorrtbackend. For a TensorRT-free build, either skip bothonnxruntimeandtensorrt, or keeponnxruntimeand disable the provider with--override-backend-cmake-arg onnxruntime:TRITON_ENABLE_ONNXRUNTIME_TENSORRT=OFF; then removetensorrt-develfromDockerfile.base.rhel.
Choose a Triton version#
These variables build main; all steps below use them. To build a release, set TRITON_REF to
its branch and take the other values from the table.
export TRITON_REF=main # or a release branch from the table
export CUDA_VERSION=13.4.1
export CUDNN_VERSION=9.25.1.1
export TENSORRT_VERSION=11.2.1.2-1.cuda13.3
export TORCH_VERSION=2.14.0 # used by Steps 2 and 4
export TORCH_INDEX_URL=https://download.pytorch.org/whl/cu132
export PYTORCH_BACKEND_CPYTHON_DIR=cpython-3.12.13
Known-good versions per release:
Release |
|
|
|
|
|
|
|---|---|---|---|---|---|---|
26.08 |
|
|
|
|
|
|
[!IMPORTANT] Releases before 26.08 used a different, pyenv-based
rhelbuild path — use the tutorial revision that shipped with that release rather than this one.
Step 1: Build the public base image#
On the rhel path, build.py installs its own build tooling and DCGM (NVIDIA’s Data Center
GPU Manager) but expects the base image to already be a manylinux image with the
CUDA/cuDNN/TensorRT stack on it: since 26.08 it relies on pypa’s /opt/_internal layout — the
CPython builds, their static libpython archive, and the
pipx “shared” venv it puts first on PATH and uses as python3/pip3 for its tooling, the
Triton wheels and the python backend. Dockerfile.base.rhel starts from
pypa’s official quay.io/pypa/manylinux_2_34_x86_64 image (AlmaLinux 9 = RHEL 9 / glibc 2.34),
adds gcc-toolset-13 (the compiler NVIDIA’s 26.08 artifacts are built with; EL9’s RapidJSON 1.1.0
doesn’t compile under GCC 14), enables EPEL + CRB for the -devel packages build.py installs,
adds the CUDA toolkit, cuDNN and TensorRT from the public cuda-rhel9 repo with the same
environment the nvidia/cuda images export, and recreates that shared venv from the image’s
CPython 3.12 — the interpreter Triton’s RHEL build targets. (PyTorch’s extra runtime
libraries — NCCL, cuSPARSELt — are not added here; the Step 4 completion image installs them,
so the base stays generic.)
Build it with the values from your release. TensorRT trails CUDA by a minor, which is why 26.08
pairs CUDA 13.4.1 with a TensorRT built against cuda13.3. The pypa manylinux image tag is
pinned in the Dockerfile; pass --build-arg BASE_IMAGE=... to change it:
docker build -f Dockerfile.base.rhel \
--build-arg CUDA_VERSION="$CUDA_VERSION" \
--build-arg CUDNN_VERSION="$CUDNN_VERSION" \
--build-arg TENSORRT_VERSION="$TENSORRT_VERSION" \
-t triton-manylinux-base:example .
Step 2 (optional): Build the PyTorch backend image#
Only needed if you build the pytorch backend.
Dockerfile.pytorch.rhel installs a public torch wheel into the
same manylinux_2_34 image so the PyTorch backend can extract a libtorch that runs on EL9’s
glibc 2.34 (the default Ubuntu-based libtorch is built against a newer glibc and won’t load
there). The backend copies from a hardcoded /opt/_internal/cpython-3.12.x path
(cpython-3.12.13 on r26.08); the Dockerfile symlinks that name to whatever 3.12 patch the
image actually ships.
docker build -f Dockerfile.pytorch.rhel \
--build-arg TORCH_VERSION="$TORCH_VERSION" \
--build-arg TORCH_INDEX_URL="$TORCH_INDEX_URL" \
--build-arg PYTORCH_BACKEND_CPYTHON_DIR="$PYTORCH_BACKEND_CPYTHON_DIR" \
-t triton-manylinux-pytorch:example .
The pytorch backend’s CMake, which build.py runs inside the builder container, does an
unconditional docker pull of --image=pytorch; --no-container-pull only affects build.py’s
own base image build. So the image must be reachable from a registry. Push it to a temporary
local one:
docker run -d -p 5000:5000 --name registry registry:2
docker tag triton-manylinux-pytorch:example localhost:5000/triton-manylinux-pytorch:example
docker push localhost:5000/triton-manylinux-pytorch:example
localhost:5000 is just a stand-in for NVIDIA’s internal registry. Step 3’s command already
includes the PyTorch flags; the built image can’t serve PyTorch models until Step 4 completes it.
Step 3: Build the server#
Clone the server repo at the matching release branch and run build.py with your base
image:
git clone https://github.com/triton-inference-server/server.git
cd server && git checkout "$TRITON_REF"
./build.py -v --target-platform=rhel --no-container-pull \
--image=base,triton-manylinux-base:example \
--image=pytorch,localhost:5000/triton-manylinux-pytorch:example \
--extra-core-cmake-arg=PYBIND11_FINDPYTHON=ON \
--extra-core-cmake-arg=Python_LIBRARY=/opt/_internal/nolibpython/libpython3.12.a \
--enable-gpu --enable-logging --enable-stats --enable-metrics \
--enable-gpu-metrics --enable-cpu-metrics --enable-tracing \
--endpoint=http --endpoint=grpc \
--backend=onnxruntime --backend=pytorch --backend=python \
--extra-backend-cmake-arg=pytorch:TRITON_PYTORCH_ENABLE_TORCHVISION=OFF \
--repoagent=checksum
Key flags:
No
--version/--container-version/--upstream-container-version, and no:tagon the--backend/--repoagentflags — build.py reads all of them from the checkout (DEFAULT_TRITON_VERSION_MAPand the branch), so this command is the same for everyTRITON_REF.--no-container-pull— assuming your base image is local, this stops Docker from trying to pull it. It does not stop the pytorch backend’s own pull of--image=pytorch— that’s why Step 2 pushes to a local registry.If you’re running headless (CI,
sshwithout a TTY, etc.), add--no-container-interactive— build.py launches the compile withdocker run -itby default, which aborts withthe input device is not a TTYwhen no terminal is attached.--extra-core-cmake-arg=PYBIND11_FINDPYTHON=ON— makes pybind11 use CMake’s modernFindPython, so the Python 3.12 venv onPATHis what thetritonserverwheel is built against rather than whatever legacy discovery finds first.--extra-core-cmake-arg=Python_LIBRARY=/opt/_internal/nolibpython/libpython3.12.a— the core links libpython into its Python bindings module even though an extension module gets those symbols from the interpreter, and pypa’s only libpython is a static archive that can’t go into a shared library. This points CMake at an empty archive from the Step 1 image so nothing is linked, matching NVIDIA’s released module.--image=pytorch,localhost:5000/…— the prebuilt PyTorch image from Step 2. The other backends (onnxruntime, tensorrt, python) compile from source during the build; PyTorch instead reuses a prebuilt libtorch (too heavy to build in-tree), which the build pulls and extracts from this image — the only backend that needs an--image.TRITON_PYTORCH_ENABLE_TORCHVISION=OFF— this example builds without torchvision; wiring torchvision up from public sources is an untested path in this tutorial.
The python backend is optional — drop --backend=python if you don’t need it. tensorrt is
optional too (ONNX Runtime already pulls in its TensorRT provider); add --backend=tensorrt
for the standalone backend.
Triton’s common, core, backend, and third_party repos don’t need explicit
--repo-tag flags either — build.py defaults them, like the backend and repoagent tags
above, to the checked-out release branch (or main for a dev checkout).
ONNX Runtime is compiled from source here (~2 hours — it builds CUDA kernels for several GPU architectures).
The build produces the install tree under build/install/ and a local tritonserver
Docker image.
Step 4: Verify#
1. Manylinux artifact generation
Now ls the artifacts to verify they were generated correctly:
ls build/install/backends # onnxruntime pytorch python
find build/install -name '*.whl' # ...-cp312-cp312-manylinux_2_34_x86_64.whl
2. Serve real workloads from Manylinux container
Run the server and real inference inside the built image (AlmaLinux 9); the binary needs
glibc 2.34 and won’t start on an EL8 host. First create a model repository with two
OUTPUT0 = INPUT0 + INPUT1 models — one Python, one ONNX.
# python backend model
mkdir -p models/add_py/1
cat > models/add_py/config.pbtxt <<'EOF'
name: "add_py"
backend: "python"
max_batch_size: 0
input [
{ name: "INPUT0", data_type: TYPE_FP32, dims: [4] },
{ name: "INPUT1", data_type: TYPE_FP32, dims: [4] }
]
output [ { name: "OUTPUT0", data_type: TYPE_FP32, dims: [4] } ]
instance_group [ { kind: KIND_CPU } ]
EOF
cat > models/add_py/1/model.py <<'EOF'
import numpy as np
import triton_python_backend_utils as pb_utils
class TritonPythonModel:
def execute(self, requests):
out = []
for r in requests:
a = pb_utils.get_input_tensor_by_name(r, "INPUT0").as_numpy()
b = pb_utils.get_input_tensor_by_name(r, "INPUT1").as_numpy()
t = pb_utils.Tensor("OUTPUT0", (a + b).astype(np.float32))
out.append(pb_utils.InferenceResponse(output_tensors=[t]))
return out
EOF
# onnx model (validates the onnxruntime backend)
mkdir -p models/add_onnx/1
cat > models/add_onnx/config.pbtxt <<'EOF'
name: "add_onnx"
backend: "onnxruntime"
max_batch_size: 0
input [
{ name: "INPUT0", data_type: TYPE_FP32, dims: [4] },
{ name: "INPUT1", data_type: TYPE_FP32, dims: [4] }
]
output [ { name: "OUTPUT0", data_type: TYPE_FP32, dims: [4] } ]
instance_group [ { kind: KIND_CPU } ]
EOF
# generate the .onnx in a temporary container (no host install)
cat > /tmp/gen_onnx.py <<'EOF'
import onnx
from onnx import helper, TensorProto
g = helper.make_graph(
[helper.make_node("Add", ["INPUT0", "INPUT1"], ["OUTPUT0"])], "add",
[helper.make_tensor_value_info("INPUT0", TensorProto.FLOAT, [4]),
helper.make_tensor_value_info("INPUT1", TensorProto.FLOAT, [4])],
[helper.make_tensor_value_info("OUTPUT0", TensorProto.FLOAT, [4])])
# ir_version 7 pairs with opset 13; onnx's default (its newest IR) can exceed what ORT accepts.
onnx.save(helper.make_model(g, ir_version=7, opset_imports=[helper.make_opsetid("", 13)]),
"/models/add_onnx/1/model.onnx")
EOF
docker run --rm -v "$PWD/models:/models" -v /tmp/gen_onnx.py:/gen.py:ro python:3.12-slim \
bash -c "pip install --quiet onnx && python /gen.py"
Start the server (CPU‑only, so no GPU is required for this check). If port 8000 is already in
use, map a free host port instead, e.g. -p 8080:8000, then use localhost:8080 below:
docker run --rm -p8000:8000 -p8001:8001 -v "$PWD/models:/models" \
tritonserver:latest tritonserver --model-repository=/models
# wait for: "successfully loaded 'add_py'" / "'add_onnx'" and "Started HTTPService"
In a second terminal, send inference:
curl -s localhost:8000/v2/health/ready -o /dev/null -w "ready: %{http_code}\n"
# ready: 200
for m in add_py add_onnx; do
echo "== $m =="
curl -s localhost:8000/v2/models/$m/infer -H 'Content-Type: application/json' -d '{
"inputs": [
{"name": "INPUT0", "shape": [4], "datatype": "FP32", "data": [1, 2, 3, 4]},
{"name": "INPUT1", "shape": [4], "datatype": "FP32", "data": [10, 20, 30, 40]}
]
}'
echo
done
Expected output:
== add_py ==
{"model_name":"add_py","model_version":"1","outputs":[{"name":"OUTPUT0","datatype":"FP32","shape":[4],"data":[11.0,22.0,33.0,44.0]}]}
== add_onnx ==
{"model_name":"add_onnx","model_version":"1","outputs":[{"name":"OUTPUT0","datatype":"FP32","shape":[4],"data":[11.0,22.0,33.0,44.0]}]}
A correct add_onnx result means the onnxruntime backend serves on the reconstructed public
base. Stop the server with Ctrl‑C.
PyTorch backend#
The pytorch backend is verified separately, in its own model repo (models_torch/, GPU). First complete the serving image: Dockerfile.pytorch-runtime.rhel installs torch into the image’s Python 3.12 plus the NCCL and cuSPARSELt libraries libtorch links:
cd <tutorials-repo>/Build_Guide/RHEL_Manylinux # Step 3 left you in server/
docker build -f Dockerfile.pytorch-runtime.rhel \
--build-arg TORCH_VERSION="$TORCH_VERSION" --build-arg TORCH_INDEX_URL="$TORCH_INDEX_URL" \
-t tritonserver-pytorch:example .
Add a TorchScript OUTPUT__0 = INPUT__0 + INPUT__1 model (the PyTorch backend uses the
INPUT__N / OUTPUT__N naming convention):
mkdir -p models_torch/add_torch/1
cat > models_torch/add_torch/config.pbtxt <<'EOF'
name: "add_torch"
backend: "pytorch"
max_batch_size: 0
input [
{ name: "INPUT__0", data_type: TYPE_FP32, dims: [4] },
{ name: "INPUT__1", data_type: TYPE_FP32, dims: [4] }
]
output [ { name: "OUTPUT__0", data_type: TYPE_FP32, dims: [4] } ]
instance_group [ { kind: KIND_GPU } ]
EOF
# script the model in the Step 2 image (it already has torch)
cat > /tmp/gen_pt.py <<'EOF'
import torch
class Add(torch.nn.Module):
def forward(self, a, b):
return a + b
torch.jit.script(Add()).save("/models/add_torch/1/model.pt")
EOF
docker run --rm -v "$PWD/models_torch:/models" -v /tmp/gen_pt.py:/gen.py:ro \
triton-manylinux-pytorch:example python /gen.py
Serve with the completed image (KIND_GPU, so a GPU is required) and infer. (Again, if 8000 is
taken, map a free host port — -p 8080:8000, then localhost:8080.)
docker run --rm --gpus all -p8000:8000 -v "$PWD/models_torch:/models" \
tritonserver-pytorch:example tritonserver --model-repository=/models
# wait for: "successfully loaded 'add_torch'", then in a second terminal:
curl -s localhost:8000/v2/models/add_torch/infer -H 'Content-Type: application/json' -d '{
"inputs": [
{"name": "INPUT__0", "shape": [4], "datatype": "FP32", "data": [1, 2, 3, 4]},
{"name": "INPUT__1", "shape": [4], "datatype": "FP32", "data": [10, 20, 30, 40]}
]
}'
echo
Expected output:
{"model_name":"add_torch","model_version":"1","outputs":[{"name":"OUTPUT__0","datatype":"FP32","shape":[4],"data":[11.0,22.0,33.0,44.0]}]}
The PyTorch backend, built entirely from public sources, is serving correct inference. Stop the
server with Ctrl‑C.
Run the checks as tests#
The same two checks are tests in the server repo: qa/L0_rhel_tutorial_cpu (add_py and add_onnx)
and qa/L0_rhel_tutorial_gpu (add_torch). Like every test under qa/, they run inside the server
image: each test.sh writes its models, starts the server, and runs a small pytest suite against
it. Mount the qa/ tree of the server/ checkout from Step 3 and run them the way the server’s
test guide
does:
docker run --rm -v "<server-checkout>/qa:/opt/tritonserver/qa" tritonserver:latest \
bash -c "cd /opt/tritonserver/qa/L0_rhel_tutorial_cpu && bash -ex ./test.sh"
docker run --rm --gpus all -v "<server-checkout>/qa:/opt/tritonserver/qa" tritonserver-pytorch:example \
bash -c "cd /opt/tritonserver/qa/L0_rhel_tutorial_gpu && bash -ex ./test.sh"
Each ends with *** Test Passed, or *** Test FAILED followed by the pytest and server logs.
The tests are on the server’s main branch; if your TRITON_REF predates them, mount a main
checkout’s qa/ instead.
The PyTorch image also runs the server’s own pytorch backend tests that do not need torchvision:
qa/L0_libtorch_inference_mode, qa/L0_libtorch_io_names and qa/L0_libtorch_io_types. They
load models from the server’s QA model repository, so generate its PyTorch part once, on a host
with Docker and a GPU (the generator pulls nvcr.io/nvidia/pytorch; see the
QA model generation notes):
cd <server-checkout>/qa/common
./gen_qa_model_repository.py --pytorch # writes /tmp/<version>/qa_*_model_repository
Then mount that directory where the tests expect it and run each test with the same <version>:
docker run --rm --gpus all -v "<server-checkout>/qa:/opt/tritonserver/qa" \
-v "/tmp/<version>:/data/inferenceserver/<version>:ro" tritonserver-pytorch:example \
bash -c "pip install --quiet 'tritonclient[http]' ml_dtypes && \
cd /opt/tritonserver/qa/L0_libtorch_io_names && bash -ex ./test.sh <version>"
Known differences from the released artifacts#
This build is equivalent, not identical, to the official manylinux release:
Pin versions for parity. The Dockerfile build-args must match the
TRITON_REFyou build — use the Choose a Triton version table. Cross-check against the release’s artifact name (…-cp312-manylinux_2_34-x86_64.zip).Library patch levels can differ slightly from the release: the public
cuda-rhel9repo does not carry the release’s exact cuDNN build (9.25.1.1 here vs 9.25.0.28), the CUDA 13.4 packages install at the repo’s current patch, and the pypa image ships CPython 3.12.14 vs 3.12.13 (hence the Step 2 symlink).The PyTorch backend uses the public torch wheel (
2.14.0+cu132) rather than NVIDIA’s own PyTorch build, and is built without torchvision (Step 3).RHEL 8 hosts can’t run these artifacts.
manylinux_2_34needs glibc 2.34 (EL9); releases up to 26.07 shippedmanylinux_2_28, which also ran on EL8.
For background on build.py and its options, see the server repo’s
build documentation.