Repository navigation
Conversation
A node that accepts a connection and then produces nothing costs the caller a full first-byte deadline, and candidate selection had no memory of it: the next request ranked the same node first and spent the same deadline again. That is the 2.1x in NVIDIA#6, measured against an absent node — which is cheap, because its dial fails in a tenth of the time. The cooldown records only what was measured. An attempt that produced no content at all and reached the first-byte budget stamps that candidate, and the ordering passes put it behind its peers until the stamp lapses. It is demotion, never exclusion: a cooling node is still dispatched to once the others are exhausted, and with a single candidate the back is also the front. The trigger is measured rather than classified from the error. A dial failure is bounded by the much shorter dial timeout and a status-based retry commits a response, so only an attempt that produced nothing can reach the budget. That covers both shapes of silence — no headers at all, and headers with no body — without having to name either, and without a liveness model that would have to tell a frozen node from a cold load. NVIDIA#57 is the case that rules such a model out: Ollama legitimately sends no headers for longer than this deadline while a model loads, so no single number separates the two. The cooldown does not try to. It says the last attempt here cost a full deadline, so prefer someone else for a while. The window is one deadline rather than a constant of its own. The trigger cost exactly that much, so waiting the same span adds nothing to tune and moves with whatever an operator has set the deadline to. Two hooks, not one. resolveCandidates partitions the list, but the reserved head is picked by load rather than list position, so a scheduler-ranked node would be put straight back at the front of the round it was just demoted from; reserveCandidate therefore skips cooling candidates too, falling back to the ordinary pick when every candidate is cooling. The second hook is also what makes the demotion hold when an explicit selection is in play, since that path returns from reserveCandidate before the load pick. Both read one predicate, and both branch on the empty case, so a process that has never met a stalled node does exactly what it did before. State is process-local and in-memory. The stamp records what this proxy measured on its own path to that node, which is the only thing it can honestly claim. Nothing is shared between processes and nothing is persisted, so two proxies are free to disagree about a peer — each reflecting its own experience — and the disagreement is bounded, because the stamp lapses on time rather than waiting for a success that a quiet node may never be asked for. Refs NVIDIA#6. This is the cost half only; the stale-workload sweep that mislabels a peer's completed work is a separate mechanism. Signed-off-by: canja006 <[email protected]>
Noah-Tervalon-Nvidia
self-requested a review
October 5, 2026 15:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #6 — the cost half. The stale-workload sweep that mislabels a peer's completed work is a separate mechanism and is not touched here.
A node that accepts a connection and then produces nothing costs the caller a full first-byte deadline, and candidate selection had no memory of it: the next request ranked the same node first and spent the same deadline again. An absent node is cheap by comparison, because its dial fails in a tenth of the time.
The shape
Two hooks, not the one you expected
You said the merged proxies would leave a single insertion point. That is true in the sense you meant — this no longer has to be written twice across separate binaries — but inside the merged proxy two mechanisms decide order, and both have to honour the cooldown or it does not hold:
resolveCandidatespartitions the list, but the reserved head is picked by load rather than list position, so a scheduler-ranked node is put straight back at the front of the round it was just demoted from.reserveCandidatetherefore skips cooling candidates too, falling back to the ordinary least-loaded pick when every candidate is cooling.TestReserveCandidate_SkipsCoolingfails with only the first hook, so this is a measured claim rather than a preference. The second hook is also what makes the demotion hold when an explicit selection is in play, since that path returns fromreserveCandidatebefore the load pick.Both read one predicate and both branch on the empty case, so a process that has never met a stalled node does exactly what it did before.
TestReserveCandidate_UnchangedWithoutCoolingpins that, and the balance invariants inreservation_test.godepend on it.On the cross-node divergence you raised
Node A demoting B while C finds B healthy is the intended behaviour rather than a wrinkle to design out. The stamp records what that proxy measured on its own path to the peer, which is the only thing it can honestly claim — a direct-connect link, a congested hop or a busy queue genuinely differ per observer. Sharing the judgement would need consensus between proxies, which costs far more than the 2.1x it would be buying.
The divergence is also bounded rather than cumulative: the stamp lapses on time, so the two views reconverge without either proxy learning anything from the other, and nothing is persisted, so a restart starts from no opinion.
One decision worth your call
The demotion runs after all three ordering passes, so it also applies to an explicitly selected node. The selection is "this node first", not "this node only" — the rest of the list is still appended — so a demoted selection is still dispatched to once the healthy candidates are exhausted.
TestResolveCandidates_ExplicitSelectionIsAlsoDemoteddocuments it so the behaviour is visible rather than incidental.The argument for it: a pinned node that just cost a full deadline is exactly the case where the caller benefits from being routed around. The argument against: a pin is a user instruction, and overriding it silently is a bigger change than the issue asks for. Exempting it is a one-line move of the partition above the selection pass — say which you prefer.
Tests
cooldown_test.go, 10 cases:TestHandleHTTP_StalledNodeIsNotPaidForTwiceasserts the dispatch count, not the candidate order. A reordering that still sent the next request into the same stall would pass an order-only test while costing the caller a second full deadline, so the staller's hit count is the thing checked.gofmt,go vetandgo test -race ./...clean againstdevelop.