Skip to content

fix(skills): correct nightly data gathering and lane scoring - #240

Draft
LukeAVanDrie wants to merge 1 commit into
llm-d:mainfrom
LukeAVanDrie:nightly-gather-fixes
Draft

fix(skills): correct nightly data gathering and lane scoring#240
LukeAVanDrie wants to merge 1 commit into
llm-d:mainfrom
LukeAVanDrie:nightly-gather-fixes

Conversation

@LukeAVanDrie

@LukeAVanDrie LukeAVanDrie commented Aug 12, 2026

Copy link
Copy Markdown

What does this PR do?

Corrects the bash and jq in .claude/skills/nightly/. nightly/SKILL.md triages a failing nightly run and nightly/report/SKILL.md produces the weekly fleet report. An agent runs their code blocks verbatim against the GitHub Actions API, so a wrong query returns a wrong report without raising an error. Only .claude/ changes; CI behavior is unchanged.

Sub-skill reachability. Skill discovery scans .claude/skills/*/SKILL.md one level deep, so the seven sub-skills at .claude/skills/nightly/<name>/SKILL.md were never registered and Skill(nightly:<x>) failed. All 13 references in the router and in .claude/CLAUDE.md become file paths to Read.

Data gathering. gh run list --limit N sampled the fleet. llm-d/llm-d served 915 scheduled runs over 2026-08-06..12, so --limit 500 covered under five days, and both --limit and the REST endpoint stop at 1000 results while exiting 0. Queries page the REST endpoint with server-side event and created filters, one day per query, and check each day's returned count against the server's total_count. Lane selection moves to .path with an exclude list, which keeps the consolidate-status-* workflows out of the fleet count. The daily triage window is a rolling 24-hour timestamp; a bare date spanned two nights and doubled every lane.

Coverage gaps. A new phase anti-joins the workflow registry against the runs, so a lane that stopped running no longer reads as healthy. Two guards keep the result honest. Lanes registered within the last two days are marked TOO NEW, since they have not reached a first cron. A token-overlap matcher proposes rename successors, because the -acc- migration left no shared prefixes to match on. Candidates are filtered by the same exclude list as lane selection; unfiltered, consolidate-status-<lane> carries the whole lane name plus two tokens and wins the overlap on every row. The matcher emits its top two candidates, because token overlap ties on platform synonyms and ibm- is the OpenShift-GPU prefix.

Triage on error text. Standup and Run each cover several unrelated causes, so classifying on the step name grouped failures wrongly. Step 4b pulls the failing step's output: errtext.sh windows the job log on the step's timestamps, then falls back to errsig.sh, which greps the whole log through a noise filter, when the window names no cause. Step 4c adds a trailing failure streak and last-green date per lane. Past roughly 15 red lanes, an Explore subagent summarizes the collected error text so it stays out of the conversation context.

Lane scoring. Lanes classify on pass rate and trailing streak together. NO VERDICT is a majority-of-cancellations test, and those lanes stay out of attribution and clustering. Lifetime streaks count decisive runs only, and report NEVER PASSED, NO DECISIVE RUN or LOOKUP FAILED rather than dropping a lane out of the table.

Report content. Figures the template left as placeholders are computed: fleet summary, lifetime table, failure categories derived from the step-ID prefixes in reusable-ci-nightly-benchmark.yaml, per-lane failure cost, and under-merge detection in clustering.

Shared scripts. The shell and jq live in .claude/skills/nightly/scripts/ as 15 files with a README.md index, invoked as sh $NS/<name>.sh. Both documents need most of the same code, and duplicating it as heredocs let the copies drift: the TOO NEW guard reached one document and not the other, so the weekly report counted every lane added yesterday as a coverage gap. Scripts that fan out feed xargs one NUL-delimited record and split the fields with cut, because xargs splits on any whitespace and a lane display name like Nightly - Optimized Baseline E2E (GKE GPU) shifts every field of that row and of the rows batched after it.

Reference tables. llm-d-workload-variant-autoscaler nightlies run in llm-d/llm-d. llm-d-infra's own three nightlies were unmonitored. The reusable-*-helmfile.yaml entries in the router and in .claude/CLAUDE.md named workflows that do not exist. The schedule matrix is derived from the crons instead of hand-maintained.

Why is this change needed?

Each defect fails silently. Truncation drops the oldest runs and exits 0; a 7-day query returned 1000 of 1020 runs. A removed lane contributes no runs and reads as healthy. An unregistered sub-skill raises no error, so sessions improvised the missing phase.

How was this tested?

  • Manual testing performed

Every bash and jq block in both files ran in order against llm-d/llm-d and llm-d/llm-d-infra with exit 0: 278 scheduled runs over 7/7 days for llm-d, 42 lanes classified, 5 REMOVED gaps each proposing a running -acc- successor, 24 failures attributed to their steps, 27 lifetime rows with no stderr.

Edge cases exercised: the truncation guard on a >1000-run range, which exits 1 with the split-by-hour message; errtext.sh on failed runs and on a run with no failing job; a workflow basename the API 404s on; a lane whose every run was cancelled; the Phase 6 window check for equal length and no overlap; an all-green input; an in-flight run; a failed log download; a month-boundary window.

The failure-category taxonomy was cross-checked against the badge commit messages on gh-pages.

Checklist

  • Commits are signed off (git commit -s) per DCO
  • Code follows project contributing guidelines
  • Tests pass locally (make test) — n/a, no Makefile in this repo
  • Linters pass (make lint) — n/a, no linter covers .claude/
  • Documentation updated (if applicable)

@LukeAVanDrie
LukeAVanDrie force-pushed the nightly-gather-fixes branch 4 times, most recently from a29d5e4 to 666432f Compare August 14, 2026 18:09
@LukeAVanDrie

Copy link
Copy Markdown
Author

Here is an example report that this skill generates now. Triggered via /nightly.


Nightly Status — 2026-08-14

26 of 44 scheduled runs failed. Seven distinct causes. Two are new as of the night of 2026-08-13 and account for 12 lanes — both trace to image-pin changes merged that day. No coverage gaps.


1. Bad release image pins from #2260 — 5 lanes, new 2026-08-13

ada02c1b Image component resturcture and v0.26.0 bump (#2260) moved image pins into shared kustomize components. Two tags in the release/ variant do not exist in their registries:

File Pins Registry says
guides/recipes/modelserver/components/images/routing-sidecar/release/kustomization.yaml ghcr.io/llm-d/llm-d-router-disagg-sidecar:v0.8.1 tag absent — published tags jump mainv0.9.0v0.10.0-rc.1
guides/recipes/modelserver/components/images/gpu-sglang/release/kustomization.yaml docker.io/lmsysorg/sglang:v0.5.16.0 httperror 404: tag 'v0.5.16.0' not found

The sidecar is an init container (routing-proxy), so the decode pod never starts at all.

Failed to pull image "ghcr.io/llm-d/llm-d-router-disagg-sidecar:v0.8.1": rpc error:
code = NotFound … failed to resolve reference … : not found
Lane Streak Last green
PD Disaggregation (CKS GPU) 2 2026-08-12
PD Disaggregation (Intel XPU) 2 2026-08-12
PD Disaggregation (AMD ROCM) 2 2026-08-12
Wide EP LWS (GKE GPU) 46 2026-06-29
Optimized Baseline (GKE GPU SGLang) — sglang tag 2 2026-08-12

Will not clear on a rerun. Wide EP GKE was already chronic for another reason; this is now its top blocker.

2. vLLM v0.26.0 modelserver crash-loops — 7 lanes, new 2026-08-13

Same commit bumped vllm/vllm-openai v0.23.0 → v0.26.0. That tag pulls fine — the container starts and dies:

Back-off restarting failed container modelserver
Startup probe failed: Get "http://10.0.3.99:8000/v1/models": dial tcp … connection refused
Readiness probe failed: service unhealthy (responded with "NOT_SERVING")

Optimized Baseline (Intel XPU) · Optimized Baseline (CKS GPU) · Precise Prefix Cache Routing (CKS GPU) · Precise Prefix Cache Routing (Intel XPU) · Predicted Latency Routing (CKS GPU) · Tiered Prefix Cache CPU Offloading (Intel XPU) · Wide EP LWS (Intel XPU)

Five of the seven were green on 2026-08-12. Optimized Baseline (CKS GPU) has been red since 08-05 for a different reason; last night's failure is this one.

The harness collects events and describe but no container logs, so the actual vLLM stack trace is not in any job log. That is the first thing to fix if you want this diagnosed rather than bisected.

3. GKE TPU — DWS requests never fulfilled (4 lanes, chronic)

timed out waiting for the condition on pods/tpu-request-job-<id>
Error from server (AlreadyExists): namespaces "llm-d-nightly-…-gke-tpu" already exists

Optimized Baseline (17, last green 07-28) · PD Disaggregation (16, 07-29) · Tiered Prefix Cache (5, 08-09) · Precise Prefix Cache (2 runs, never green — new lane). TPU capacity, not the guides. The stale namespace shows teardown is also not cleaning up.

4. Benchmark harness never completes (3 lanes, last green 2026-08-05)

Pods did not complete within 1200s (phases=['Running']) at [07] deploy_harness, identical on OCP GPU, GKE GPU and AMD ROCM — shared harness code, not any one cluster. Streaks 9 / 9 / 15.

5. GKE GPU cluster out of capacity (2 lanes)

0/13 nodes are available: 4 cannot allocate all claims, 9 Insufficient cpu, 9 Insufficient memory — PD Disaggregation GKE GPU (38, last green 07-07) and GKE GPU SGLang (24 runs, never green).

6. CKS RDMA capacity (1 lane)

0/7 nodes are available: 1 Insufficient rdma/ib, 1 node(s) were unschedulable, 6 Insufficient … — Wide EP LWS (CKS GPU), 46, last green 06-29.

7. Four lanes broken since they were added

Lane Runs / green Cause
Flow Control (GKE GPU) 65 / 0 evalsymlink failure on '…/guides/flow-control/modelserver/gpu/vllm///gke' — empty path segments
Workload Autoscaling (CKS GPU) 63 / 0 quay.io/projectquay/golang:1.26@sha256:ee447103…: not found — pinned digest gone
Upstream Dependency Monitor (infra) 100 / 0 cat: /opt/gh-aw/prompts/xpia.md: No such file or directory
Workload Autoscaling (OCP GPU) 4, green 08-10 hit 27s timeout running git fetch …llm-d-workload-variant-autoscaler main — remote kustomize base

These need their authors, not tracking issues.


Coverage Gaps

None. Five lanes show REMOVED; all five are the -acc- rename and all five successors ran last night.

Retired Successor (token overlap)
nightly-e2e-wide-ep-lws-gke …-wide-ep-lws-gke-acc-gpu-vllm-x (4/4)
…-tiered-prefix-cache-gke-tpu …-tiered-prefix-cache-gke-cpu-tpu-vllm-native (5/5)
…-tiered-prefix-cache-cpu-offloading-gke …-tiered-prefix-cache-gke-cpu-gpu-vllm-native (5/6)
…-cpu-offloading-lmcache-gke …-gke-cpu-gpu-vllm-lmcache (6/7)
…-cpu-offloading-ocp …-tiered-prefix-cache-ibm-cpu-gpu-vllm-native (4/6 — ibm- is the OCP-GPU prefix)

Highest-leverage fix: repin the two tags in guides/recipes/modelserver/components/images/*/release/kustomization.yaml (sidecar → v0.9.0, sglang → an existing tag). That clears 5 lanes outright. The v0.26.0 crash needs container logs first — 7 more lanes ride on it.

The nightly skills are executable documentation: a session reads them and
runs their bash and jq against the GitHub Actions API to triage failures and
produce the weekly report. Both sampled a fraction of the fleet, scored the
lanes they did see against wrong denominators, and asked for report fields no
phase computed.

Gathering

  gh run list --limit N truncates. llm-d/llm-d produced 915 scheduled runs
  over 2026-08-06..12, so Step 1's --limit 50 covered about nine hours and
  Phase 0's --limit 500 under five days. Raising it does not help: --limit and
  the REST runs endpoint both stop at 1000 results and exit 0. A 7-day
  server-side query returned 1000 of 1020, dropping 20 runs across 12 nightly
  lanes. Both skills now page the REST endpoint one day per query and check
  each day against the server's total_count.

  Neither skill filtered on event, so PR and dispatch runs diluted pass rates.
  The [Nn]ightly|[Ee]2[Ee] name regex also swept in 252 weekly runs of the
  consolidate-status-* workflows, which are named "Past Status - ... E2E
  (...)". Lane selection is now on .path with an explicit exclude list, and
  event=schedule is applied server-side.

  A lane that stops running is invisible to a check that starts from a list of
  runs: it reads as not failing while testing nothing. A new phase anti-joins
  the workflow registry against the runs, classifies each zero-run lane
  REMOVED, STALE, NEVER RAN, DISABLED, TOO NEW or LOOKUP FAILED, and proposes
  rename successors by token overlap.

Scoring

  Phase 1 crashed with "Cannot index object with null" whenever a nightly was
  in flight, which is most nights, because {...}[null] raises before // "?"
  can apply. In-progress runs now render as ?.

  Pass rate alone cannot separate a lane that broke last night from one that
  passed early in the week and has been dead since; both read 3/7. Lanes now
  classify on rate and trailing failure streak together. NO VERDICT fired only
  when every run was cancelled, so one stray failure among seven cancellations
  scored CHRONIC 0/1. It is now a majority test, exposed as a `red` field that
  attribution, clustering and lifetime lookup all select on; each previously
  selected on the streak alone and pulled in NO VERDICT lanes carrying a stale
  failure reported as current. Lifetime streaks counted cancellations as
  failures, turning a lane with 53 cancellations and 2 failures into a
  55-night streak reported as the fleet's oldest breakage. They count decisive
  runs only.

  Week-over-week compared a 7-day window against an 8-day one, so a lane's
  denominator changed between halves of the comparison. Both windows are now
  $DAYS long and disjoint, and the block asserts it. An all-green week
  produced two jq errors: `add` over an empty array is null.

Routing

  Skill discovery scans .claude/skills/*/SKILL.md one level deep. All seven
  sub-skills sit at .claude/skills/nightly/<name>/SKILL.md, a level below
  that, so none was registered and the `name: nightly:<x>` in their
  frontmatter declared an identity the loader never reads. A session reaching
  for one got "no such skill" and improvised the phase from scratch. All 13
  references are now file paths to Read.

  Step 4 classified straight off the failed step name, but Standup and Run
  each name a whole phase of the reusable workflow. On 2026-08-12 they covered
  a pod-readiness timeout, DWS provisioning starvation, a malformed kustomize
  path and a CrashLoopBackOff between them. Step 4b pulls each failing step's
  real output first and groups lanes on error text.

Error text

  gh run view --log renders every step as the literal string UNKNOWN STEP, and
  the run-level log zip holds per-job files only. errtext.sh windows the job
  log on the failing step's timestamps and cuts on the ##[group] boundaries,
  taking 1700 lines to under ten. The download needs --allow-escape-sequences,
  without which gh exits 1 on a body carrying ANSI and prints a header and
  nothing else, indistinguishable from a step that produced no output.

  Standup emits its own ::group:: around the env dump, so the cut lands inside
  it and returns a content-free body on 9 of 34 runs. errtext.sh falls back to
  errsig.sh, which greps the whole job log for error signatures, excluding
  four post-failure debug patterns that appear in 29 of 41 failing job logs
  and crowd the real cause out of the tail. All nine bodies then name a cause.

Report content

  Computed the figures the template left as placeholders: fleet decisive-run
  count, ex-chronic pass rate, never-passed count, mean failure minutes per
  lane, under-merge detection in clustering, and lifetime streaks carrying a
  first-run column so a lane merged yesterday does not qualify as
  never-passed on a single run. Failure categories come from the step-ID
  prefixes in reusable-ci-nightly-benchmark.yaml, replacing a five-category
  table no phase produced. Categorizing by step name reported phantom
  accelerator contention: "Request TPUs (DWS)" is infra_request_tpus and dies
  in about a second on stale state.

Shared scripts

  The shell and jq live in .claude/skills/nightly/scripts/ as 15 files with a
  README index, invoked as sh $NS/<name>.sh. Both documents need most of the
  same code, and duplicating it as heredocs let the copies drift: the TOO NEW
  guard reached one document and not the other, so the weekly report counted
  every lane added yesterday as a coverage gap. Scripts that fan out feed
  xargs one NUL-delimited record and split the fields with cut, because xargs
  splits on any whitespace and a lane display name shifts every field of that
  row and of the rows batched after it.

Reference tables

  llm-d-workload-variant-autoscaler has no scheduled E2E of its own; those
  lanes live in llm-d/llm-d as nightly-e2e-workload-autoscaling-*.
  llm-d-infra's own three nightlies were unmonitored. The
  reusable-*-helmfile.yaml workflows named in the table do not exist, and the
  same stale names appeared in .claude/CLAUDE.md, which loads into every
  session in this repo. reusable-ci-nightly-benchmark.yaml is not
  workflow_call-only: it carries `schedule: - cron: '0 0 * * *'` and runs in
  llm-d-infra in its own right. The schedule matrix is derived from the crons.

The session shell is zsh, where "/$cand[.]y" parses as an array subscript and
raises "bad floating point constant", and `set -- $pair` does not word-split.
Both are named at the top of the router.

Every bash and jq block in both files ran in order against llm-d/llm-d and
llm-d/llm-d-infra, including the empty, all-green, in-flight, failed-download
and month-boundary paths.

Signed-off-by: Luke Van Drie <lukevandrie@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant