Docker Setup Guide
llenergymeasure uses Docker to run vLLM and TensorRT-LLM (and future SGLang) engines in
isolated containers with GPU access. This guide walks through setting up Docker with GPU support
from scratch on a Linux host.
Docker is the supported path. All published measurements run in containers - that is what gives you reproducibility, dependency isolation between engines, and access to vLLM and TensorRT-LLM. The Transformers engine still has a
processrunner mode, but it is a developer convenience for smoke-testing on a host with a working CUDA toolchain, not a recommended way to produce results.
Prerequisites
Before starting, confirm you have:
- A Linux host (GPU passthrough in Docker is Linux-only - no macOS or Windows Docker GPU support)
- An NVIDIA GPU installed
- Root or sudo access to install system packages
No prior Docker knowledge is assumed. Each step includes a verification command.
Step 1: Install Docker
If Docker is already installed, skip to the verification step.
Follow the official Docker Engine install guide for your distribution. For Ubuntu/Debian, Docker's own script is the easiest path:
# Install Docker Engine (Ubuntu/Debian)
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify Docker Compose and Buildx versions (v2.32+ and v0.17+ recommended for fast rebuilds):
docker compose version # need v2.32+
docker buildx version # need v0.17+
If your versions are below these, you can upgrade the plugins directly:
# Upgrade Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" \
-o /usr/libexec/docker/cli-plugins/docker-compose
sudo chmod 755 /usr/libexec/docker/cli-plugins/docker-compose
# Upgrade Docker Buildx (find latest version at https://github.com/docker/buildx/releases)
BUILDX_VERSION=v0.32.1
sudo curl -L "https://github.com/docker/buildx/releases/download/${BUILDX_VERSION}/buildx-${BUILDX_VERSION}.linux-amd64" \
-o /usr/libexec/docker/cli-plugins/docker-buildx
sudo chmod 755 /usr/libexec/docker/cli-plugins/docker-buildx
Post-install step: Add your user to the docker group so you can run Docker without sudo:
sudo usermod -aG docker $USER
newgrp docker
Verify Docker is working:
docker run hello-world
Expected output includes Hello from Docker!.
Rootless Docker and Podman
The tested and supported runtime is rootful Docker Engine (the standard install
above). llem resolves whatever docker binary is on PATH, so a
Docker-compatible CLI - rootless Docker, or Podman exposed as docker - may work,
and llem records the detected runtime in its environment snapshot. These paths
are not tested and GPU passthrough on them is not validated: rootless Docker
and Podman each have their own NVIDIA Container Toolkit / CDI setup that differs
from rootful Docker. If you must use one, verify GPU passthrough with the container
check in Step 4 first, and configure the
toolkit per your runtime's own documentation. For a supported setup, use rootful
Docker.
BuildKit builder setup (recommended)
Docker image builds use BuildKit under the hood. The default builder has a conservative garbage-collection limit (~10% of disk) that is too small when building all three engine images (Transformers, vLLM, TensorRT). This causes build cache eviction and expensive recompilation (FA3 takes ~1 hour from scratch).
Create a dedicated builder with a 200 GiB cache limit:
make docker-builder-setup
This creates a docker-container driver builder called llem-builder with tuned GC limits
(configured in docker/buildkitd.toml). To use it, set the BUILDX_BUILDER environment
variable:
export BUILDX_BUILDER=llem-builder
docker compose build
Or add BUILDX_BUILDER=llem-builder to your .env file for project-scoped use.
The command is idempotent - running it again is a no-op if the builder already exists.
To recreate the builder (e.g. after changing buildkitd.toml):
make docker-builder-rm
make docker-builder-setup
Step 2: Install NVIDIA Drivers
If nvidia-smi already works on your host, skip this step.
nvidia-smi
If nvidia-smi is not found or returns an error, install NVIDIA drivers for your distribution.
Follow the NVIDIA driver installation guide
for your OS and GPU model.
Expected nvidia-smi output (your GPU name and driver version will differ):
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.14 Driver Version: 550.54.14 CUDA Version: 12.4 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
|=========================================================================================|
| 0 NVIDIA A100-SXM4-80GB On | 00000000:00:04.0 Off | 0 |
| N/A 34C P0 54W / 400W | 0MiB / 81920MiB | 0% Default |
+-----------------------------------------------------------------------------------------+
Step 3: Install NVIDIA Container Toolkit
The NVIDIA Container Toolkit enables Docker containers to access the host GPU. This is the
critical step that makes docker run --gpus all work.
Ubuntu/Debian:
# Add NVIDIA Container Toolkit repository
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
# Configure Docker to use the NVIDIA runtime
sudo nvidia-ctk runtime configure --runtime=docker
# Restart Docker for the configuration to take effect
sudo systemctl restart docker
Other distributions: Follow the NVIDIA Container Toolkit install guide for RHEL/CentOS, Fedora, SUSE, or Arch Linux.
Step 4: Verify GPU Access in Docker
Run a container with GPU access and check that nvidia-smi works inside it:
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
This command:
- Pulls a small CUDA base image (~100 MB)
- Launches a container with all host GPUs passed through (
--gpus all) - Runs
nvidia-smiinside the container - Exits and removes the container (
--rm)
Expected output: the same nvidia-smi table you saw in Step 2, but printed from inside
the container.
This is exactly the check that
llem's Docker pre-flight performs automatically before launching any engine container. If this command works,llempre-flight will pass.
If this command fails, see Troubleshooting below.
Step 5: Verify with LLenergyMeasure
With Docker and NVIDIA CT installed, verify that llem sees the GPU:
llem doctor
Expected output shows your GPU under the GPU / driver section and the Docker
runner availability under Docker and Engines.
See Installation for the full llem doctor output format.
Run a vLLM experiment by creating a YAML file:
# experiment.yaml
serving_mode: offline
engine: vllm
task:
model: gpt2
dataset:
source: aienergyscore
n_prompts: 50
runners:
vllm: container
Then run it:
llem run experiment.yaml
llem will automatically pull the vLLM Docker image on first use, launch a container, run
the experiment inside it, and return the results. See Getting Started for
an annotated walkthrough.
Run a TensorRT-LLM experiment by creating a YAML file:
# experiment.yaml
serving_mode: offline
engine: tensorrt
task:
model: meta-llama/Llama-2-7b-hf
dataset:
source: aienergyscore
n_prompts: 50
runners:
tensorrt: container
Then run it:
llem run experiment.yaml
llem will pull the TensorRT-LLM Docker image and run inference. The default
pytorch backend loads the model directly; the compiled trt backend instead
compiles a TensorRT engine (first run of a config only - several minutes) and
caches it on disk. See Getting Started
for the full TensorRT-LLM walkthrough.
Image Management
Image sources
each engine has two possible image sources:
| Source | Tag pattern | Built by | Use case |
|---|---|---|---|
| Local build | llenergymeasure:{engine} | make docker-build-{engine} | Development - reflects current source tree |
| Registry (transformers) | ghcr.io/henrycgbaker/llenergymeasure/transformers:v{package_version} | CI on release tags | Production, CI, pip-install users |
| Upstream (vllm, tensorrt) | vllm/vllm-openai:v{engine_version}, nvcr.io/nvidia/tensorrt-llm/release:{engine_version} | vLLM / NVIDIA (NGC) | Same - the canonical engine image, project source bind-mounted |
Image resolution
When you run llem run, the tool resolves which image to use for each engine. Resolution
follows a precedence chain (highest wins):
- Environment variable
LLEM_IMAGE_{ENGINE}(e.g.LLEM_IMAGE_VLLM=my/custom:tag) - Study YAML
images:section - Runner spec shorthand (
container:my/custom:taginrunners:) - User config
images:section (~/.config/llenergymeasure/config.yaml) - Smart default: local build image if present, otherwise registry image
In practice, most users rely on the smart default (level 5). If you have built images locally
with make docker-build, those are used. Otherwise, llem resolves the per-engine default:
the first-party GHCR image for transformers (at the package version), and the canonical
upstream image for vLLM / TensorRT-LLM (vllm/vllm-openai,
nvcr.io/nvidia/tensorrt-llm/release, at the pinned engine version).
Heads-up: a stale local tag silently wins. Because the local build takes precedence, a months-old
llenergymeasure:{engine}tag keeps winning level 5 long after the pinned default has moved on, which can surface as a schema-handshake mismatch. When a local tag wins,llemlogs a warning naming the pinned default it bypassed, andllem doctorflags it on the engine's row. To restore the pinned default, remove the local tag withdocker rmi llenergymeasure:{engine}; to keep a specific image, pin it explicitly at level 1-4 (for exampleLLEM_IMAGE_{ENGINE}).
Auto-pull on first use
When llem needs a registry image that is not cached locally, it pulls it automatically
before the experiment runs. The preflight panel shows which images will be used and whether
they are cached or need pulling:
Runners
vllm docker
↳ llenergymeasure:vllm ← local build
transformers docker
↳ ghcr.io/henrycgbaker/llenergymeasure/transformers:v0.6.0 ← registry
Study-level image preparation
For multi-experiment studies, llem checks and pulls all required Docker images once
before the first experiment runs, not per-experiment. This avoids redundant pulls when
multiple experiments share the same engine image. The CLI shows this as a
"Preparing Docker images" section with per-image status (cached vs pulled) and metadata
(image ID, size, age, layers).
Pre-fetch images manually
For offline environments or to avoid pull latency during experiments:
# Pull the first-party transformers image for your installed version
make docker-pull
# Or pull each engine's default image individually. transformers is a
# first-party GHCR image tagged with the llenergymeasure version; vLLM and
# TensorRT-LLM are upstream images tagged with the pinned engine version.
docker pull ghcr.io/henrycgbaker/llenergymeasure/transformers:v0.6.0
docker pull vllm/vllm-openai:v0.19.1
docker pull nvcr.io/nvidia/tensorrt-llm/release:1.2.1
Replace the transformers tag with your installed version (llem --version); the
vLLM and TensorRT-LLM tags track the pins in engine_versions/<engine>/current.yaml.
llem doctor prints the exact image each engine resolves to.
Check current image resolution
To see which images llem will use for each engine:
make docker-images
Output shows local vs registry source for each engine:
=== Image resolution ===
tensorrt -> nvcr.io/nvidia/tensorrt-llm/release:1.2.1 (registry)
transformers -> ghcr.io/henrycgbaker/llenergymeasure/transformers:v0.6.0 (registry)
vllm -> vllm/vllm-openai:v0.19.1 (registry)
Building or pulling images locally
Only the Transformers engine is built from a project Dockerfile. vLLM and TensorRT-LLM use canonical upstream images directly and bind-mount the project source at run time.
# Transformers - build from project source
make docker-build
# vLLM - pull upstream
docker pull vllm/vllm-openai:v0.19.1
# TensorRT-LLM - pull upstream (NGC)
docker pull nvcr.io/nvidia/tensorrt-llm/release:1.2.1
make docker-build builds the project's first-party engine images
(currently just transformers - the only project-built image). It uses
docker compose build under the hood and pulls cached layers from GHCR
on first build (see
Fast rebuilds and first-pull cost
for the full mechanism).
Advanced. Setting
COMPOSE_BAKE=trueroutes builds throughbuildx bakefor parallel multi-engine builds. With the current cache architecture this is rarely worth enabling - vLLM/TRT cold builds are already 4-13 min and warm rebuilds are seconds, so the parallelism gain is small. Left out of.env.exampleto avoid noise; opt in only if you frequently runmake docker-buildfrom cold.
When to rebuild. Images bundle the
llenergymeasuresource at build time. If you modify config models, engines, or the container entrypoint, rebuild for changes to take effect inside containers. Process-runner experiments (Transformers without a container) use the installed source directly and do not need a rebuild.
Override images in YAML
runners:
transformers: process # host execution, no container
vllm: container # default resolution (local → registry)
tensorrt: "container:my/custom:tag" # explicit image override
Future engines
SGLang images (when SGLang ships) will follow the same naming convention, resolution logic, and auto-pull behaviour. No additional setup is needed when SGLang ships.
Layer cache sharing via GHCR registry
See installation.md - Fast rebuilds and first-pull cost
for the user-facing walkthrough (mechanism, sizes, authentication, offline fallback)
and the ref breakdown of what the local seed and the promotion publish
(transformers-cache:transformers-<VER> promotion source, its
-buildcache companion, and the canonical
transformers:transformers-<VER>/transformers:latest).
Operator notes:
make docker-seed-transformerswrites the:transformers-<VER>-buildcacheref (separate from the runnable promotion-source image), so storage growth is bounded by version drift.- Inspect what's cached on the active builder:
docker buildx du --builder llem-builder. - If the cache is corrupt, recreate it with
make docker-builder-rm && make docker-builder-setup. The remote buildcache ref repopulates on the next local seed.
TensorRT-LLM engine build cache
Distinct from the Docker layer cache above, and specific to the compiled trt
backend: the TensorRT engine build cache holds compiled .engine artefacts so
re-running the same experiment config skips the multi-minute compile. It ships
on out of the box - .env.example sets LLEM_TRT_BUILD_CACHE_ENABLED=1
(delete the line to fall back to TensorRT-LLM's disabled default). It lives on
the host at ~/.cache/trt-llm and is bind-mounted into every tensorrt
container at /root/.cache/trt-llm; llem defaults the cache location to that
mount out of the box (override with LLEM_TRT_BUILD_CACHE_PATH). TensorRT-LLM
keys each entry by the full build config (model, dtype, tensor-parallel size,
max-shape, quantisation) plus the engine version: an identical config hits even
across separate containers, while a TP / quantisation / max-shape change or a
version bump keys to a distinct engine and misses. Each result records
engine_build_cache_hit so you can tell a reused engine from a fresh compile.
The lifecycle is manual and visible - llem never auto-evicts. Inspect it with:
llem doctor
which reports the cache location, engine-entry count, and total size. Entries
are large (often 1-15 GB each); clean the cache manually when it grows.
Entries are written by the container's root process, so removing them from the
host needs sudo:
sudo rm -rf ~/.cache/trt-llm/engine-*
Image labels
docker/Dockerfile.transformers stamps a single OCI label:
| Label | Purpose |
|---|---|
org.opencontainers.image.source | Points at the GitHub repository |
Inspect the labels on a local image:
docker image inspect llenergymeasure:transformers \
--format '{{json .Config.Labels}}' | python3 -m json.tool
Schema-fingerprint label is legacy; engine-version probe is live. Earlier versions stamped
org.opencontainers.image.versionandllem.expconf.schema.fingerprintlabels at build time soStudyRunner._prepare_imagescould detect host/container schema skew. Once the image stopped baking the project source (it is bind-mounted at runtime - see the development guide), the schema-fingerprint check became structurally redundant: the in-container source always equals the host source. That label is no longer set on first-party images and never existed on upstream-direct images (vllm, tensorrt). Whatversion_handshake.pydoes today is a different check: it probes each image's engine library version (vllm.__version__,tensorrt_llm.__version__,transformers.__version__) and compares it against the engine_version envelope on the wheel-bundled rules
- schema artefacts. A real library/artefact mismatch is a hard error; set
LLEM_SKIP_IMAGE_CHECK=1to bypass if you know the skew is harmless.
See troubleshooting.md for the remediation flow when a mismatch is reported.
Troubleshooting
"docker: Error response from daemon: could not select device driver"
NVIDIA Container Toolkit is not installed, or Docker was not restarted after configuration.
Fix:
- Confirm toolkit is installed:
which nvidia-ctkshould return a path. - Re-run:
sudo nvidia-ctk runtime configure --runtime=docker - Restart Docker:
sudo systemctl restart docker - Retry the
docker run --gpus all ...command.
"nvidia-container-cli: initialization error"
A driver version mismatch between the host NVIDIA driver and the CUDA version in the container.
Fix: Check your host driver version with nvidia-smi. The container image requires a
minimum driver version for its CUDA release. See the
CUDA compatibility matrix to confirm
your driver supports the container's CUDA version.
For example, CUDA 12.4 requires driver >= 525.60.13.
"Permission denied" when running docker commands
Your user is not in the docker group.
Fix:
sudo usermod -aG docker $USER
newgrp docker # apply group change in current shell
Or log out and log back in. Verify with: groups | grep docker.
GPU not visible inside container
The --gpus all flag is missing from the docker run command.
llem adds --gpus all automatically when launching engine containers. If you are running
Docker commands manually, ensure you include --gpus all (or --gpus device=0 for a specific
GPU).
To make llem itself target specific GPUs on a shared host, there are two levers:
LLEM_DOCKER_GPUS(env var, host-wide). Set it to thedocker run --gpusvalue (empty means every visible GPU). Quote multi-device values so the shell keeps the comma, e.g.LLEM_DOCKER_GPUS="device=2,3".study_execution.gpu_indices(study YAML, per study). A list of host GPU indices, e.g.gpu_indices: [2, 3], translated to--gpus device=2,3. This lets a study YAML declare its own GPU placement without an env var.
Both are host device indices as the NVIDIA driver / NVML enumerate them (what nvidia-smi
shows). Restricting at the docker level (rather than setting CUDA_VISIBLE_DEVICES inside the
container) keeps CUDA and NVML indices consistent inside the container - both re-enumerate from
0 - so energy attribution addresses the correct physical device without any index translation.
Precedence: LLEM_DOCKER_GPUS (env) overrides study_execution.gpu_indices (config). When
both are set the env wins and llem logs a one-line warning; the config indices are ignored.
Because the env fully overrides the config, the two never compose - there is no "config indices
index into the env-restricted set" case to reason about. Pick one lever per run.
For tensor-parallel runs, llem also forwards every NCCL_* host variable into the container;
see multi-GPU with TensorRT-LLM.
LLEM_DOCKER_GPUS also accepts a Multi-Instance GPU (MIG) instance UUID
(device=MIG-<uuid>) to pin llem to a single MIG slice on a partitioned A100 or
H100. See Running on a cloud GPU VM - MIG and partitioned GPUs
for the operational steps and the power-telemetry caveat.
Shared memory errors with vLLM
vLLM requires more than the default 64 MB of shared memory (/dev/shm). llem automatically
sets --shm-size 8g when launching engine containers. Override the size with the
LLEM_DOCKER_SHM_SIZE env var (e.g. LLEM_DOCKER_SHM_SIZE=16g for very large tensor-parallel
runs, or a smaller value on memory-constrained hosts); empty means the 8g default. If you are
running the vLLM container manually, add --shm-size 8g to your docker run command.
Model cache location
llem bind-mounts the host HuggingFace cache into each engine container so model weights persist
across runs instead of re-downloading. Override the host directory with the LLEM_DOCKER_HF_CACHE
env var (default $HOME/.cache/huggingface) to point at shared storage or a larger disk.
Pre-flight check failures
llem runs pre-flight checks before launching any Docker container. The checks and their
failure modes:
| Check | Failure message | Fix |
|---|---|---|
| Docker CLI on PATH | Docker not found on PATH | Install Docker Engine (Step 1) |
| NVIDIA Container Toolkit binary | NVIDIA Container Toolkit not found | Install NVIDIA CT (Step 3) |
Host nvidia-smi | Warning (non-blocking) | Expected if using remote Docker daemon |
| GPU visibility in container | GPU not accessible inside Docker container | Re-run Steps 3-4 |
| CUDA/driver compatibility | CUDA/driver compatibility error inside container | Update host driver |
To bypass pre-flight checks temporarily (not recommended for production):
llem run experiment.yaml --skip-preflight
Keeping Engine Schemas Fresh
When you update an engine version (by bumping the pin in
engine_versions/<engine>/current.yaml), the discovered parameter schema must
be regenerated. This is a local maintainer step - CI verifies the committed
schema but never runs discovery itself. Run:
./scripts/refresh_discovered_schemas.sh <engine> # equivalently: make discover-schema ENGINE=<engine>
The script runs discovery inside the engine's Docker image, writes
src/llenergymeasure/engines/<engine>/schema.discovered.json, and prints the
diff for you to review and commit. See
Schema refresh (operations guide) for the full
workflow and the schema-version-check CI guard.
Next Steps
- Getting Started - run your first vLLM or TensorRT-LLM experiment
- Engine Configuration - configure vLLM, TensorRT-LLM, and switch between engines
- Fast rebuilds and first-pull cost - how the GHCR layer cache speeds up local Docker builds
- Running on a cloud GPU VM - AWS/GCP/Azure quickstart, provider images, and MIG guidance