[CI] Bound the CRCR report by build age, and stop gating it on a job (#59066)

This commit is contained in:
Andrey Talman
2026-09-28 17:42:32 -07:00
committed by GitHub
parent b39f760cc7
commit 869278cbb0
2 changed files with 100 additions and 35 deletions
+83 -17
View File
@@ -79,12 +79,22 @@ if [[ -z "${OIDC_TOKEN}" ]]; then
fi
BUILD_JSON="$(mktemp)"
trap 'rm -f "${BUILD_JSON}"' EXIT
BUILD_JSON_STAGING="$(mktemp)"
trap 'rm -f "${BUILD_JSON}" "${BUILD_JSON_STAGING}"' EXIT
BUILD_URL="https://api.buildkite.com/v2/organizations/${BUILDKITE_ORGANIZATION_SLUG}/pipelines/${BUILDKITE_PIPELINE_SLUG}/builds/${BUILDKITE_BUILD_NUMBER}"
# Staged so a failed request cannot clobber the last good snapshot: curl -o
# writes the error body too, and the deadline path reports from whatever
# snapshot it has.
fetch_build() {
curl -sS -w '%{http_code}' -o "${BUILD_JSON}" \
-H "Authorization: Bearer ${BK_TOKEN}" "${BUILD_URL}"
local code
code="$(curl -sS -w '%{http_code}' -o "${BUILD_JSON_STAGING}" \
-H "Authorization: Bearer ${BK_TOKEN}" "${BUILD_URL}")"
if [[ "${code}" == "200" ]]; then
mv -f "${BUILD_JSON_STAGING}" "${BUILD_JSON}"
BUILD_JSON_STAGING="$(mktemp)"
fi
echo "${code}"
}
# Count jobs that have not reached a terminal state, ignoring this job: the
@@ -113,22 +123,78 @@ PY
# finished and none of the eventual 152 hard failures visible, and HUD recorded
# that nightly as green.
#
# Polling rather than a full `depends_on` barrier: Buildkite groups carry no
# key, so a barrier means naming all ~250 step keys, which silently rots when a
# step is added and fails the pipeline upload when one is removed or excluded
# from this lane (AMD, retired A100). The pipeline does gate this step on the
# long pole so it is scheduled near the end of the build -- that keeps the agent
# from idling for hours, but it is only a hint, and this loop is what actually
# guarantees the snapshot is complete.
POLL_INTERVAL_S="${CRCR_POLL_INTERVAL_S:-60}"
# Under the step's own timeout, so a stuck build still gets a partial report
# rather than the job being killed with nothing sent.
WAIT_DEADLINE=$(( $(date +%s) + ${CRCR_MAX_WAIT_S:-46800} ))
# Polling rather than a `depends_on` barrier: Buildkite groups carry no key, so
# a barrier means naming all ~250 step keys, which silently rots when a step is
# added and fails the pipeline upload when one is removed or excluded from this
# lane (AMD, retired A100). Gating on a single step was tried and is worse: a
# dependency that fails or is cancelled takes the report with it despite
# allow_dependency_failure (builds 91191 and 91461 both lost their report that
# way), so this step depends only on the image it runs in.
# 15 min: the wait now spans the whole build, so a 60s poll made ~420 requests
# against an API shared with the rest of CI to learn something that changes on
# the order of minutes. The cost is up to one interval of extra latency after
# the last job lands.
POLL_INTERVAL_S="${CRCR_POLL_INTERVAL_S:-900}"
# The deadline is measured from when the *build* started, not from when this
# script did, so "report no later than N hours into the build" holds however
# late this step is scheduled. Anchoring it to script start made the bound
# meaningless whenever the step was itself delayed.
#
# Resolved after the first fetch, from the build's own created_at.
MAX_BUILD_AGE_S="${CRCR_MAX_BUILD_AGE_S:-25200}"
WAIT_DEADLINE=""
# Used until created_at is known. Without it, an API outage on the very first
# request leaves the deadline unresolved and the loop spins to the step timeout.
FALLBACK_DEADLINE=$(( $(date +%s) + MAX_BUILD_AGE_S ))
build_deadline() {
python3 - "${BUILD_JSON}" "${MAX_BUILD_AGE_S}" <<'PY'
import datetime, json, sys
build = json.load(open(sys.argv[1]))
created = build.get("created_at")
if not created:
raise SystemExit(1)
started = datetime.datetime.fromisoformat(created.replace("Z", "+00:00"))
print(int(started.timestamp()) + int(sys.argv[2]))
PY
}
while :; do
http_code="$(fetch_build)"
if [[ "${http_code}" != "200" ]]; then
echo "buildkite API returned ${http_code} -- skipping report"
exit 0
# A transient 429/5xx must not cost the whole report: treating one
# failure as fatal would drop a nightly for a blip. Keep polling on the
# last good snapshot; only give up if the deadline passes having never
# fetched one.
echo "buildkite API returned ${http_code}"
if [[ ! -s "${BUILD_JSON}" ]]; then
if (( $(date +%s) >= ${WAIT_DEADLINE:-$FALLBACK_DEADLINE} )); then
echo "no build data was ever fetched -- nothing to report"
exit 0
fi
sleep "${POLL_INTERVAL_S}"
continue
fi
if (( $(date +%s) >= ${WAIT_DEADLINE:-$FALLBACK_DEADLINE} )); then
echo "deadline reached while the API is unavailable --" \
"reporting from the last good snapshot"
break
fi
sleep "${POLL_INTERVAL_S}"
continue
fi
if [[ -z "${WAIT_DEADLINE}" ]]; then
WAIT_DEADLINE="$(build_deadline)" || WAIT_DEADLINE=""
if [[ -z "${WAIT_DEADLINE}" ]]; then
# No created_at to anchor to; fall back to script start so the loop
# still has a bound rather than running until the step times out.
WAIT_DEADLINE=$(( $(date +%s) + MAX_BUILD_AGE_S ))
echo "build created_at unavailable -- deadline measured from now"
fi
echo "reporting deadline: $(date -u -d "@${WAIT_DEADLINE}" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
|| date -u -r "${WAIT_DEADLINE}" '+%Y-%m-%dT%H:%M:%SZ')" \
"(build age limit ${MAX_BUILD_AGE_S}s)"
fi
remaining="$(outstanding_jobs)" || remaining=""
if [[ -z "${remaining}" ]]; then
@@ -140,7 +206,7 @@ while :; do
break
fi
if (( $(date +%s) >= WAIT_DEADLINE )); then
echo "still ${remaining} job(s) running at the wait deadline --" \
echo "still ${remaining} job(s) running at the build-age deadline --" \
"reporting a partial view"
break
fi
+17 -18
View File
@@ -1,31 +1,30 @@
group: CRCR Report
# The report waits for the rest of the build (see crcr-report.sh), so gate it on
# the long pole rather than letting it wait on an agent. A torch-nightly build
# is tail-heavy: of 347 jobs in build 90830, 344 finished inside 3.5h and three
# stragglers stretched it to 12.7h, with lm-eval-large-models-4xh100 last in
# both 90830 (+760 min) and 90640 (+799 min) by a margin of hours.
# Only image-build-cpu: this step must always run.
#
# Depending on it means Buildkite does not schedule this step until the build is
# nearly over, so the agent polls briefly instead of idling ~13h. It is a
# scheduling hint, not the correctness mechanism: the script still waits for
# every job, so if this stops being the slowest step the report is late to start
# but still correct.
# It was briefly gated on the build's long pole (lm-eval-large-models-4xh100)
# so the agent would not idle. That backfired on the first nightly to use it,
# build 91191: the long pole OOMed, hung, and was cancelled -- and a cancelled
# dependency propagates past `allow_dependency_failure`, so the step went
# `waiting_failed` and that nightly got no CRCR row at all. Nothing in this
# pipeline sets timeout_in_minutes (the pipeline default and org maximum are
# both unset), so a hung job has no terminal state to reach either: gating on
# any single job can suppress the report indefinitely.
#
# The bound now lives in the script, measured from the build's created_at, so
# the report lands within CRCR_MAX_BUILD_AGE_S of the build starting whatever
# else happens.
depends_on:
- image-build-cpu
- lm-eval-large-models-4xh100
steps:
- label: ":satellite: Report nightly results to CRCR"
key: crcr-report
if: build.env("TORCH_NIGHTLY") == "1"
soft_fail: true
allow_dependency_failure: true
# depends_on defers the start to near the end of the build, so in the normal
# case the script polls for minutes. This only has to cover the case where
# the long pole finishes early and something else is still running, so it is
# sized against the whole build rather than the expected wait. The script's
# own deadline (CRCR_MAX_WAIT_S, 13h) trips first and still sends a partial
# report; this is the backstop that stops the job outliving the build.
timeout_in_minutes: 840
# The script polls from the start of the build now, so this covers the whole
# wait. It sits above the script's own build-age deadline (7h) so the script
# sends its partial report rather than being killed with nothing sent.
timeout_in_minutes: 480
device: cpu-small
commands:
- bash /vllm-workspace/.buildkite/scripts/crcr-report.sh