From 2743fc0235fefa4632a0134aede9d5affc197459 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 6 Jun 2026 08:50:48 -0400 Subject: [PATCH 1/2] feat: add pinned CUDA manylinux image aliases The manylinux_cuda project publishes manylinux containers bundling the CUDA Toolkit (documented in #2896). Add pinned aliases for them so users can pass a short name (e.g. manylinux_2_28_cuda13_1) to the manylinux-*-image options, just like the existing manylinux_2_28 alias, instead of a full registry URL. These images are only published with a 'latest' tag, and each architecture has its own repository, so (unlike the PyPA images) they are pinned by digest. bin/update_docker.py learns to resolve and pin these, keeping them up to date via `nox -s update_pins`. Aliases are added for x86_64 and aarch64 (the only arches these images exist for): manylinux_{2_28,2_34}_cuda{12_9,13_1}. Assisted-by: ClaudeCode:claude-opus-4.8 Co-Authored-By: Claude Opus 4.8 --- bin/update_docker.py | 38 +++++++++++++++++++ .../resources/pinned_docker_images.cfg | 8 ++++ docs/faq.md | 36 +++++++++--------- unit_test/options_test.py | 22 +++++++++++ 4 files changed, 86 insertions(+), 18 deletions(-) diff --git a/bin/update_docker.py b/bin/update_docker.py index 8eccde3af..89770f439 100755 --- a/bin/update_docker.py +++ b/bin/update_docker.py @@ -33,6 +33,21 @@ def __init__(self, manylinux_version: str, platforms: list[str], tag: str | None super().__init__(manylinux_version, platforms, image_name, tag, True) +class CudaImage(Image): + """ + A CUDA manylinux image from the manylinux_cuda project. + + Each architecture has its own repository and the images are only published + with a ``latest`` tag, so (unlike the PyPA images) these are pinned by + digest. ``{arch}`` in the image name is substituted per platform. + """ + + def __init__(self, manylinux_version: str, cuda_version: str, platforms: list[str]): + alias = f"{manylinux_version}_cuda{cuda_version}" + image_name = f"quay.io/manylinux_cuda/{manylinux_version}_{{arch}}_cuda{cuda_version}" + super().__init__(alias, platforms, image_name) + + images = [ # manylinux2014 images PyPAImage( @@ -86,11 +101,34 @@ def __init__(self, manylinux_version: str, platforms: list[str], tag: str | None PyPAImage( "musllinux_1_2", ["x86_64", "i686", "aarch64", "ppc64le", "s390x", "armv7l", "riscv64"] ), + # CUDA manylinux images (x86_64 and aarch64 only, pinned by digest) + *( + CudaImage(manylinux_version, cuda_version, ["x86_64", "aarch64"]) + for manylinux_version in ("manylinux_2_28", "manylinux_2_34") + for cuda_version in ("12_9", "13_1") + ), ] config = configparser.ConfigParser() for image in images: + if "{arch}" in image.image_name: + # Per-architecture repositories pinned by digest (the 'latest' tag is + # the only published tag, so there is no dated tag to pin to). + for platform in image.platforms: + arch = platform.removeprefix("pypy_") + image_name = image.image_name.format(arch=arch) + _, _, repository_name = image_name.partition("/") + response = requests.get( + f"https://quay.io/api/v1/repository/{repository_name}?includeTags=true" + ) + response.raise_for_status() + digest = response.json()["tags"]["latest"]["manifest_digest"] + if not config.has_section(platform): + config[platform] = {} + config[platform][image.manylinux_version] = f"{image_name}@{digest}" + continue + # get the tag name whose digest matches 'latest' if image.tag is not None: # image has been pinned, do not update diff --git a/cibuildwheel/resources/pinned_docker_images.cfg b/cibuildwheel/resources/pinned_docker_images.cfg index 461e6bf30..57d73adc0 100644 --- a/cibuildwheel/resources/pinned_docker_images.cfg +++ b/cibuildwheel/resources/pinned_docker_images.cfg @@ -3,6 +3,10 @@ manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2026.06.04-1 manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2026.06.04-1 manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2026.06.04-1 musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2026.06.04-1 +manylinux_2_28_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda12_9@sha256:a36d4f9da7552935b3f925c486f899234fb4ded34712cc066efcc7cccbffb493 +manylinux_2_28_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda13_1@sha256:9018e4a6241cce193ca360a206a5db940831c9857e14229308991e7f037d691e +manylinux_2_34_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_34_x86_64_cuda12_9@sha256:b7e0faeebe1767b8917fb2afe9a989f3add0d198f4e9d8d8fb5a5bfd13a52864 +manylinux_2_34_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_34_x86_64_cuda13_1@sha256:c5000751022c1d9ad7152b45e30bbf4d0871175a7e7c30c4f25fafd480378917 [i686] manylinux2014 = quay.io/pypa/manylinux2014_i686:2026.06.04-1 @@ -15,6 +19,10 @@ manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2026.06.04-1 manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2026.06.04-1 manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2026.06.04-1 musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2026.06.04-1 +manylinux_2_28_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda12_9@sha256:b7ceb066edb6790b35d550642d21b2018eb7c177270dc96dd13de81744afb702 +manylinux_2_28_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda13_1@sha256:7a8e559eac1597055ff5f5cabc1c4e238bb5bc13c988f59f0e49a521f3c9dae4 +manylinux_2_34_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_34_aarch64_cuda12_9@sha256:f1d7f2e0e50e8ded421250db3757157a6fadedc59f905cb640d2908dc61a82ce +manylinux_2_34_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_34_aarch64_cuda13_1@sha256:40cab0122de2ff89ae5f2171cd76201b85f368bc3e2af1ff5c9b96b608aa5a86 [ppc64le] manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2026.06.04-1 diff --git a/docs/faq.md b/docs/faq.md index 456022a40..4699a47e0 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -224,24 +224,26 @@ Consider incorporating these into your package, for example, in `setup.py` using ### Building wheels with CUDA on Linux On Linux, you can build binary wheels with CUDA to take advantage of NVIDIA GPUs for hardware acceleration. -Specify the custom Docker containers with CUDA Toolkit as follows: +The [manylinux_cuda](https://quay.io/organization/manylinux_cuda) project publishes manylinux containers that bundle the CUDA Toolkit, and cibuildwheel ships pinned aliases for them. Just like `manylinux_2_28`, you can pass an alias to the `manylinux-*-image` options and cibuildwheel will expand it to a specific, pinned image digest: ```yaml -CIBW_MANYLINUX_X86_64_IMAGE: >- - quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda13_1:latest -CIBW_MANYLINUX_AARCH64_IMAGE: >- - quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda13_1:latest +CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28_cuda13_1 +CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28_cuda13_1 ``` -Currently, we support the following CUDA manylinux containers: -* `quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda12_9:latest` -* `quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda12_9:latest` -* `quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda13_1:latest` -* `quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda13_1:latest` -* `quay.io/manylinux_cuda/manylinux_2_34_x86_64_cuda12_9:latest` -* `quay.io/manylinux_cuda/manylinux_2_34_aarch64_cuda12_9:latest` -* `quay.io/manylinux_cuda/manylinux_2_34_x86_64_cuda13_1:latest` -* `quay.io/manylinux_cuda/manylinux_2_34_aarch64_cuda13_1:latest` +The following CUDA aliases are available (for `x86_64` and `aarch64` only): + +* `manylinux_2_28_cuda12_9` +* `manylinux_2_28_cuda13_1` +* `manylinux_2_34_cuda12_9` +* `manylinux_2_34_cuda13_1` + +These aliases resolve to images under `quay.io/manylinux_cuda/`, named +`manylinux___cuda` (e.g. +`quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda13_1`). If you want a CUDA/glibc/arch +combination that isn't aliased above, or you'd rather track the latest build yourself, you +can point at the repository directly with an explicit tag or digest, e.g. +`quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda13_1:latest`. A typical GitHub Actions workflow will look like this: @@ -267,10 +269,8 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v3 env: - CIBW_MANYLINUX_X86_64_IMAGE: >- - quay.io/manylinux_cuda/${{ matrix.manylinux-base }}_x86_64_cuda${{ matrix.cuda-version }}:latest - CIBW_MANYLINUX_AARCH64_IMAGE: >- - quay.io/manylinux_cuda/${{ matrix.manylinux-base }}_aarch64_cuda${{ matrix.cuda-version }}:latest + CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux-base }}_cuda${{ matrix.cuda-version }} + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux-base }}_cuda${{ matrix.cuda-version }} CIBW_BUILD: cp312-manylinux_${{ matrix.target.arch }} ``` diff --git a/unit_test/options_test.py b/unit_test/options_test.py index 6b586038f..807f4c819 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -125,6 +125,28 @@ def test_passthrough(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: } +@pytest.mark.parametrize("arch", ["x86_64", "aarch64"]) +@pytest.mark.parametrize( + "alias", + [ + "manylinux_2_28_cuda12_9", + "manylinux_2_28_cuda13_1", + "manylinux_2_34_cuda12_9", + "manylinux_2_34_cuda13_1", + ], +) +def test_cuda_pinned_images(arch: str, alias: str) -> None: + pinned_images = _get_pinned_container_images() + + # CUDA aliases exist for x86_64 and aarch64, and are pinned by digest + image = pinned_images[arch][alias] + assert image.startswith(f"quay.io/manylinux_cuda/{alias.replace('cuda', arch + '_cuda', 1)}@") + assert "@sha256:" in image + + # ... but not for architectures without CUDA images + assert alias not in pinned_images["i686"] + + @pytest.mark.parametrize( "env_var_value", [ From 0ed5106bcc7d7d63ebef00ff2b220f26e31e5287 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 9 Jun 2026 13:06:02 -0400 Subject: [PATCH 2/2] feat: pin CUDA images by dated tag, link source The manylinux_cuda repositories now publish dated tags (e.g. 2026.06.08-1) alongside 'latest', so pin to those just like the PyPA images instead of by digest. Also link the source project (gpu-ci-demo/manylinux-cuda-container) from the CudaImage docstring and the CUDA docs. Assisted-by: ClaudeCode:claude-opus-4.8 --- bin/update_docker.py | 27 ++++++++++++------- .../resources/pinned_docker_images.cfg | 16 +++++------ docs/faq.md | 2 +- unit_test/options_test.py | 7 ++--- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/bin/update_docker.py b/bin/update_docker.py index 89770f439..63b35fcd3 100755 --- a/bin/update_docker.py +++ b/bin/update_docker.py @@ -35,11 +35,13 @@ def __init__(self, manylinux_version: str, platforms: list[str], tag: str | None class CudaImage(Image): """ - A CUDA manylinux image from the manylinux_cuda project. + A CUDA manylinux image from the manylinux_cuda project, built by + https://github.com/gpu-ci-demo/manylinux-cuda-container. - Each architecture has its own repository and the images are only published - with a ``latest`` tag, so (unlike the PyPA images) these are pinned by - digest. ``{arch}`` in the image name is substituted per platform. + Each architecture has its own repository (``{arch}`` in the image name is + substituted per platform), so (unlike the PyPA images) these are resolved + one repository at a time, but they carry the same dated tags and are pinned + to those just like the PyPA images. """ def __init__(self, manylinux_version: str, cuda_version: str, platforms: list[str]): @@ -101,7 +103,7 @@ def __init__(self, manylinux_version: str, cuda_version: str, platforms: list[st PyPAImage( "musllinux_1_2", ["x86_64", "i686", "aarch64", "ppc64le", "s390x", "armv7l", "riscv64"] ), - # CUDA manylinux images (x86_64 and aarch64 only, pinned by digest) + # CUDA manylinux images (x86_64 and aarch64 only, one repository per arch) *( CudaImage(manylinux_version, cuda_version, ["x86_64", "aarch64"]) for manylinux_version in ("manylinux_2_28", "manylinux_2_34") @@ -113,8 +115,8 @@ def __init__(self, manylinux_version: str, cuda_version: str, platforms: list[st for image in images: if "{arch}" in image.image_name: - # Per-architecture repositories pinned by digest (the 'latest' tag is - # the only published tag, so there is no dated tag to pin to). + # Per-architecture repositories: each arch has its own repo, so resolve + # the dated tag matching 'latest' for each one individually. for platform in image.platforms: arch = platform.removeprefix("pypy_") image_name = image.image_name.format(arch=arch) @@ -123,10 +125,17 @@ def __init__(self, manylinux_version: str, cuda_version: str, platforms: list[st f"https://quay.io/api/v1/repository/{repository_name}?includeTags=true" ) response.raise_for_status() - digest = response.json()["tags"]["latest"]["manifest_digest"] + tags_dict = response.json()["tags"] + latest_tag = tags_dict.pop("latest") + # find the tag whose manifest matches 'latest' + tag_name = next( + name + for (name, info) in tags_dict.items() + if info["manifest_digest"] == latest_tag["manifest_digest"] + ) if not config.has_section(platform): config[platform] = {} - config[platform][image.manylinux_version] = f"{image_name}@{digest}" + config[platform][image.manylinux_version] = f"{image_name}:{tag_name}" continue # get the tag name whose digest matches 'latest' diff --git a/cibuildwheel/resources/pinned_docker_images.cfg b/cibuildwheel/resources/pinned_docker_images.cfg index 57d73adc0..b3dc3f947 100644 --- a/cibuildwheel/resources/pinned_docker_images.cfg +++ b/cibuildwheel/resources/pinned_docker_images.cfg @@ -3,10 +3,10 @@ manylinux2014 = quay.io/pypa/manylinux2014_x86_64:2026.06.04-1 manylinux_2_28 = quay.io/pypa/manylinux_2_28_x86_64:2026.06.04-1 manylinux_2_34 = quay.io/pypa/manylinux_2_34_x86_64:2026.06.04-1 musllinux_1_2 = quay.io/pypa/musllinux_1_2_x86_64:2026.06.04-1 -manylinux_2_28_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda12_9@sha256:a36d4f9da7552935b3f925c486f899234fb4ded34712cc066efcc7cccbffb493 -manylinux_2_28_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda13_1@sha256:9018e4a6241cce193ca360a206a5db940831c9857e14229308991e7f037d691e -manylinux_2_34_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_34_x86_64_cuda12_9@sha256:b7e0faeebe1767b8917fb2afe9a989f3add0d198f4e9d8d8fb5a5bfd13a52864 -manylinux_2_34_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_34_x86_64_cuda13_1@sha256:c5000751022c1d9ad7152b45e30bbf4d0871175a7e7c30c4f25fafd480378917 +manylinux_2_28_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda12_9:2026.06.08-1 +manylinux_2_28_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda13_1:2026.06.08-1 +manylinux_2_34_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_34_x86_64_cuda12_9:2026.06.08-1 +manylinux_2_34_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_34_x86_64_cuda13_1:2026.06.08-1 [i686] manylinux2014 = quay.io/pypa/manylinux2014_i686:2026.06.04-1 @@ -19,10 +19,10 @@ manylinux2014 = quay.io/pypa/manylinux2014_aarch64:2026.06.04-1 manylinux_2_28 = quay.io/pypa/manylinux_2_28_aarch64:2026.06.04-1 manylinux_2_34 = quay.io/pypa/manylinux_2_34_aarch64:2026.06.04-1 musllinux_1_2 = quay.io/pypa/musllinux_1_2_aarch64:2026.06.04-1 -manylinux_2_28_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda12_9@sha256:b7ceb066edb6790b35d550642d21b2018eb7c177270dc96dd13de81744afb702 -manylinux_2_28_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda13_1@sha256:7a8e559eac1597055ff5f5cabc1c4e238bb5bc13c988f59f0e49a521f3c9dae4 -manylinux_2_34_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_34_aarch64_cuda12_9@sha256:f1d7f2e0e50e8ded421250db3757157a6fadedc59f905cb640d2908dc61a82ce -manylinux_2_34_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_34_aarch64_cuda13_1@sha256:40cab0122de2ff89ae5f2171cd76201b85f368bc3e2af1ff5c9b96b608aa5a86 +manylinux_2_28_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda12_9:2026.06.08-1 +manylinux_2_28_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda13_1:2026.06.08-1 +manylinux_2_34_cuda12_9 = quay.io/manylinux_cuda/manylinux_2_34_aarch64_cuda12_9:2026.06.08-1 +manylinux_2_34_cuda13_1 = quay.io/manylinux_cuda/manylinux_2_34_aarch64_cuda13_1:2026.06.08-1 [ppc64le] manylinux2014 = quay.io/pypa/manylinux2014_ppc64le:2026.06.04-1 diff --git a/docs/faq.md b/docs/faq.md index 4699a47e0..d9a9ec525 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -224,7 +224,7 @@ Consider incorporating these into your package, for example, in `setup.py` using ### Building wheels with CUDA on Linux On Linux, you can build binary wheels with CUDA to take advantage of NVIDIA GPUs for hardware acceleration. -The [manylinux_cuda](https://quay.io/organization/manylinux_cuda) project publishes manylinux containers that bundle the CUDA Toolkit, and cibuildwheel ships pinned aliases for them. Just like `manylinux_2_28`, you can pass an alias to the `manylinux-*-image` options and cibuildwheel will expand it to a specific, pinned image digest: +The [manylinux_cuda](https://quay.io/organization/manylinux_cuda) project ([source](https://github.com/gpu-ci-demo/manylinux-cuda-container)) publishes manylinux containers that bundle the CUDA Toolkit, and cibuildwheel ships pinned aliases for them. Just like `manylinux_2_28`, you can pass an alias to the `manylinux-*-image` options and cibuildwheel will expand it to a specific, pinned image: ```yaml CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28_cuda13_1 diff --git a/unit_test/options_test.py b/unit_test/options_test.py index 807f4c819..a2c957fb4 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -138,10 +138,11 @@ def test_passthrough(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_cuda_pinned_images(arch: str, alias: str) -> None: pinned_images = _get_pinned_container_images() - # CUDA aliases exist for x86_64 and aarch64, and are pinned by digest + # CUDA aliases exist for x86_64 and aarch64, each in their own repository image = pinned_images[arch][alias] - assert image.startswith(f"quay.io/manylinux_cuda/{alias.replace('cuda', arch + '_cuda', 1)}@") - assert "@sha256:" in image + repository, _, tag = image.partition(":") + assert repository == f"quay.io/manylinux_cuda/{alias.replace('cuda', arch + '_cuda', 1)}" + assert tag # ... but not for architectures without CUDA images assert alias not in pinned_images["i686"]