A running playbook of failures the Klaud-Cold image-bump cron has hit, the diagnoses, and the fixes/workarounds applied. Read this first before debugging a new failing claude/* PR — most failure modes here recur.
When you fix something not yet listed, add it here so the next session doesn't re-learn it.
Symptom: the setup job fails before any sweep runs with
ValueError: Deletions are not allowed in /home/runner/work/InferenceX/InferenceX/perf-changelog.yaml.
Only additions to the changelog are permitted. Found deleted line: ...
Root cause: Cron-PR branches go stale; when main merges new changelog entries, the PR's local snapshot of perf-changelog.yaml no longer covers them, so the validator sees the missing lines as deletions. A naive rebase can also strip trailing whitespace from unrelated entries — same effect (e.g. pr-link: ...1311 → pr-link: ...1311).
Fix (canonical):
# In the PR's worktree, after `git merge origin/main` conflicts on perf-changelog.yaml:
git checkout origin/main -- perf-changelog.yaml # take main's bytes verbatim
cat >> perf-changelog.yaml <<EOF # then append THIS PR's entry at tail
- config-keys:
- <recipe-key>
description:
- "<one-line summary>"
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/<N>
EOF
python3 -c "import yaml; yaml.safe_load(open('perf-changelog.yaml'))"Do not try a 3-way merge of perf-changelog.yaml — whitespace edits will silently re-trigger the deletion check.
After committing and pushing the resolution, the synchronize run checks the
changelog with the same matrix processor used by setup, then checks the reuse
authorization. This catches deleted history or malformed appended entries
before reuse can skip setup. utils/merge_with_reuse.sh <PR> performs the push
and waits for the PR checks automatically.
Symptom: vLLM workers OOM during weight loading or right after warmup:
HSA_STATUS_ERROR_OUT_OF_RESOURCES: Available Free mem : 0 MB(AMD)torch.OutOfMemoryError: CUDA out of memory. ... GPU N has X GiB of which Y MiB is free(NVIDIA)- vLLM may also log
_check_enough_kv_cache_memoryfailing with negative available bytes (e.g.-25.24 GiB).
Root cause: v0.21.0 (and v0.20.2+) enabled an aggressive CUDA-graph memory profiler that pre-reserves a large chunk of VRAM up front (~30% on B200), shrinking effective --gpu-memory-utilization well below what the flag says. Old SHA-pinned custom images had a smaller footprint, so the recipe's existing 0.95 setting now starves the KV cache.
Fix: in benchmarks/single_node/<recipe>.sh, either:
- Lower
--gpu-memory-utilization(0.95 → 0.90, sometimes 0.85). Matches the H100/H200/B200 NVIDIA pattern. Smallest blast radius. - Disable the profiler entirely for cases where lowering isn't enough:
export VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0beforevllm serve. Matchesbenchmarks/single_node/agentic/kimik2.5_fp4_b200.sh:65.
Seen on: #1395 (kimik2.5-fp4-b200-vllm — needed env var), #1403 (gptoss-fp4-mi300x-vllm — needed 0.90), #1461 (dsv4-fp8-h200-vllm — needed 0.90).
Symptom: TP1 + data/expert-parallel decode workers load successfully and
allocate the KV cache, then fail in breakable_cudagraph.py at
torch.cuda.graph.capture_end() with CUDA error: out of memory. Large GB300
VRAM does not prevent this because vLLM fills the configured memory budget with
KV cache before capturing hundreds of persistent graphs.
Root cause: max-num-seqs and max-cudagraph-capture-size were sized from
global benchmark concurrency instead of per-DP-rank concurrency. MiniMax-M3
DEP4/DEP8 recipes requested capture sizes of 4096-8192 and up to 4096 sequences,
creating 358-806 graphs per GPU.
First-line tuning: keep gpu-memory-utilization: 0.90, but size graph limits
to the per-rank load. For the GB300 MiniMax-M3 sweep, use
max-num-seqs: 512 and max-cudagraph-capture-size: 2048 on DEP decoders.
This matches the single-node GB300 recipe and still covers the largest 512
requests per DP rank. If capture still OOMs, lower decode
gpu-memory-utilization to 0.85.
Seen on: #1735 (MiniMax-M3 MXFP8 GB300 dynamo-vLLM).
Symptom: DSV4 recipes work on their SHA-pinned lmsysorg/sglang:deepseek-v4-hopper@sha256:... (or deepseek-v4-b300, deepseek-v4-blackwell) custom builds, but OOM on weights load when bumped to the generic v0.5.12-cu130 release tag. Example: DSV4-Pro FP8+MTP weights consume ~125.43 GB / 141 GB per H200, leaving -4.05 GB for KV cache.
Root cause: The custom DSV4 images use a different weight layout / EAGLE draft handling that fits in less memory than the generic release. The release tag isn't a drop-in replacement.
Fix: keep DSV4 recipes pinned to their custom SHA-pinned image until upstream sglang gains the same DSV4-specific weight handling. Bumping to the generic tag is currently NOT viable.
Seen on: #1460 (dsv4-fp8-h200-sglang+mtp).
Three distinct upstream regressions on NVIDIA B300 (Blackwell Ultra, sm_103 — compute capability 10.3) shipped in lmsysorg/sglang:v0.5.12-cu130. (sm_120 is for consumer Blackwell / RTX 50 series, not B300 — don't propagate that.)
Symptom: CUDA graph capture aborts with CUDA_ERROR_ILLEGAL_ADDRESS (700) at /deepgemm/csrc/.../runtime_utils.hpp:143 on the first batch size for every TP rank. Server never serves a prompt.
Workarounds (any one):
--fp8-gemm-runner-backend cutlassto bypass DeepGemm via CUTLASS.export SGL_ENABLE_JIT_DEEPGEMM=0beforepython -m sglang.launch_serverto skip JIT DeepGemm.- Pin recipe to
lmsysorg/sglang:v0.5.11-cu130.
Filed upstream: sgl-project/sglang#25551. Seen on #1421.
Symptom: EAGLE draft CUDA graph capture crashes immediately at the largest batch size with RuntimeError ... trtllm_batched_gemm_runner.cu:276 ... numBatches=256, GemmMNK 128x1024x6144. The target model captures fine; only the draft model crashes.
Workarounds:
- Cap
--cuda-graph-max-bsand--max-running-requeststo 64 in the launch script to avoid the bs=128 trigger. - Comment out the MTP/EAGLE scenarios on B300 in the recipe.
- Pin to v0.5.11-cu130.
Filed upstream: sgl-project/sglang#25563. Seen on #1420.
Symptom: All 4 TP workers AssertionError on first forward pass:
File "/opt/venv/.../sglang/srt/layers/attention/flashattention_backend.py:..."
assert sm_100 <= arch <= sm_110f
B300 is sm_103 (compute capability 10.3, Blackwell Ultra) — which is nominally inside the asserted sm_100..sm_110f range, yet the assertion still fires. Best guess is the cute kernel's Arch.sm_110f set only matches the architecture-specific feature-flag variants it was compiled for (e.g. sm_100, sm_100f, sm_110, sm_110f) and sm_103 / sm_103a isn't in that explicit list. Server never becomes healthy; warmup times out at 600s.
Fix: Needs an sglang image with flash_attn that recognises sm_103 / sm_103a — no local workaround. Pin to v0.5.11-cu130 in the meantime.
Seen on #1422.
- g09:
pyxis is broken - g19:
Kill task failed (JobId=N StepId=N) - g37:
permission issues with GHA runner workflows : Not responding(down since Mar 2026)
If a sweep job lands on any of these, it'll never start. Nothing to do at the recipe level — these stay drained until ops fixes them.
Symptom: mi355x jobs fail with permission denied while trying to connect to the docker API at unix:///var/run/docker.sock during the docker stop $(docker ps -a -q) cleanup step, cascading into SLURM job expiration.
Fix: ops needs to fix docker group / socket perms on these nodes. Recipe-level workaround: none.
Symptom: pyxis container extraction fails with No space left on device writing to /nvme_home/gharunner/.local/share/enroot/pyxis_*/opt/rocm-*/.... The /nvme_home partition is hosted under / on this node and has been chronically near-full.
Fix already landed: runners/launch_mi300x-amds.sh now pins salloc to only known-good mi300x nodes (chi-mi300x-[034-036,054,057-058]) — see PR #1462. chi-mi300x-049 is held in State=DOWN by a watchdog on the controller (/home/gharunner/_audit/drain_049_watchdog.sh) that re-applies the drain every 10s if SLURM auto-clears it (which it does on dynamic-norm nodes).
Symptom: sglang server bind fails with [Errno 98] Address already in use on port 8888. Held by an MLPerf accuracy run started outside SLURM.
Fix: SSH to controller, find the holder via ss -tlnp | grep :8888, kill the PID. If recurring, file with the team running MLPerf experiments.
- amd-vultr-mi300: SLURM controller for 7 mi300x nodes (3 down, see 5.3).
- amd-vultr-mi325: SLURM controller for 6 mi325x nodes.
- amd-tw-mi355: jumpbox → ssh further to compute nodes (
mia1-p01-gNN). 12 nodes (3 drained, see 5.1). /homeis NFS-mounted across clusters fromchi-mi325x-pod1-001:/nfs/homes, root-writable./tmpand/nvme_homeare per-node local; HF cache lives at node-local/raid/hf-hub-cache/(2.7T per mi300x node).- Use
srun -w <FQDN>(with the full FQDN, not the short hostname) from the controller to run admin commands on a compute node.
SLURM auto-clears State=DRAIN on DYNAMIC_NORM nodes when they re-register. To keep a node out of the pool sticky-style, use State=DOWN AND start a watchdog:
# on the controller, as root
nohup bash -c '
while true; do
s=$(scontrol show node <FQDN> 2>/dev/null | grep -oE "State=[A-Z+_]+")
if ! echo "$s" | grep -qE "DOWN|DRAIN"; then
scontrol update NodeName=<FQDN> State=DOWN Reason="watchdog" >/dev/null 2>&1
fi
sleep 10
done
' > /home/gharunner/_audit/drain_<node>_watchdog.log 2>&1 &Doesn't survive controller reboots — for permanent removal a SLURM admin should edit slurm.conf.
Don't invent a "release" tag pattern from a date-suffixed nightly. lmsysorg/sglang-rocm:v0.5.12-rocm720-mi35x does not exist — only the dated v0.5.12-rocm720-mi35x-20260517 does. All MI355X sglang-rocm:rocm720 tags follow the dated-nightly pattern.
Before bumping an image, verify the target tag exists:
curl -sI "https://hub.docker.com/v2/repositories/lmsysorg/sglang-rocm/tags/v0.5.12-rocm720-mi35x"
# 200 → exists; 404 → doesn'tOr check whether any other recipe on main uses the proposed tag — if zero uses, suspect.
gh run rerun <id> --failedonly works when the workflow run is completed withconclusion=failure. If the run is stillqueued/in_progress, the call returns "cannot be rerun".- To abandon an in-flight run and start fresh, push an empty commit to the PR branch:
The old run will be auto-cancelled by
git commit --allow-empty -m "Re-trigger sweep" git pushworkflow/cancel-sweep-on-merge(provided the head SHA changed). - For a
cancelledrun (notfailure), usegh run rerun <id>without--failedto re-run everything.
Reusable source artifacts are authoritative. The merge-time
reuse-ingest-artifacts job validates that downloaded artifacts are readable,
non-duplicated, and internally consistent, but it does not require them to
match a matrix regenerated from the merge commit. A generator-policy change
between the PR sweep and merge therefore does not require another GPU sweep.
Raw and aggregate eval identities must still match, as must agentic point/raw
artifacts and summaries. Batched eval identities come from
completed_eval_concs, so an explicitly pinned failed run may reuse only the
points it completed. Missing or invalid metadata, duplicate identities, and
raw/aggregate disagreement still fail reuse.
gh pr editsilently aborts on a Projects-classic deprecation GraphQL error. Title/body updates won't apply. Usegh api -X PATCH "repos/<org>/<repo>/pulls/<N>" -f title="..." -F body=@file.mdinstead.- Same issue for adding labels — use
gh api -X POST "repos/<org>/<repo>/issues/<N>/labels" -f "labels[]=<name>". gh pr view ... --jq .headRefNameoutput can have a trailing\r. Strip it:gh pr view <N> --json headRefName --jq .headRefName | tr -d '\r\n'. Otherwise shell concatenation producesbranchunners/launch_mi300x-amds.sh-style corruption.gh pr list --json statusCheckRolluptruncates each PR's rollup — never trust it for per-check filters. Re-query each PR individually withgh pr view <N> --json statusCheckRollup.ghand the GitHub Actions API:conclusionis""(empty string, notnull) for in-flight checks, sojq's// .statusfallback doesn't trigger. Use:def state: if (.conclusion // "") != "" then .conclusion else .status end;
- Image-bump / new-recipe PRs I open on behalf of the user (or that the user creates) get the
[Klaud Cold]title prefix. - Add the
full-sweep-enabledlabel so a canary-gated full sweep actually runs (gh api -X POST ... labels[]=full-sweep-enabled). Usenon-canary-full-sweep-enabledinstead only when the single-node canary is flaky or unrepresentative; it runs the full sweep without the canary gate. Without one of the sweep labels, the sweep is mostly SKIPPED. - After any code change that shifts a PR's scope (drops a recipe, changes an image tag), update the PR title AND body in the same step and verify with
gh pr view <N> --json title,body—gh pr editsilently fails (see §8). utils/merge_with_reuse.sh <N>is the merge entrypoint; it handles theperf-changelog.yamlauto-append.
/find-mergeable-claude-prs— listsclaude/*PRs whose full sweep finished all-green./list-claude-pr-status— lists READY/RUNNING (and optionally FAILED) state perclaude/*PR./fix-klaud-cron-prs— diagnoses failingclaude/*PRs by reading their failed job logs./merge-prs <N> [<N>...]— sequential merge viautils/merge_with_reuse.sh.
Each command file is self-contained; read them to understand the exact jq filters they use.
Symptom: MiniMax M3 fails during MSA kernel warmup with:
ValueError: q2k_indices must be contiguous with layout [head_kv, total_q, topK]
The stack ends in sparse_attention_msa.py -> build_k2q_csr(). TP4/TP8
canaries may pass while TP1 data-parallel-attention jobs fail.
Root cause: vllm/vllm-openai:minimax-m3-0618-x86_64-cu130 stores top-k
indices in a persistent [head_kv, max_num_batched_tokens, topK] buffer for
CUDA graphs. The MSA prefill path slices the token dimension before calling
build_k2q_csr(). That view retains the full-buffer head stride and is not
contiguous when a worker has multiple local KV/index heads. Data-parallel
attention forces TP1, exposing all four MiniMax M3 KV/index heads per worker.
Workaround: Before server startup, patch the installed
vllm/models/minimax_m3/nvidia/sparse_attention_msa.py assignment from:
prefill_topk = topk[:, nd:num_tokens, :]to:
prefill_topk = topk[:, nd:num_tokens, :].contiguous()Use an exact-source guard and remove the workaround once the image includes the fix.
Seen on: #1834.