Add scheduled vLLM torch-nightly regression triage - #8447
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
vLLM's Buildkite pipeline runs three scheduled builds on main, all from the same HEAD. "Full CI run torch nightly" (TORCH_NIGHTLY=1, Mon/Tue/Thu 23:00 PT) and "Full CI run - nightly" (daily, same 23:00 PT slot) therefore fire in the same second on the same commit, differing only by that one env var. That pair is a controlled A/B: a job failing in the former and passing in the latter is attributable to torch nightly with the vLLM variable held constant. Nothing was consuming that signal. tools/torchci/vllm_torch_nightly_triage.py finds the newest such pair in ClickHouse and buckets every job into regressed / fails-on-both / baseline-only, excluding soft_failed (non-blocking by design) and retried (superseded attempts). It clusters regressions by job-name family and checks whether failures concentrate on one agent, which would indicate a sick host rather than a torch regression. The workflow runs it Tue/Wed/Fri after the nightly lands, then a second job root-causes the result with the checked-in .claude/skills/vllm-pytorch-ci-triage skill. Logs are fetched by the first job, not the model: one representative log per cluster, ANSI- and BKT-marker-stripped and tail-trimmed, so the read-only Buildkite token never enters the agent's tool surface and the model runs with --allowedTools "Read,Glob,Grep,Write" -- no Bash, no network, no credentials. Follows the greenlight-pr-review.yml pattern (claude-code-action directly, environment: bedrock, OIDC), which works from schedule unlike _claude-code.yml. Reports only -- job log, run summary, artifacts. Files nothing anywhere. Why the A/B matters: on build 82195 a naive "what failed on torch nightly" view showed 57 failures, 21 of which fail identically on the baseline, including six Distributed Compile timeouts that looked like a distributed-compile regression and are pre-existing. Those would have been filed upstream wrongly. Reuses the existing CLICKHOUSE_HUD_USER_* secrets; no new ClickHouse credentials. Verified against live data: build 82454 vs 82455 at commit 50c51682a18c, 27 regressions across 20 agents with no single-host concentration.
3f9e3e7 to
0f6789b
Compare
| - name: Generate report | ||
| id: report | ||
| working-directory: tools | ||
| env: |
There was a problem hiding this comment.
Should these secrets be in an environment? I wonder why we still keep them outside
There was a problem hiding this comment.
hi @huydhn ok will create a BE PR tomorrow to move rest of the secrets into env . Moved BUILDKITE_CI_READ_ONLY under vllm-triage env
BUILDKITE_CI_READ_ONLY lives in the vllm-triage environment, and environment secrets only resolve for jobs that declare the environment. CLICKHOUSE_HUD_USER_* are repo-level and resolve either way. root-cause keeps environment: bedrock -- a job gets exactly one environment and the two need different ones. vllm-triage has no protection rules or branch policy, so this adds no approval gate.
Per review: _claude-code.yml authenticates to Bedrock by assuming role/gha_workflow_claude_code via aws-actions/configure-aws-credentials and passes no github_token. This workflow copied the github_token from greenlight-pr-review.yml but omitted the AWS configure step, so it had no Bedrock credentials at all -- environment: bedrock only scopes secrets and branch access, it does not configure AWS. Add the OIDC configure step with role-duration-seconds long enough to outlive the 30-minute model timeout, and drop github_token: the agent reads local files and writes findings.md, so it needs no GitHub API access.
huydhn
left a comment
There was a problem hiding this comment.
Overall LGTM! I review mainly the workflow setup. The python script as it has only simple functionalities to query CI results and pull logs, so Claude probably gets it right and we can iterate on that if needed
cc @izaitsevfb for your review too
izaitsevfb
left a comment
There was a problem hiding this comment.
findings from my agent:
- The cron fires before the builds finish, and in-flight baseline jobs are scored as torch regressions. Cron is 12:00 UTC; both builds start 06:00 UTC and finish at 16:23 / 20:22 / +21h / +4 days — 5 of 5 recent pairs still running at 12:00. find_latest_pair() never checks build.finished_at. And compare() buckets regressed on tn_bad and in_base, where in_base only means the job exists in baseline, any state — canceled is the 2nd-commonest state (16,596/30d, ahead of failed). Fix: terminal-state gate + base_state == "passed" + cron +24h.
- GROUP BY job_name + anyIf is nondeterministic on this data. In the PR's own test pair, 7 names have mixed states — Kernels Core Operation Test = passed,passed,failed (3 shards). Same immutable data, different verdict per run; state/url/agent each resolve from a different row. Fix: group by (name, parallel_group_index), argMaxIf(x, finished_at, …).
- template injection:
${{ inputs.lookback_days }}spliced into run: in the step holding both secrets Unscoped Read + live AWS creds + public artifact.
details: https://www.internalfb.com/phabricator/paste/view/P2450487683
Builds start 06:00 UTC and take ~11h, but the tail is retries rather than new signal. On 82455 all five jobs that set the build duration had been retried 2-4 times and every one still ended failed; only 15 of 323 jobs retry at all. So the report does not need to wait for a terminal build state -- a job that has already failed is enough to triage, and waiting for retries to exhaust delays the result by hours without changing it. 12:00 UTC was 8:00 EDT, before any recent pair had finished even its first attempt.
…rministic Cron 18:00 -> 17:00 UTC (13:00 EDT). Measured across 11 recent pairs, every job's first attempt had finished by +10.6h, median +8.4h, so 17:00 is the earliest slot that still covers all of them; 16:00 would cover 8 of 11. The retry tail is what produced the +105h and +21h outliers and is not waited on, since retried jobs re-confirm a failure already visible hours earlier. Two correctness fixes from review: 1. A regression now requires base_state == "passed", not merely that the job exists in the baseline build. "Present in the baseline" also matches cancelled, skipped and still-running jobs, all of which were being scored as torch regressions. Jobs absent from the torch-nightly build are handled separately instead of falling through. 2. Group by (name, parallel_group_index) with argMax over finished_at instead of name alone with any(). Buildkite parallelism gives every shard the same name and shards disagree -- one measured pair had "Kernels Core Operation Test" as passed,passed,failed across three shards -- so any() returned a different verdict per run on identical data, with state, url and agent each able to come from a different row. Also: find_latest_pair walks torch-nightly builds newest-first rather than taking only the newest. Off-schedule builds (manual triggers outside the 06:00 slot) have no same-commit sibling, and stopping at the newest let one of them mask the most recent comparable pair -- observed live with 82682. Verified against 82454/82455: 26 regressed, 7 pre-existing, 0 baseline-only (was 27/6/4 before the passing-baseline requirement).
|
Thanks @izaitsevfb — the agent findings were accurate and all three are now fixed, plus a fourth bug that surfaced while verifying. Summary of everything changed since the review. 1. Cron timing
The original 12:00 UTC was indeed too early — measured over 11 complete pairs in the last 30 days, zero were finished at 12:00 UTC. The
So 17:00 UTC is the earliest slot covering all 11 pairs (16:00 covers 8 of 11). Deliberately no terminal-state gate — gating on build completion would wait on precisely those retry tails, delaying the report by hours without changing it. A job that has already failed is enough to triage. 2.
|
The workflow_dispatch lookback_days input was spliced into the run: block that
holds CLICKHOUSE_HUD_USER_* and BUILDKITE_CI_READ_ONLY, so a crafted value would
have been evaluated as shell in a step with live credentials. Nobody needed the
knob -- drop the input and pass a literal 14.
No run: block in this workflow interpolates ${{ }} any more.
Summary
vLLM's Buildkite pipeline runs three scheduled builds on
main, all from the sameHEAD:0 23 * * 1,2,4Full CI run torch nightlyTORCH_NIGHTLY=10 23 * * *Full CI run - nightly0 14 * * *Full CI run - dailyThe first two share the 23:00 slot, so on Mon/Tue/Thu they fire in the same second on the same commit, differing only by that one env var. That pair is a controlled A/B: a job failing in the torch-nightly build and passing in its sibling is attributable to torch, with the vLLM variable held constant. Nothing was consuming that signal.
What this adds
tools/torchci/vllm_torch_nightly_triage.py— finds the newest torch-nightly build with a same-commit baseline and:soft_failed(non-blocking by design) andretried(superseded attempts, otherwise double-counted).github/workflows/vllm-torch-nightly-triage.yml— two jobs:triage— runs the above Tue/Wed/Fri at 12:00 UTC, after the nightly has finished. Prints to the job log and run summary, uploads report + logs as an artifact.root-cause— analyses the result with the checked-in.claude/skills/vllm-pytorch-ci-triageskill.Reports only. Files nothing anywhere.
Why the A/B earns its keep
On build 82195 a naive "what failed on torch nightly" view showed 57 failures. 21 of those fail identically on the baseline — including six
Distributed Compile*timeouts that looked like a distributed-compile regression and are pre-existing. They would have been filed upstream wrongly.Security posture of the agent job
Logs are fetched by the
triagejob, not the model. So:--allowedTools "Read,Glob,Grep,Write"— no Bash, no network, no credentialsroot-causeholds no ClickHouse or Buildkite secrets, only Bedrock via OIDCFollows the
greenlight-pr-review.ymlpattern:claude-code-actiondirectly with a literal prompt,environment: bedrock. That works fromschedule, unlike_claude-code.yml, whose gate requires an@claudemention in an issue body.Test plan
Verified against live ClickHouse and Buildkite. Latest pair, #82454 vs #82455 at commit
50c51682a18c:Log fetching returns 22 cleaned cluster logs with real failures surfacing, e.g.
FAILED v1/test_tensor_ipc_queue.py::test_multiple_api_servers_to_engine - _queue.Empty.CI-verified on a temporary
pull_requesttrigger before it was removed:triagegreen,root-causecorrectly skipped. Locally clean under the repo's own linter adapters (PYFMT with the pinnedruff==0.14.4/usort==1.0.8.post1, RUFF, MYPY) andshellcheckon everyrun:block.Reuses the existing
CLICKHOUSE_HUD_USER_*secrets — no new ClickHouse credentials.Known limitations
This lands as a working example; a follow-up will replace the analysis core with the substantially better engine in morrison-turnansky/vllm-nightly-audit, which compares test signatures (
test_id,exception_class,exception_chain) rather than job names, classifiesNEW/PRE_EXISTING/NO_BASELINEagainst ≥3 main builds, and detects near-misses.Multi-Modal Processor (CPU) 1-4collapses correctly, but the NixlConnector family does not, because its distinguishing token is a prefix. Keyword clustering would fix it and risks merging unrelated jobs, so this is deliberately conservative — and superseded by the follow-up.BUILDKITE_CI_READ_ONLYcurrently returns 401 on every log fetch. Detection is unaffected, butroot-causegates onlog_count != '0'and will skip until the token is fixed (likely a trailing newline, missingread_build_logs, or thevllmorg not selected).root-causeis unverified. It cannot run on PRs by design, so first real exercise is aworkflow_dispatchafter merge.