Skip to content

Commit db8b643

Browse files
authored
Move docker-builds to OSDC runners (#22106)
Part 1 of moving ExecuTorch's Linux CI to [OSDC](https://github.com/pytorch/test-infra/blob/main/docs/osdc_runners.md). The `linux_job_v2` -> `v3` migration follows in #22245 onward; #22108 covers the native jobs. OSDC pods have no docker daemon, so the builds move to the in-cluster BuildKit pool: `build.sh` switches to `docker buildx build --push` under `REMOTE_BUILDKIT`, driven by test-infra's `docker-build-remote-buildkit` action. The tag is computed in the workflow now, but it is the same string `calculate-docker-image` produced, so jobs still on v2 keep resolving. That action only retries failures from before BuildKit starts, so the three retries `calculate-docker-image` wrapped the build in are kept around the SDK downloads. sccache resolved its S3 credentials from EC2 instance metadata, which a BuildKit pod has none of, so every compiler invocation died on a metadata timeout. They now reach the build as a BuildKit secret, and `install_pytorch.sh` caches locally when it is absent. Dropping the S3 backend would have been the smaller change, but the PyTorch build layer takes ~19 minutes only because sccache serves nearly all of it, and uncached it would not fit the job timeout. `pull_request` gives way to `ciflow/docker`, since a fork PR gets no OIDC token and could never push. The tag is registered in `pytorch-probot.yml` and auto-applied on the paths that need a rebuild, so such a PR still rebuilds its images without anyone having to remember — the same rule pytorch has. Last commit adds `_docker-image.yml`, the consumer-side counterpart: it emits `git rev-parse HEAD:.ci/docker` for the v3 jobs to name their image with. Nothing calls it yet. All 16 images build and push green on OSDC in [run 32799247406](https://github.com/pytorch/executorch/actions/runs/32799247406): 16–48 min each against 37–56 on EC2, arm64 included. Authored with Claude Code.
1 parent 517e56a commit db8b643

7 files changed

Lines changed: 172 additions & 42 deletions

File tree

.ci/docker/build.sh

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,16 @@ fi
116116
# Copy requirements-lintrunner.txt from root to here
117117
cp ../../requirements-lintrunner.txt ./
118118

119-
docker build \
119+
# OSDC runners have no docker daemon, so the build runs on the in-cluster
120+
# BuildKit pool via a remote buildx builder. That builder has nowhere to load
121+
# an image into, so the result has to go straight to the registry.
122+
if [[ -n "${REMOTE_BUILDKIT:-}" ]]; then
123+
BUILD_CMD=(docker buildx build --push)
124+
else
125+
BUILD_CMD=(docker build)
126+
fi
127+
128+
"${BUILD_CMD[@]}" \
120129
--no-cache \
121130
--progress=plain \
122131
--build-arg "OS_VERSION=${OS_VERSION}" \

.ci/docker/common/install_pytorch.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ set -ex
1010
# shellcheck source=/dev/null
1111
source "$(dirname "${BASH_SOURCE[0]}")/utils.sh"
1212

13+
# The compiler stubs run through sccache, which fails hard rather than
14+
# compiling uncached when it cannot reach its S3 bucket. Use the credentials
15+
# docker-builds mounts, or fall back to a local cache so a build without them
16+
# still works.
17+
SCCACHE_CREDENTIALS=/run/secrets/aws-credentials
18+
if [[ -s "${SCCACHE_CREDENTIALS}" ]]; then
19+
export AWS_SHARED_CREDENTIALS_FILE="${SCCACHE_CREDENTIALS}"
20+
else
21+
echo "No sccache credentials; caching compiler output locally" >&2
22+
unset SCCACHE_BUCKET SCCACHE_S3_KEY_PREFIX
23+
export SCCACHE_DIR=/tmp/sccache
24+
fi
25+
1326
install_domains() {
1427
echo "Install torchvision and torchaudio"
1528
pip_install --no-build-isolation --user "git+https://github.com/pytorch/audio.git@${TORCHAUDIO_VERSION}"

.ci/docker/ubuntu/Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,12 @@ ARG SKIP_PYTORCH
7070
ARG PYTORCH_BUILD_MAX_JOBS
7171
COPY ./common/install_pytorch.sh install_pytorch.sh
7272
COPY ./common/utils.sh utils.sh
73-
RUN if [ -z "${SKIP_PYTORCH}" ]; then bash ./install_pytorch.sh; fi && rm install_pytorch.sh utils.sh
73+
# A host docker build let sccache reach its S3 bucket with the EC2 instance
74+
# role; a BuildKit pod has no instance metadata, so docker-builds passes the
75+
# credentials in as this secret. World-readable because the sccache server runs
76+
# as ci-user; a secret mount never lands in a layer.
77+
RUN --mount=type=secret,id=aws-credentials,mode=0444 \
78+
if [ -z "${SKIP_PYTORCH}" ]; then bash ./install_pytorch.sh; fi && rm install_pytorch.sh utils.sh
7479

7580
ARG LINTRUNNER
7681
# Install lintrunner if needed

.github/labeler.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,12 @@ ciflow/trunk:
1313
- any-glob-to-any-file:
1414
- 'backends/arm/**'
1515
- 'examples/arm/**'
16+
17+
# Same paths docker-builds triggers on. It has no pull_request trigger, so this
18+
# label is what rebuilds the images for a PR that changes them.
19+
ciflow/docker:
20+
- changed-files:
21+
- any-glob-to-any-file:
22+
- '.ci/docker/**'
23+
- '.github/workflows/docker-builds.yml'
24+
- 'requirements-lintrunner.txt'

.github/pytorch-probot.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ ciflow_push_tags:
55
- ciflow/apple
66
- ciflow/cuda
77
- ciflow/cuda-perf
8+
- ciflow/docker
89
- ciflow/metal
910
- ciflow/mlx
1011
- ciflow/rocm
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Resolve CI docker image
2+
3+
# linux_job_v3 passes `docker-image` straight to the runner pod's container, which
4+
# is pulled before any step runs, so nothing inside the job can derive it. Callers
5+
# depend on this workflow and name the image as
6+
# 308535385114.dkr.ecr.us-east-1.amazonaws.com/executorch/ci-image:<name>-<hash>.
7+
#
8+
# Nothing here waits for docker-builds. A commit that changes .ci/docker needs the
9+
# ciflow/docker label to build the images it asks for; without them the pod fails
10+
# to pull and the job has to be re-run once they land.
11+
12+
on:
13+
workflow_call:
14+
outputs:
15+
ci-docker-hash:
16+
description: Tag suffix shared by every executorch CI image for this commit.
17+
value: ${{ jobs.resolve.outputs.ci-docker-hash }}
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
resolve:
24+
name: resolve
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 10
27+
outputs:
28+
ci-docker-hash: ${{ steps.hash.outputs.ci-docker-hash }}
29+
30+
steps:
31+
- name: Checkout ExecuTorch
32+
uses: actions/checkout@v4
33+
with:
34+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
35+
36+
- name: Compute the docker tag
37+
id: hash
38+
run: |
39+
set -eu
40+
echo "ci-docker-hash=$(git rev-parse HEAD:.ci/docker)" >> "$GITHUB_OUTPUT"
Lines changed: 93 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
name: docker-builds
22

3+
# No pull_request trigger: a fork PR gets no OIDC token, so the build could not
4+
# assume role/arc to push and every run would fail. A PR that needs fresh images
5+
# gets the ciflow/docker label instead, which tags the PR head and fires the push
6+
# trigger below with the PR's own SHA. Matches pytorch/pytorch.
37
on:
48
workflow_dispatch:
5-
pull_request:
6-
paths:
7-
- .ci/docker/**
8-
- .github/workflows/docker-builds.yml
9-
- requirements-lintrunner.txt
109
push:
1110
branches:
1211
- main
1312
- release/*
13+
tags:
14+
- ciflow/docker/*
1415
paths:
1516
- .ci/docker/**
1617
- .github/workflows/docker-builds.yml
@@ -19,19 +20,30 @@ on:
1920
- cron: 1 3 * * 3
2021

2122
concurrency:
22-
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref_name }}-${{ github.ref_type == 'branch' && github.sha }}-${{ github.event_name == 'workflow_dispatch' }}-${{ github.event_name == 'schedule' }}
23+
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_type == 'branch' && github.sha }}-${{ github.event_name == 'workflow_dispatch' }}-${{ github.event_name == 'schedule' }}
2324
cancel-in-progress: true
2425

2526
env:
2627
AWS_DEFAULT_REGION: us-east-1
28+
ECR_REGISTRY: 308535385114.dkr.ecr.us-east-1.amazonaws.com
29+
ECR_REPOSITORY: executorch/ci-image
30+
31+
permissions:
32+
id-token: write
33+
contents: read
2734

2835
jobs:
2936
docker-build:
3037
timeout-minutes: 240
3138
strategy:
3239
fail-fast: false
3340
matrix:
34-
runner: [linux.4xlarge]
41+
# BuildKit runs out of cluster on the OSDC BuildKit pool; this runner is
42+
# only an orchestrator that streams the (tiny) .ci/docker context and
43+
# waits, so it can be small. The architecture still has to match, because
44+
# the helper picks the per-architecture buildkitd address off `uname -m`,
45+
# and on arm64 this is the smallest label that actually schedules.
46+
runner: [mt-l-x86iavx512-8-64]
3547
docker-image-name: [
3648
executorch-ubuntu-22.04-gcc11,
3749
executorch-ubuntu-22.04-gcc9-nopytorch,
@@ -45,49 +57,90 @@ jobs:
4557
executorch-ubuntu-22.04-clang12-android,
4658
executorch-ubuntu-24.04-gcc14,
4759
executorch-ubuntu-26.04-gcc14,
60+
# Built on x86: this image only cross-compiles for CUDA on Windows,
61+
# so building it never needed the GPU runner it used to run on.
62+
executorch-ubuntu-22.04-cuda-windows,
4863
]
4964
include:
5065
- docker-image-name: executorch-ubuntu-22.04-gcc11-aarch64
51-
runner: linux.arm64.2xlarge
66+
runner: mt-l-arm64g4-16-62
5267
- docker-image-name: executorch-ubuntu-22.04-gcc11-aarch64-android
53-
runner: linux.arm64.2xlarge
68+
runner: mt-l-arm64g4-16-62
5469
- docker-image-name: executorch-ubuntu-22.04-gcc11-aarch64-arm-sdk
55-
runner: linux.arm64.2xlarge
56-
- docker-image-name: executorch-ubuntu-22.04-cuda-windows
57-
runner: linux.g5.4xlarge.nvidia.gpu
70+
runner: mt-l-arm64g4-16-62
5871

59-
runs-on: [self-hosted, "${{ matrix.runner }}"]
60-
env:
61-
DOCKER_IMAGE: 308535385114.dkr.ecr.us-east-1.amazonaws.com/executorch/${{ matrix.docker-image-name }}
72+
runs-on: ${{ matrix.runner }}
73+
container:
74+
image: ghcr.io/actions/actions-runner:latest
6275
steps:
63-
- name: Clean workspace
64-
shell: bash
65-
run: |
66-
echo "${GITHUB_WORKSPACE}"
67-
sudo rm -rf "${GITHUB_WORKSPACE}"
68-
mkdir "${GITHUB_WORKSPACE}"
76+
- name: Checkout ExecuTorch
77+
uses: actions/checkout@v4
6978

70-
- name: Setup SSH (Click me for login details)
71-
uses: pytorch/test-infra/.github/actions/setup-ssh@main
79+
- name: Configure AWS credentials
80+
uses: aws-actions/configure-aws-credentials@v4
7281
with:
73-
github-secret: ${{ secrets.GITHUB_TOKEN }}
82+
role-to-assume: arn:aws:iam::308535385114:role/arc
83+
aws-region: us-east-1
84+
role-duration-seconds: 18000
7485

75-
- name: Checkout Executorch
76-
uses: actions/checkout@v3
86+
# buildx forwards the client's registry auth to the remote builder, so the
87+
# push at the end of build.sh authenticates with what this step writes.
88+
- name: Login to ECR
89+
uses: aws-actions/amazon-ecr-login@v2
90+
with:
91+
registries: "308535385114"
7792

78-
- name: Setup Linux
79-
uses: pytorch/test-infra/.github/actions/setup-linux@main
93+
# sccache cannot source S3 credentials from instance metadata inside a
94+
# BuildKit pod, so hand the assumed-role ones to the build as a secret. An
95+
# empty file is a valid outcome: install_pytorch.sh then caches locally.
96+
- name: Stage sccache credentials
97+
id: sccache-credentials
98+
shell: bash
99+
run: |
100+
set -euo pipefail
101+
creds="${RUNNER_TEMP}/aws-credentials"
102+
install -m 600 /dev/null "${creds}"
103+
if [[ -n "${AWS_ACCESS_KEY_ID:-}" ]]; then
104+
{
105+
echo "[default]"
106+
echo "aws_access_key_id=${AWS_ACCESS_KEY_ID}"
107+
echo "aws_secret_access_key=${AWS_SECRET_ACCESS_KEY}"
108+
echo "aws_session_token=${AWS_SESSION_TOKEN}"
109+
} > "${creds}"
110+
else
111+
echo "::warning::No AWS credentials; the image build will not use the sccache S3 cache"
112+
fi
113+
echo "path=${creds}" >> "${GITHUB_OUTPUT}"
80114
81-
- name: Build docker image
82-
id: build-docker-image
83-
timeout-minutes: 145
84-
uses: pytorch/test-infra/.github/actions/calculate-docker-image@main
85-
with:
86-
docker-image-name: ci-image:${{ matrix.docker-image-name }}
87-
always-rebuild: true
88-
push: true
89-
force-push: true
115+
- name: Compute the image tag
116+
id: tag
117+
shell: bash
118+
run: |
119+
set -eux
120+
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
121+
# Same tag that calculate-docker-image derives for consumers of
122+
# `ci-image:<name>`, so jobs still on linux_job_v2 keep resolving.
123+
DOCKER_TAG=$(git rev-parse HEAD:.ci/docker)
124+
echo "docker-image=${ECR_REGISTRY}/${ECR_REPOSITORY}:${{ matrix.docker-image-name }}-${DOCKER_TAG}" >> "${GITHUB_OUTPUT}"
90125
91-
- name: Teardown Linux
92-
uses: pytorch/test-infra/.github/actions/teardown-linux@main
93-
if: always()
126+
# No step timeout: the action retries a cold BuildKit pool for at least two
127+
# hours, and the job's timeout-minutes is what bounds that.
128+
- name: Build and push to ECR
129+
uses: pytorch/test-infra/.github/actions/docker-build-remote-buildkit@main
130+
with:
131+
# calculate-docker-image wrapped the build in three retries because it
132+
# "frequently fails with network error downloading various stuffs".
133+
# The action only retries failures from before BuildKit starts, so keep
134+
# a retry of our own around the SDK downloads.
135+
command: |
136+
cd .ci/docker
137+
for attempt in 1 2 3; do
138+
if REMOTE_BUILDKIT=1 ./build.sh ${{ matrix.docker-image-name }} \
139+
-t ${{ steps.tag.outputs.docker-image }} \
140+
--secret id=aws-credentials,src=${{ steps.sccache-credentials.outputs.path }}; then
141+
exit 0
142+
fi
143+
echo "::warning::docker build attempt ${attempt} of 3 failed"
144+
if [ "${attempt}" -lt 3 ]; then sleep 90; fi
145+
done
146+
exit 1

0 commit comments

Comments
 (0)