fix(doctor): node-fit checks FREE memory, not allocatable (backend#2870) - #628
Conversation
…-committed control plane is caught (backend#2870) `checkNodeFit` fit the training envelope against each node' Allocatable and never against what is already requested on it. So a node whose control plane claims memory passed -- allocatable said "big enough" -- while the pod went Pending / Insufficient memory. This is the layer the ticket says owns nothing: the installer sizes from allocatable, cli doctor checked against allocatable, and the one number that would expose it (free memory) was computed and thrown away. Sum the requests already on each Ready node and fit cpu+memory against FREE (allocatable - requested, floored at 0). A node large enough by allocatable but not beside its own control plane now FAILS with a message that names the over-commit, distinct from "no node is big enough at all". Fail-closed: if the pod list cannot be read, free is UNKNOWN -- say so and fall back to allocatable rather than passing it off as free. Terminal pods hold nothing and are skipped (mirrors requestedMemory). Scope: this is DoD item 3 of backend#2870 (the cli-doctor layer). The installer-side refusal (1), the footprint schedulability test tied to the #2460 constant (2), the jobs-manager permanent-vs-transient message (4) and the CPU envelope audit (5) remain. Test: a 16Gi node whose control plane requests 12Gi (4Gi free) refuses an 8Gi envelope; the same node without the neighbour passes (the subtraction is load-bearing); a terminal neighbour is ignored. Mutation-proven: dropping the memory subtraction flips the over-commit case to a false OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
v0.10.21 is already released and this PR changes published files under internal/*; the version-bump-gate requires the develop VERSION to lead the released tag so the next train hop cuts an unreleased version. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saqlainsyed007
left a comment
There was a problem hiding this comment.
Holding — CI is red (bugbot / review failing) and three Bugbot threads are open, so this isn't landable yet. I verified the top one against the code rather than taking it on trust:
"Doctor fails during healthy training" (High) is real. checkNodeFit sums requests from every non-terminal, node-assigned pod — which includes a currently running training job that already holds the envelope — then tests whether a fresh cpuReq/memReq fits in the remainder. On a single-node install with a healthy training job running, freeMemB = allocatable − (that job's request) is below the envelope, so nodeCPUMem goes false, the node reports as not fitting, and doctor exits 2 while training is fine. That's a false negative on the exact healthy state doctor is meant to bless.
The other two are worth a look while you're in there:
- "Unknown free reported as schedulable" (High): the code comment says the unknown-free caveat "is reported below" — please confirm the
StatusOKpath actually emits it whenfreeKnown == false, since that's precisely what the finding says is missing. - "Over-commit message always blames memory" (Medium): the over-commit copy naming FREE memory unconditionally will misreport a CPU or ephemeral-storage shortfall.
Requesting changes; happy to re-review as soon as these are addressed and CI is green.
…free, name the short dimension (backend#2870) Three findings on the first cut (Saqlain + Bugbot): - HIGH: the free sum counted every non-terminal pod INCLUDING a running training job that already holds the envelope, so on a single-node install doctor returned StatusFail and exited 2 during HEALTHY training -- a false negative on the state it exists to bless. Skip batch-Job pods (the job-name label the batch/v1 controller stamps); the envelope must fit beside the STEADY-STATE control plane, not a transient workload. - HIGH: when the pod list could not be read the OK path returned StatusOK with no caveat, asserting free schedulability it never verified. Now WARN with an allocatable-only caveat. - MEDIUM: the over-commit message always blamed FREE memory; it now names the actual short dimension (cpu / memory / both). Tests: a running job labelled job-name is excluded (healthy training stays ok); an unreadable pod list warns; a cpu over-commit names cpu. All mutation-adjacent and green; golden updated for the new copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
saqlainsyed007
left a comment
There was a problem hiding this comment.
Thanks for the fast turnaround — my three original findings are genuinely addressed on this commit: running jobs are excluded from the free sum via the job-name label (no more false-fail during healthy training), unknown-free now returns StatusWarn rather than a clean OK, and the over-commit message tracks overCPU/overMem.
Still holding, because Bugbot's pass on the new commit caught two real follow-ons to those same two fixes — and I've confirmed both against the code:
- Disk shortfall still blamed on memory (
doctor.go:769): the over-commit arm fires on!cpuMemFits && allocOnlyFit, butallocOnlyFitonly inspects allocatable cpu+memory — a node that fits cpu+memory and fails only onephemeral-storagestill takes this arm, andoverCPU/overMemare both false, soshortdefaults to"memory". The message fix covered cpu but not the disk dimension — the same misdiagnosis classTestCheckNodeFitDiskexists to prevent. - Rollup still greens unverified free capacity (
doctor.go:818): the newStatusWarndetail beginsa Ready node can schedule a training job, butsummarizeDoctoronly treats the two older Node-capacity Warn prefixes as a can't-check — so it falls through to "Ready to run training" and prints "Everything looks good" at exit 0 on the default path. The Warn is there but the rollup doesn't act on it, so the over-committed control plane this PR exists to surface stays invisible without--verbose.
Fold the ephemeral-storage dimension into the short-dimension logic, and teach summarizeDoctor to treat the new Warn prefix as a can't-check, and this is good to go. CI is also red on bugbot / review for these.
…blame disk shortfalls on memory (backend#2870) Two follow-on findings from the free-memory fix: - HIGH: the unknown-free StatusWarn detail started with "a Ready node can schedule...", which summarizeDoctor does not recognise as a can't-check, so the rollup fell through to "Ready to run training" at exit 0 -- the over-commit stayed invisible on the default path. Give the warn a distinct "could not read the pod list" prefix and teach the rollup to treat it as a can't-check (Unknown), beside the RESOURCE_REQUESTS / nodes-unlistable ones. - MEDIUM: the over-commit arm fired on !cpuMemFits && allocOnlyFit, which a DISK-only shortfall also satisfies (allocOnlyFit is cpu+mem only), so a disk short was reported as a FREE-memory over-commit. Guard the arm with (overCPU || overMem); a disk short now falls through to the generic fail that names ephemeral-storage. Tests: disk-only shortfall is not blamed on memory; the unknown-free warn rolls up to "couldnt check free compute", not Ready. golden updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
…kend#2870)
Bugbot High. `summarizeDoctor` classifies a Node-capacity Warn as a can't-check
by PREFIX, and the `gpuRequested && !fullFits` case sits ABOVE the `!freeKnown`
branch with a detail carrying no such prefix. So with a GPU requested and the pod
list unreadable, the soft GPU Warn won, the can't-check was never emitted, and
the rollup fell through to "Ready to run training" at exit 0 -- a clean bill over
a cluster whose free compute doctor had not looked at.
Not an exotic path: the chart stamps `nvidia.com/gpu` on CPU-only installs, so
`gpuRequested` is commonly true where no node exposes a GPU, which IS this case.
The GPU fallback stays soft and stays reported; it just no longer suppresses the
stronger statement. Both facts ride in one Warn, can't-check FIRST because the
rollup matches on the prefix.
AND THE PREFIX IS NOW DEFINED ONCE. It was written out three times -- producer,
classifier, and the classifier's test -- which is a check holding its own copy of
the rule it verifies. `doctor.CantVerifyFreeCompute` is the single definition;
internal/cli already imports internal/doctor, so the classifier reads it rather
than retyping it, and the test builds its fixture from it. A producer that
rewords the phrase now moves all three together instead of leaving a test green
against a string nothing emits.
Three guards, two of them new:
- unknown free AND gpu requested -> asserts the detail STARTS with the
constant (Contains would pass on a detail that merely mentions it somewhere
and still greens the run), and that the GPU fact survives.
- gpu requested with free KNOWN -> asserts NO can't-check prefix, so the fix
cannot be read as "always warn about free".
- the classifier's can't-check table gains the COMBINED detail, which is what
a GPU-requesting install with an unreadable pod list actually produces.
Mutation-proved: deleting the new `!freeKnown` branch reddens the first test with
the pre-fix detail verbatim.
Verified: `go build ./...`, `go vet ./...` clean, `go test ./...` all packages ok.
`zz-all-strings.golden` regenerated for the two new user-facing strings --
additions only, no deletions and no orphaned literal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
…machine (backend#2870)
Bugbot Medium, and the advice was not merely vague -- it was the opposite of the
fix. `computeRemedy` ends every variant with "size runs to this machine with
`resources set max`", and `set max` measures the machine's TOTAL, which is the
figure the #2870 Fail just rejected. A user who follows the top line asks for
MORE and the training stays stuck; the accurate wording was only behind
`--verbose`.
The two Node-capacity Fails need opposite advice and were sharing one rollup arm:
- "no Ready node can fit" -> the machine is too small; sizing runs to it, or
giving it more, is right.
- "a Ready node IS large enough, but not beside what is already on it" -> the
machine is fine; asking for more is what breaks it.
So the over-commit Fail gets its own arm, keyed on a new `doctor.OverCommitted`
prefix that the producer now composes its detail FROM -- the same one-definition
treatment `CantVerifyFreeCompute` got in the previous commit, and for the same
reason: this classification is by prefix, so a reworded producer would silently
fall back to the generic arm and restore the bug.
Plain terms, no Kubernetes vocabulary, like its two neighbours --
`renderDoctorDetails` is documented as the only place that appears, and the
granular Remedy one `--verbose` away already names the knob.
Two tests, both directions, because a one-sided fix here is easy to get wrong:
- over-commit -> must NOT contain "resources set max", must warn against it.
Its fixture detail is BUILT from `doctor.OverCommitted` so arm and producer
cannot drift.
- generic too-small Fail -> must STILL offer the sizing fix, so this cannot
strip correct advice from the case it is correct for.
Mutation-proved: deleting the new arm reddens the first test with the generic
remedy verbatim ("... or size runs to this machine with `tracebloc resources set
max`.").
Verified: `go build`, `go vet` clean, `go test ./...` all packages ok.
`zz-all-strings.golden` regenerated -- three new strings, one replaced by its
constant-composed form.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
saqlainsyed007
left a comment
There was a problem hiding this comment.
Good progress — everything from my earlier passes is genuinely in: running jobs excluded from the free sum, unknown-free warns (and now, in this commit, that can't-check correctly OUTRANKS the soft GPU warn via the CantVerifyFreeCompute prefix so summarizeDoctor no longer greens it), disk-vs-memory shortfall named, and the shared OverCommitted/CantVerifyFreeCompute prefixes tidy that up nicely.
One open item, and I confirmed it against the code — Bugbot's "OK nudge still quotes allocatable max" (doctor.go:832) is real: the fit now moves to FREE, but the StatusOK path still appends the resources set max nudge computed from allocatable-minus-overhead (maxCores/maxGiB). On a node that's large enough but already partly claimed, that advertised ceiling can exceed free — so an operator who follows "size up to cpu=…,memory=…Gi" recreates exactly the over-commit this check now fails on, and the next doctor run tells them not to size to the machine. The nudge should be bounded by free (or suppressed when free < the allocatable-derived max) so the advice can't contradict the verdict. Once that's addressed it's good to go — CI is green and every other thread is resolved.
…end#2870) Bugbot Medium, confirmed by @saqlainsyed007 against the code: the fit moved to FREE and the `resources set max` nudge stayed on allocatable-minus-overhead. `bestCPU`/`bestMem` are the largest ALLOCATABLE across Ready nodes; the verdict beside them is computed from per-node free. On a node that is big enough and already partly claimed those disagree, and the nudge won -- so an operator who followed "this machine could give a run up to cpu=...,memory=...Gi" recreated the exact over-commit this check now fails on, and the next `doctor` run told them not to size to their own machine. Advice that contradicts the verdict printed beside it is worse than no advice, so the ceiling is now bounded by free: the largest FREE cpu/memory on any one Ready node is tracked alongside the allocatable-derived best, and the machine handed to `MaxRunCores`/`MaxRunGiB` is the smaller of the two. When free is UNKNOWN the nudge is suppressed entirely rather than falling back to allocatable. There is nothing to bound it with, and that arm is already a Warn saying free could not be verified -- emitting a confident ceiling from the number we just said we could not trust is the same defect one step along. Test: a 32/64Gi node with 24 cores + 40Gi held by a non-job pod. The run (2/8Gi) still fits the free 8/24Gi so the result stays OK, and the detail must not advertise the allocatable ceiling `cpu=31,memory=61Gi`. Mutation-proved: forcing the bound off reddens exactly that test and nothing else; restored, the suite is green. go build, go vet, gofmt: clean. `go test ./...`: all packages ok. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
saqlainsyed007
left a comment
There was a problem hiding this comment.
My own asks are all addressed now — the resize nudge is bounded by free and suppressed when free is unknown, exactly right, and I'm satisfied with that.
The one thing still blocking is Bugbot's new finding (doctor.go:861, "Nudge ceiling diverges from set max"), and I think it's a fair one: the nudge now prints a free-bounded ceiling, but the command it points the operator at — resources set max — sizes from allocatable via nodeLarger, so on a claimed node the number shown and the number set max would actually apply disagree, and following the parenthetical reapplies the over-commit this check now refuses. Either recompute the advertised ceiling the same way set max will (or teach set max/nodeLarger to bound by free too), or drop the resources set max pointer from the free-bounded nudge so the display and the command can't contradict. If you consider set max's allocatable sizing out of scope for this PR, say so on the Bugbot thread and we can treat it as a follow-up — either way that thread needs closing before this can land. Everything else is green and resolved.
…bers (backend#2870) Bugbot Medium on db56239, and it is the second half of my own last fix. Bounding the ceiling by free corrected the FIGURE and left the ATTRIBUTION behind: `tracebloc resources set max` sizes from allocatable via `LargestReadyNode`, so on a claimed node it applies more than the number printed beside it. An operator who read "up to cpu=7,memory=21Gi" and ran the command it named got the allocatable ceiling instead -- reapplying the over-commit this check had just started refusing. Same defect as the finding before it, one step along: the verdict and the advice beside it disagreed. `set max` is now named only when it would genuinely land on the printed numbers -- when nothing meaningful is claimed and the free-bounded ceiling equals the allocatable one. Otherwise the nudge names the explicit form, `tracebloc resources set --cores N --memory NGi`, which applies exactly what it printed. The drift signal survives on a busy node instead of being suppressed, and it stays truthful. The existing test asserted the FIGURE only, which is why a wrong command could sit beside a right number and stay green. It now also asserts that a claimed node does not name `set max` and does name the explicit form. Mutation-proved with a mutation that COMPILES -- my first attempt left `allocM` unused, and a build failure is not a caught mutation, it is an invalid one. Relaxing the equality to `<=` (always true for a free-bounded machine) makes the nudge say `set max` again and reddens exactly that assertion. `internal/cli`'s zz-all-strings golden is regenerated: the two format strings this changes, and nothing else (`git diff --numstat` = 2 added, 1 removed). go build, go vet, gofmt: clean. `go test ./...`: 18 packages ok. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
…auses (backend#2870) The previous commit fixed the FIGURE the nudge prints and left the ROLLUP still recommending the thing. summarizeDoctor consulted the Pod-health stuck-Pending arm before the over-commit arm, and that arm ends in computeRemedy -- "size runs to this machine with `resources set max`" -- which sizes from allocatable, the number the over-commit Fail just rejected. Following it raises the ask and leaves the job stuck. The two states CO-OCCUR BY CONSTRUCTION, which is what makes this a correctness bug rather than a preference about ordering: the producer's own Detail ends "so the pod schedules Pending" (internal/doctor/doctor.go:778), so an over-committed node is EXPECTED to also have pods stuck Pending. The over-commit arm was therefore close to unreachable in the field. Moved above the stuck-Pending WARN and no further: a hard Pod-health Fail is a different problem with a different fix (reinstall) and is not caused by this one, so it still wins. Both directions are asserted. The existing over-commit test could not have caught this -- it builds its fixture from allOK, so Pod health is OK and the over-commit arm wins either way. The new test constructs the state where both fire, which is the state the producer says is the normal one. Verified: go test ./... -> 18 packages ok, 0 failed; gofmt clean; go vet clean. Mutation-proved -- putting the stuck-Pending arm back on top reddens the new case with the real `set max` remedy in the message, and reddens ONLY the new case, which is the coverage gap Bugbot found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c3d8d4d. Configure here.
saqlainsyed007
left a comment
There was a problem hiding this comment.
Approving — this supersedes my earlier change-request. The Bugbot precedence finding is genuinely fixed, not papered over, and I verified the fix and its tests against the code.
The reorder is the correct fix: the over-commit Node capacity Fail arm now sits ABOVE the stuck-Pending Pod health Warn arm. That ordering is a correctness requirement, not a preference — the two states co-occur by construction (the over-commit producer's own Detail ends "so the pod schedules Pending", internal/doctor/doctor.go:778), so an over-committed node is expected to also show a stuck-Pending pod. With the old ordering the stuck-Pending arm matched first and printed computeRemedy → resources set max, putting the exact remedy this Fail exists to refuse back in front of the operator. The earlier commit fixed the figure; this fixes the rollup that was still recommending it. Only a hard Pod health Fail (pods not running at all — a different, reinstall-class problem, not caused by over-commit) still outranks it, which is right.
The two added tests pin both directions and are mutation-proof:
- "over-commit outranks stuck-Pending, which it CAUSES" sets BOTH Node capacity=Fail(OverCommitted) and Pod health=Warn(stuck Pending) and asserts the remedy has no
resources set max, carries the over-commit top line ("already claimed the room"), and the "Do NOT" wording — so a regression to the old ordering reddens it. - "a hard Pod-health Fail still outranks over-commit" is the bound: it proves the reorder didn't slide up past a harder failure.
Both build their details from the producer's owndoctor.OverCommittedconstant rather than retyping, so a reworded producer reddens them instead of silently falling through.
CI green (28 pass), no open threads.
|
/fr-pass Functional review on staging — passed. Tier-A journey against staging (this hop deployed), run 33724330593: install → cluster-anchor → ingest (15/16 task types) → use case → train → leaderboard ( Two caveats, stated rather than glossed:
|
…ot enough compute" (backend#2870) Bugbot High on #639: the transient shortage's own symptom is a second training pod Pending until the running job frees the room, and the stuck-Pending Fail arm matched it first -- "Not ready ... not enough free compute" with `resources set max` at exit 2, in exactly the case the HeldByRunningJob Warn was written for. A combined arm now sits above the stuck-Pending arm: when Pod health reports a pod Pending past grace AND Node capacity has measured that a running job holds the room, the top line says the next training is waiting for the running one, the remedy says wait or stop the job and that resizing will not help, and -- since checkPods cannot see WHY a pod is Pending -- what to do if the wait outlives the job. Warn, not Fail: the inference "Pending so cannot schedule" is refuted by the measured cause, and a Fail would exit 2 on healthy training (the Bugbot High on #628). A Pod-health FAIL and the OverCommitted Fail still outrank it; a Pending pod with no running job keeps the generic Fail and its sizing advice. Mutation: moving the combined arm back below the stuck-Pending arm reddens the new both-signals test with Bugbot's exact output. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ady computes, and tells a permanent shortage from a transient one (backend#2870) (#639) * fix(doctor): the node-capacity check compares the free memory it already computes, and tells a permanent shortage from a transient one (backend#2870) Node capacity: batch-Job pods (the `job-name` label the CLI already uses to find a training pod) go into their own per-node sum instead of being dropped. The steady-state fit still decides the PERMANENT Fail (OverCommitted, now with the node's free figures); a node that fits beside the platform but not beside its running Jobs is the new TRANSIENT Warn (HeldByRunningJob) with the opposite remedy — wait or stop the job, do not resize. The rollup gets a matching arm so the top line says the next run waits instead of a green. Machine capacity: requestedMemory returns its read error instead of 0, so an unreadable pod list renders "unrequested: unknown" and StatusUnknown rather than the whole VM as free under a check-mark. The over-commit Warn, which needs no pod list, still fires. VERSION 0.10.24 (v0.10.23 is tagged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(doctor): a Pending pod beside a running job names the job, not "not enough compute" (backend#2870) Bugbot High on #639: the transient shortage's own symptom is a second training pod Pending until the running job frees the room, and the stuck-Pending Fail arm matched it first -- "Not ready ... not enough free compute" with `resources set max` at exit 2, in exactly the case the HeldByRunningJob Warn was written for. A combined arm now sits above the stuck-Pending arm: when Pod health reports a pod Pending past grace AND Node capacity has measured that a running job holds the room, the top line says the next training is waiting for the running one, the remedy says wait or stop the job and that resizing will not help, and -- since checkPods cannot see WHY a pod is Pending -- what to do if the wait outlives the job. Warn, not Fail: the inference "Pending so cannot schedule" is refuted by the measured cause, and a Fail would exit 2 on healthy training (the Bugbot High on #628). A Pod-health FAIL and the OverCommitted Fail still outrank it; a Pending pod with no running job keeps the generic Fail and its sizing advice. Mutation: moving the combined arm back below the stuck-Pending arm reddens the new both-signals test with Bugbot's exact output. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Part of tracebloc/backend#2870 — the cli-doctor layer (DoD item 3).
The bug
checkNodeFit(the "Node capacity" check) fit the training envelope against each Ready node's Allocatable, never against what is already requested on it. So a node whose own control plane claims memory passed the check — allocatable said "a Ready node can schedule a training job" — while the pod then went Pending / Insufficient memory. The ticket's framing: no layer owned the question "is this number schedulable here?", and the one value that would expose it — free memory — was computed elsewhere and thrown away.The fix
Sum the requests already on each Ready node and fit cpu + memory against FREE (
allocatable − requested, floored at 0):requestedMemory).Verification
go test ./internal/doctor/ ./internal/cli/— green. NewTestCheckNodeFitFreeMemory: a 16Gi node whose control plane requests 12Gi (4Gi free) refuses an 8Gi envelope; the same node without the neighbour passes (the subtraction is load-bearing); a terminal neighbour is ignored.OK.gofmt/go vetclean. The copy-catalog golden was regenerated — it added only the two new over-commit strings.Scope (what remains on backend#2870)
This is one of five DoD items. Still open: the installer-side refusal (1), the footprint schedulability test tied to the #2460 overhead constant (2 — currently red-by-construction until that lands), the jobs-manager permanent-vs-transient
waiting_for_capacitymessage (4), and the CPU envelope audit (5). This PR makes the class detectable at the diagnostic layer without depending on the #2460 measurement.🤖 Generated with Claude Code
Note
Medium Risk
Changes doctor readiness verdicts, exit semantics, and remediation text; cluster-wide pod listing adds an RBAC dependency. Behavior is heavily tested but rollup ordering is subtle.
Overview
tracebloc doctornow judges whether a training job can schedule using free CPU/memory on each node (allocatable minus steady-state pod requests), not raw allocatable. That surfaces over-committed machines where the control plane already consumes the headroom and jobs stay Pending despite looking “big enough.”The Node capacity check lists pods cluster-wide (skipping terminal and batch Job pods), adds a distinct Fail with its own remedy, and warns instead of passing when the pod list is unreadable. The drift nudge caps suggested run sizes by free memory and names
resources set --cores/--memorywhenset maxwould disagree with the printed ceiling.summarizeDoctoradds a rollup arm for the over-commit Fail (explicitly not recommendingresources set max) and places it above stuck-Pending pod warnings so co-occurring symptoms get the right top-line advice. Shared prefixesOverCommittedandCantVerifyFreeComputekeep classification in sync. Version 0.10.22; tests and copy golden updated.Reviewed by Cursor Bugbot for commit c3d8d4d. Bugbot is set up for automated code reviews on this repo. Configure here.