@@ -128,6 +128,16 @@ jobs:
128128 EKS_IMAGE : " ghcr.io/mchmarny/cluster/eks@sha256:8bc33d14e6e5659d242aa2cdbcd69fca389cfcdb9e94f7a7a1b82fd33176befa"
129129 VALIDATOR_IMAGE_PREFIX : " ghcr.io/nvidia/aicr-validators"
130130 VALIDATOR_TAG : " uat-${{ github.run_id }}"
131+ # Target platform(s) for the main-cell image builds (snapshot-agent + the
132+ # validators). h100 GPU workers (p5.48xlarge) are amd64; GB200 GPU workers
133+ # (p6e-gb200.36xlarge) are Grace/arm64, and the agent + GPU-scheduled
134+ # validators are pinned to that pool — so an amd64-only image cannot start
135+ # there. GB200 therefore builds a multi-arch (amd64+arm64) manifest: amd64
136+ # still covers the m7i system pool, arm64 covers the GPU pool. h100 stays
137+ # single-arch so the common nightly path skips the slow QEMU arm64 cross
138+ # build. Release cells are unaffected (they pull the already-multi-arch
139+ # released images). Mirrors the release build's linux/amd64,linux/arm64.
140+ BUILD_PLATFORMS : ${{ inputs.accelerator == 'gb200' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
131141 # Pin validator-image resolution for MAIN cells to this run's freshly
132142 # built+pushed validators (the `Build and push validator images` step
133143 # tags them `:uat-<run_id>`), mirroring how AICR_IMAGE pins the agent — so
@@ -165,22 +175,31 @@ jobs:
165175 nightly|daytime-up|daytime-down) ;;
166176 *) echo "::error::unknown lifecycle '${LIFECYCLE}' (want nightly|daytime-up|daytime-down)"; exit 1 ;;
167177 esac
168- if [[ ! -f "${TEST_CONFIG}" ]]; then
169- echo "::error::test config not found: ${TEST_CONFIG}"; exit 1
170- fi
171- # The account the OIDC role is assumed into (AWS_ACCOUNT_ID) is the
172- # cluster-config's .deployment.tenancy — the single source of truth
173- # for which tenancy this run provisions + keeps state in. Deriving it
174- # here (rather than accepting it as an input) means the role can never
175- # be assumed into a different account than the one being provisioned.
176- # A cross-account reservation (e.g. a GB200 Capacity Block in a
177- # separate tenancy) is selected purely by its cluster-config. Uses the
178- # runner-preinstalled yq (Install tools runs later).
178+ # Derive + export the account FIRST, before any lifecycle-specific
179+ # gate below can exit. The account the OIDC role is assumed into
180+ # (AWS_ACCOUNT_ID) is the cluster-config's .deployment.tenancy — the
181+ # single source of truth for which tenancy this run provisions + keeps
182+ # state in. Deriving it here (rather than accepting it as an input)
183+ # means the role can never be assumed into a different account than
184+ # the one being provisioned; a cross-account reservation (e.g. a GB200
185+ # Capacity Block in a separate tenancy) is selected purely by its
186+ # cluster-config. Exporting it up front is load-bearing for teardown:
187+ # daytime-down re-authenticates and builds the role ARN from
188+ # AWS_ACCOUNT_ID, so a later early-exit here must not skip this — else
189+ # teardown cannot assume the role and the cluster + Capacity Block
190+ # leak. Uses the runner-preinstalled yq (Install tools runs later).
179191 TENANCY=$(yq eval '.deployment.tenancy' "${CLUSTER_CONFIG}")
180192 if [[ -z "${TENANCY}" || "${TENANCY}" == "null" ]]; then
181193 echo "::error::${CLUSTER_CONFIG} has no .deployment.tenancy"; exit 1
182194 fi
183195 echo "AWS_ACCOUNT_ID=${TENANCY}" >> "${GITHUB_ENV}"
196+ # The test config only matters for the run phases (nightly / daytime-up
197+ # provision+deploy). daytime-down just tears the held cluster down and
198+ # never reads it, so skip the existence check there — a missing sibling
199+ # config must not block teardown and strand the Capacity Block.
200+ if [[ "${LIFECYCLE}" != "daytime-down" && ! -f "${TEST_CONFIG}" ]]; then
201+ echo "::error::test config not found: ${TEST_CONFIG}"; exit 1
202+ fi
184203 echo "intent=${INTENT} lifecycle=${LIFECYCLE} test-config=${TEST_CONFIG} cluster=${DEPLOYMENT_ID} account=${TENANCY}"
185204
186205 # Versions
@@ -253,6 +272,18 @@ jobs:
253272 - name : Authenticate to GHCR
254273 uses : ./.github/actions/ghcr-login
255274
275+ # QEMU registers binfmt handlers so the amd64 runner can build the arm64
276+ # layer of a GB200 multi-arch image. Only needed when BUILD_PLATFORMS
277+ # includes arm64 (GB200); h100 builds native amd64 and skips it, so the
278+ # common nightly path pays no emulation cost. buildx (below) is always set
279+ # up because the multi-platform build path uses `buildx build --push`.
280+ - name : Set up QEMU
281+ if : inputs.aicr_version == '' && inputs.skip_tests != true && inputs.lifecycle != 'daytime-down' && contains(env.BUILD_PLATFORMS, 'arm64')
282+ uses : docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
283+ - name : Set up Docker Buildx
284+ if : inputs.aicr_version == '' && inputs.skip_tests != true && inputs.lifecycle != 'daytime-down'
285+ uses : docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
286+
256287 # Main cell only: release cells use the released validator images the
257288 # released binary self-resolves to (skipped when aicr_version is set).
258289 - name : Build and push validator images
@@ -271,20 +302,27 @@ jobs:
271302 # until the job timeout (#1636). It uses a different Dockerfile path
272303 # and takes no GO_VERSION build-arg. Keep the phase set in sync with
273304 # VALIDATOR_PHASES in on-push.yaml / on-tag.yaml.
305+ #
306+ # buildx build --push emits a multi-arch manifest for GB200
307+ # (BUILD_PLATFORMS=linux/amd64,linux/arm64) and a single-arch image
308+ # for h100 (linux/amd64) through one code path. Multi-platform builds
309+ # cannot be loaded into the local daemon, so we push directly (the
310+ # GHCR login above authenticates the push).
274311 for phase in deployment performance conformance aiperf-bench; do
275312 IMAGE="${VALIDATOR_IMAGE_PREFIX}/${phase}:${VALIDATOR_TAG}"
276313 case "${phase}" in
277314 aiperf-bench)
278- docker build -f validators/performance/aiperf-bench.Dockerfile \
279- -t "${IMAGE}" .
315+ docker buildx build --platform="${BUILD_PLATFORMS}" \
316+ -f validators/performance/aiperf-bench.Dockerfile \
317+ -t "${IMAGE}" --push .
280318 ;;
281319 *)
282- docker build -f "validators/${phase}/Dockerfile" \
320+ docker buildx build --platform="${BUILD_PLATFORMS}" \
321+ -f "validators/${phase}/Dockerfile" \
283322 --build-arg "GO_VERSION=${GO_VERSION}" \
284- -t "${IMAGE}" .
323+ -t "${IMAGE}" --push .
285324 ;;
286325 esac
287- docker push "${IMAGE}"
288326 done
289327
290328 # Auth
@@ -418,9 +456,13 @@ jobs:
418456 # build — so any artifact-schema change in the PR under test (e.g. an
419457 # apiVersion bump) produces a snapshot the freshly-built binary rejects.
420458 # Build the agent from the PR source and point AICR_IMAGE at it so the
421- # snapshot is stamped by the code under test, not a stale release. Single
422- # arch: the GPU worker (p5.48xlarge) is amd64. Lives in the aicr-validators
423- # namespace (not the release ghcr.io/nvidia/aicr repo); cleaned up below.
459+ # snapshot is stamped by the code under test, not a stale release. Built
460+ # for BUILD_PLATFORMS: amd64 for h100 (p5.48xlarge), amd64+arm64 for GB200
461+ # (the snapshot agent is pinned to the arm64 p6e-gb200 GPU pool). ko
462+ # cross-compiles Go natively — no QEMU — and emits a manifest list at the
463+ # single tag, so the kubelet pulls the node's arch. Lives in the
464+ # aicr-validators namespace (not the release ghcr.io/nvidia/aicr repo);
465+ # cleaned up below.
424466 - name : Build and push snapshot-agent image
425467 if : inputs.aicr_version == '' && inputs.skip_tests != true && inputs.lifecycle != 'daytime-down'
426468 env :
@@ -430,7 +472,7 @@ jobs:
430472 set -euo pipefail
431473 ko build ./cmd/aicr \
432474 --bare \
433- --platform=linux/amd64 \
475+ --platform="${BUILD_PLATFORMS}" \
434476 --image-label="org.opencontainers.image.revision=${{ github.sha }}" \
435477 --tags="${VALIDATOR_TAG}"
436478
0 commit comments