Repository navigation
feat(broker): opt-in round-robin turns (max turn, rotation, preemption) and gateway reply timeout - #276
feat(broker): opt-in round-robin turns (max turn, rotation, preemption) and gateway reply timeout#276chapi456 wants to merge 2 commits into
Conversation
Add a [turns] table, off by default: - max_turn / max_turn_by: a holder past its maximum turn stops outranking a waiting lower priority (holds and pinned leases excepted). - cycle: waiters of one priority are served in a configured order, starting after the last engine granted. - preempt / preempt_gap: a strictly higher priority may drain a holder inside its min_turn, at most once per gap (default 45s). Refs T-Crypt#275 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…then 504 With [gateway] reply_timeout set, an engine that takes a chat request and sends no response headers in that time gets it once more (started again if it went away); a second silence answers 504 with the code no_reply instead of leaving the client hanging. Off by default. The changelog covers both this and the turns commit. Refs T-Crypt#275 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks for the PR, and for writing up #275 so carefully. The numbers in it made the bug easy to trace. The code is clean and the tests are good. On my machine this branch passes build, gofmt, vet, the Windows build and vet, I'm not going to merge it as it stands. I think the bug you hit has a smaller fix that would help you more, so here is my reasoning. What I think happened in point 1. I worked this out from the code, not from a repro. A resume runs on its own goroutine (runner.go:351). The fix is to make a drain wait for an in-flight resume before it parks. That is a few lines in Why reply_timeout doesn't fit. It answers the hang, but not the reason the request hung. It also relies on Why the turns policy doesn't fit. Each part changes how priority works for everyone who turns it on, and #275 asked for none of them. Preemption cuts If you want to take the resume fix, it's yours, and I'll review it as soon as it's up. Please open it against Points 2 and 3 stay open on #275. |
Refs #275
What
Two opt-in additions for a card shared by engines that each want it for a while (for example an LLM server and an image generator). Every setting is off by default; without configuration Berth decides exactly as before.
1.
[turns]: round-robin turns (broker)max_turn, a holder that has had the card that long stops outranking a waiting lower priority, which drains it at the next boundary. Holds and pinned leases are never affected.cycle, the engine after the last one granted goes first (llm,image,llm,image, ...). Unlisted engines go after the listed ones, by arrival.min_turnkeeps every waiter out, however urgent. Withpreempt = true, a waiter of a strictly higher priority may drain a holder inside itsmin_turn, at most once perpreempt_gap. Each such cut makes the engine it empties reload from cold (about two minutes of idle GPU in my case), so the gap bounds the cost. Inside the gap the waiter is told when the next preemption is allowed. Equal or lower priorities still wait out the turn.None of it cuts a request off: a drain still starts at the slot or job boundary and the driver must still show the memory free before the next grant.
2.
[gateway] reply_timeoutAn engine that takes a chat request and sends no response headers within
reply_timeoutgets the request once more (started again if it went away). A second silence answers504with codeno_reply(an SSE event for streaming requests) instead of leaving the client hanging. This is the "pending request never gets a response" half of #275 point 1; the 409 on/v1/unloadand the latch themselves are not changed here.Diagrams
stateDiagram-v2 [*] --> Idle Idle --> Turn: grant state Turn { [*] --> Protected Protected --> Open: min_turn ends Open --> Expired: max_turn ends } note right of Turn Protected: equal or lower priority waits; higher priority preempts if allowed, once per preempt_gap Open: equal priority drains the holder, lower priority waits Expired: any waiting lower priority may drain the holder end note Turn --> Draining: a waiter may take the card Draining --> Idle: the driver shows the memory free Idle --> Turn: next waiter (cycle picks among equal priorities)sequenceDiagram participant A as LLM (interactive) participant B as image generator (batch) participant C as person (foreground) A->>A: granted, turn starts B-->>A: waits (lower priority, held) Note over A: max_turn ends A->>B: drained at the next boundary B->>B: granted, min_turn 45 s protects it C-->>B: asks for the card at 20 s Note over B: preempt = true: the turn is cut short B->>C: drained C->>C: granted Note over C: another foreground request within 45 s of this preemption waits for the gapWhere
internal/broker/turns.go(policy,turnExpired,preempts,cycleRank), small hooks inevaluate,schedule,grant,sortQueueinternal/config([turns],[gateway] reply_timeout, validation),internal/cli(wiring, a line inberth config showwhen something is on)internal/gateway/gateway.go(proxyTo, one replay, 504)docs/leasing.md(new section with the diagrams),docs/gateway.md,mkdocs.yml(Mermaid fence),CHANGELOG.mdTests
internal/broker/turns_test.go(no policy keeps a higher-priority holder, max turn, per-engine max turn, holds exempt, cycle vs arrival, preemption off / on / equal priority / once per gap),internal/config(defaults off, parsing, validation),internal/gateway/replytimeout_test.go(silent engine reported and replayed, plain proxy unchanged, 504 JSON and SSE).Run on Windows with Go 1.27.1:
go build ./... && gofmt -l . && go vet ./... && go test ./...is green when the machine is not loaded;GOOS=windowsbuild and vet are green. Three APU tests,TestUpdateCheckReportsNewerandTestInsightsComputedOncePerIntervalfailed once each under heavy load during a full parallel run and pass alone and on re-run (timing-based, none touch this change). golangci-lint v2.14.0 run on Windows reports 6 findings, all in files this PR does not touch (gpu/pdh_windows.go,config/credential.go,host/host.go,pagecache/pagecache.go); CI's Linux run is the reference.Not done
max_turnonberth runand the engine entry (the keyed[turns.max_turn_by]map covers it).keep_comfyui_upemptying ComfyUI's models (v0.4.0: park race (409 on /v1/unload) latches the LLM, ComfyUI emptied on short LLM calls, stop on LATCHED #275 points 1 and 3 are only addressed for the hanging request; point 2 is untouched).🤖 Generated with Claude Code