server: replace the VRAM doorbell with a want-lock
The doorbell was edge-triggered: a waiter wrote <arena>/doorbell/<pid> and the holder's warden turned the inotify event into a queue flag. A ring carried no notion of "still wanted", so it was lost whenever the holder was not already warm and listening - during its model load (the watch does not exist yet, and the kernel does not queue events for a watch that is not there) or inside restore_device - and any ring that survived past the point its sender had been satisfied caused a spurious release. Lost rings wedged the waiter permanently, because flock(LOCK_EX) never times out and, with idle-sleep disabled, the holder had no other reason to release. The wait blocks under mutex_tasks, so the whole server stopped answering while /health still returned 200. Express the request as kernel state instead. A waiter holds <arena>/want.lock shared while it waits and drops it once it owns the token; the sleep decision probes that lock non-blocking and releases the GPU while anyone is waiting. The probe needs its own fd - flock treats two open file descriptions of one file independently, so probing on the waiter's fd would convert our own lock rather than conflict with it. Nothing can be missed, nothing goes stale, and a waiter that dies is cleaned up by the kernel. Probe from should_sleep() on the loop thread rather than from a warden thread. Routing it through a flag is what made the first attempts fail: start_loop() holds mutex_tasks from should_sleep() through the callbacks to the wait, so a warden's request_yield() blocks on that mutex and is admitted only after the flag has been consumed, latching a release for the next wake. Reading live state where the decision is made has no edge to latch, and drops the warden's poll latency. Two sleep-path bugs this exposed: wait_until_no_sleep() waited on !sleeping but the loop clears req_stop_sleeping on the way in, so a loop that slept again before the waiter ran stranded it forever - re-ask on every wake. And a task queued after the waiter saw us awake could not wake us by itself, so sleep now also breaks on a non-empty queue. Hold the GPU for 100 ms after a wake: the request that woke us is not queued yet, and yielding at once only sends it round again. Measured on the RX 580 pod, two servers contending, 60 alternating handoffs: 0 stranded, median 0.309 s against the doorbell's 0.314 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZz44SLQvTXMyWGio6t9DZ
This commit is contained in:
@@ -18,7 +18,6 @@ private:
|
||||
bool running = false;
|
||||
bool sleeping = false;
|
||||
bool req_stop_sleeping = false;
|
||||
bool yield_requested = false; // set by request_yield() when another process wants the VRAM token
|
||||
int64_t time_last_task = 0;
|
||||
|
||||
// queues
|
||||
@@ -46,6 +45,7 @@ private:
|
||||
std::function<bool(server_task &&, bool)> callback_new_task;
|
||||
std::function<void(void)> callback_update_slots;
|
||||
std::vector<std::function<void(bool)>> callback_sleeping_state;
|
||||
std::function<bool()> should_yield_cb;
|
||||
|
||||
public:
|
||||
~server_queue() { worker_stop(); }
|
||||
@@ -70,11 +70,6 @@ public:
|
||||
// returns immediately if not sleeping
|
||||
void wait_until_no_sleep();
|
||||
|
||||
// request that the loop go to sleep (release VRAM) as soon as it is idle - called from the
|
||||
// VRAM-arbiter warden thread when another process rings the doorbell for the GPU token.
|
||||
// Thread-safe; wakes the loop so it releases promptly instead of at the next idle poll.
|
||||
void request_yield();
|
||||
|
||||
bool is_sleeping() {
|
||||
std::unique_lock<std::mutex> lock(mutex_tasks);
|
||||
return sleeping;
|
||||
@@ -133,6 +128,14 @@ public:
|
||||
callback_update_slots = std::move(callback);
|
||||
}
|
||||
|
||||
// Register a predicate asking the loop to sleep (release VRAM) as soon as it is idle - used by
|
||||
// the VRAM arbiter to check whether another process waits for the GPU token.
|
||||
// Called on the start_loop() thread while holding mutex_tasks, so it must not block or post
|
||||
// tasks. While it is set, the idle wait polls faster, so the loop reacts without a notify.
|
||||
void on_should_yield(std::function<bool()> callback) {
|
||||
should_yield_cb = std::move(callback);
|
||||
}
|
||||
|
||||
// Register callback for sleeping state change; multiple callbacks are allowed
|
||||
// for example: register order cb0, cb1, cb2
|
||||
// entering sleep: queue.sleeping = true --> cb0(true) --> cb1(true) --> cb2(true)
|
||||
|
||||
Reference in New Issue
Block a user