CI Docker image pipelines#
This page is for MaxText maintainers and contributors with write access to the
AI-Hypercomputer/maxtext repository. It explains when the CI builds Docker
images, and how to re-run the image-based tests without paying for a full
rebuild. To build images on your own machine, see
Build and upload MaxText Docker images.
When are Docker images built?#
Exactly one workflow builds images: .github/workflows/build_and_push_docker_image.yml.
It is a reusable workflow (workflow_call), so it never starts on its own. Three
workflows call it, and these are the only events that build an image:
Caller |
Trigger |
Images |
|---|---|---|
TPU Docker Images Pipeline ( |
Nightly at 00:00 UTC, or manual Run workflow |
|
GPU Docker Images Pipeline ( |
Nightly at 00:00 UTC, or manual Run workflow |
|
Release Pipeline ( |
GitHub release published, or manual Run workflow |
Release images. Always a full build. |
Nothing else builds an image:
A
git pushor a pull request never builds an image. The PR pipeline (ci_pipeline.yml) builds a wheel from your commit (about 35 seconds), starts a container from the prebuiltmaxtext-unit-test-tpu/maxtext-unit-test-cuda12base image, installs the wheel into a fresh virtual environment (about 20 seconds) and runs the tests. Pushing a fix re-runs everything in roughly 1.5 minutes plus test time, and a push cancels the previous in-progress run for the same PR.The
maxtext-unit-test-*base images contain only system packages, Python, pip and uv. They are built by hand fromsrc/dependencies/dockerfiles/clean_py_env*.Dockerfileand pushed rarely.
Every image pipeline run pushes
us-docker.pkg.dev/<project>/<repository>/<image>:<run_id> before the tests
run, then runs the CI test suite against that tag and, on success, adds the tag
verified-ci-<run_id>. A failed test run does not delete the image, so it can be
reused by the modes below.
Re-running tests without rebuilding images (fast-rebuild)#
When an image pipeline run fails because of a flaky test or a code bug, you often want to re-run the tests with the exact same dependencies but updated code, without waiting for a full image rebuild.
Three execution modes#
The TPU and GPU Docker Image Pipelines support three modes:
Mode |
What it does |
When to use |
|---|---|---|
build-all (default) |
Full image build from scratch |
Nightly schedule, first build on a branch, dependency changes |
fast-rebuild |
Overlay new code on a previous run’s image |
Flaky test rerun with a fix, code-only iteration |
Re-run failed jobs (GitHub native) |
Re-run only failed jobs using the same image |
Same code, same image, just retry |
How to choose:
Flaky test, code unchanged: open the failed run and click Re-run failed jobs. The run keeps its
run_id, so it reuses the<image>:<run_id>already in the registry. No build at all. Needs the run’s commit to still be on a branch. After a squash or force-push the re-run fails at startup; dispatch a fast-rebuild from that run instead.Code fix, dependency files unchanged: fast-rebuild with
source_run_idset to the failed run (or to the latest nightly run onmain).Anything under
src/dependencies/,pyproject.tomlor.dockerignorechanged: build-all.
Using fast-rebuild#
From the GitHub UI (Actions tab):
Go to Actions → TPU Docker Images Pipeline (or GPU).
Click “Run workflow” and pick your branch.
Set mode to
fast-rebuild.Optionally enter source_run_id: the run ID of the previous pipeline run whose image you want to reuse. It is the last number in the run’s URL, for example
https://github.com/.../actions/runs/34793910551→34793910551. Leave it empty to auto-detect from branch history ormain’s nightly.Click “Run workflow”.
From the command line with the GitHub CLI:
# Find the run ID of the source image (the run whose dependencies you want to reuse)
gh run list --workflow=tpu_docker_images_pipeline.yml --limit 5
# Rebuild the images with the code on <branch> on top of that run's dependency layers
gh workflow run tpu_docker_images_pipeline.yml --ref <branch> \
-f mode=fast-rebuild -f source_run_id=<run_id>
# Or omit source_run_id to auto-detect from branch history or main's nightly:
gh workflow run tpu_docker_images_pipeline.yml --ref <branch> -f mode=fast-rebuild
Use gpu_docker_images_pipeline.yml for the GPU images. The new run appears in the
Actions list as Fast-rebuild from run <run_id>.
If source_run_id is left empty, the pipeline auto-detects the source run:
Current branch: the most recent run on this branch where all image builds succeeded.
mainfallback: if no valid run exists on the branch, the most recent successful nightly onmain.Explicit input always wins: a provided
source_run_idis used as-is, skipping auto-detection.
The drift guard still runs after resolution, so a main fallback with different
dependencies is caught and refused before any build runner starts.
Other dispatch inputs:
repository_name: the Artifact Registry repository the images are pushed to (maxtext-imagesfor the nightly images). A failure issue is only opened formaxtext-images. In fast-rebuild mode the source run must have pushed to the same repository.allow_dependency_drift: overrides the dependency drift guard. Not recommended, see below.run_e2e_tests: also runs the Airflow end-to-end tests after the build.
The pipeline will:
Build a new wheel from your current code
Run the dependency drift guard as a lightweight pre-flight job, so a bad request fails before a build runner starts
Verify the source image exists in the registry
Build a new image that uses the source image’s dependency layers with your new code overlaid
Run the full CI test suite against the new image
Typical workflow on a feature branch#
First run on the branch, or after changing dependency files: dispatch with
mode=build-all.Tests fail. Fix the code and push. Dispatch again with
mode=fast-rebuildandsource_run_idset to the run from step 1. Only the code layers are rebuilt; the dependencies are the same bytes as in step 1.A test flaked and the code did not change: Re-run failed jobs on that run.
Another fix after step 2: dispatch
mode=fast-rebuildagain withsource_run_idset to the step 2 run. A fast-rebuild run can be the source of the next one, so one full build carries a branch through many fixes.
If your branch does not touch dependency files, skip step 1 and run
mode=fast-rebuild without a source_run_id. The pipeline picks the latest
nightly run on main automatically. The drift guard verifies that the two commits
have identical dependency files before anything is built.
How it works#
Fast-rebuild starts from the source run’s image by digest
(FROM <source_image>@sha256:<digest>), so the OS packages, Python dependencies and
JAX/XLA layers are reused byte-for-byte. Only the code directories (src/maxtext/,
tests/, benchmarks/, pytest.ini) come from your commit. There are two overlay
Dockerfiles under src/dependencies/dockerfiles/, and the drift guard picks one:
Fast (
maxtext_code_overlay_fast.Dockerfile):COPY --linkonly, noRUN. BuildKit never pulls the source image’s layers and only uploads the new code layers. Used whenever no code file was deleted or renamed since the source commit and, for post-training images,src/maxtext/integration/vllm/is unchanged.Clean (
maxtext_code_overlay.Dockerfile): removes the old code directories first, re-downloads the test assets and re-installs the vLLM adapter for post-training images. Exact even when files were removed, but theRUNsteps force BuildKit to pull the 3 GB source image into the builder, so it costs about as much as a full build.
The build log prints the choice (“Overlay variant selected by the drift guard: fast”) and the reason appears in the pre-flight job.
Measured on the TPU pipeline (pre-training stable image, September 2026):
Build |
Docker build step |
Whole build job |
|---|---|---|
Full build ( |
about 200 s |
about 5 min |
Fast overlay |
about 10 s |
about 1 min (2 min if the runner is cold) |
Clean overlay |
about 200 s |
about 5 min |
The remaining time in a fast job is the job’s own container start-up, checkout and wheel install, not the image build. The CI tests that follow take the same time as in any other run.
This guarantees that when you are debugging a test failure, the dependencies are provably identical to the source run, not just “probably cached”.
Dependency drift guard#
If any dependency-related file changed between the source commit and your current commit, the fast-rebuild fails with an error listing the changed files. This prevents silently using stale dependencies.
This includes changes you did not make yourself: after a rebase or merge from main
that brought in new dependency files, the guard refuses every source run from before
that point. Run build-all once on the new base and continue from that run.
The guard runs from the workflow’s own checkout and only reads the two commits as git data; it never executes code from the commit being built.
Files checked:
src/dependencies/(requirements, scripts, dockerfiles, extra_deps)pyproject.toml.dockerignore
To override (not recommended): set allow_dependency_drift to true in the
workflow dispatch form. The resulting image may then have different dependencies
than the source image.
Limitations#
Fast-rebuild requires the source image to still exist in the registry. Images may be garbage-collected after some time. If the lookup fails, the build log lists the most recent tags of that image.
The source run’s commit must still be fetchable from GitHub. A commit that was rewritten by a squash or force-push stays fetchable until GitHub garbage-collects it. If the pre-flight step fails with
Could not fetch commit, runbuild-allonce and continue from that run.Re-run failed jobs needs the run’s commit to still be on a branch. After a squash or force-push, GitHub fails the re-run at startup (“This run likely failed because of a workflow file issue”). A fast-rebuild from that run still works, because the pre-flight fetches the source commit by SHA.
The source run must have pushed to the same
repository_name.Each fast-rebuild adds a code layer of a few tens of MB on top of the source image. The nightly
build-allruns reset this.The fast overlay never deletes files. The guard therefore switches to the clean overlay when a code file was removed or renamed, which is slow but exact.
Fast-rebuild is manual by design. Nightly and release builds always use
build-all.