One owner for the card, not two neighbours (V-486)

CW2 is a ROCm process, so it registers on the KFD like any contender. Running
it as its own systemd unit made mavgpud yield llama-server to it every few
seconds. The gemma-4-12b arm was down for eight minutes on 2026-08-09 and
routing had silently fallen back to the resident model.

So mavgpud takes an `stt` block and runs the transcriber itself. `foreign` now
excludes every child rather than one pid, which is the fix. Yielding is all or
nothing, because a job that wants the card wants all of it. Idle unloading
stays llama-server's alone: CW2 holds 1.6GB and unloading it would only send
the next voice turn to the homesrv floor.

Maven still talks to the transcriber directly on 8081. There is no proxy,
because with no idle timer there is nothing for one to measure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
This commit is contained in:
2026-08-09 01:07:46 +04:00
parent 4b1edb0617
commit b975716759
5 changed files with 168 additions and 37 deletions
+17 -13
View File
@@ -10,14 +10,18 @@ import (
"time"
)
// runner owns one llama-server process. Owning it is the point of the daemon:
// the workstation cannot keep a 7-14B resident, because that holds 16GB against
// runner owns one GPU process. Owning it is the point of the daemon: the
// workstation cannot keep a 7-14B resident, because that holds 16GB against
// the owner's CPT runs, Correx and the manga-recap pipeline. So the thing that
// stays up is this, which costs no VRAM, and the model comes and goes under it.
//
// There are two of them since 09-08-2026: llama-server and the CW2 transcriber.
// name is what the log calls this one.
type runner struct {
name string
bin string
args []string
// ready is llama-server's own /health, which answers "is a model loaded".
// ready is the child's own /health, which answers "is a model loaded".
// Loading a 7-14B takes tens of seconds, so started is not ready.
readyURL string
@@ -32,9 +36,9 @@ type runner struct {
http *http.Client
}
func newRunner(bin string, args []string, readyURL string) *runner {
func newRunner(name, bin string, args []string, readyURL string) *runner {
return &runner{
bin: bin, args: args, readyURL: readyURL,
name: name, bin: bin, args: args, readyURL: readyURL,
http: &http.Client{Timeout: 2 * time.Second},
}
}
@@ -60,7 +64,7 @@ func (r *runner) isReady() bool {
return r.ready
}
// start launches llama-server. It returns as soon as the process exists, not
// start launches the child. It returns as soon as the process exists, not
// when the model is loaded.
func (r *runner) start() error {
r.mu.Lock()
@@ -76,7 +80,7 @@ func (r *runner) start() error {
return err
}
r.cmd, r.ready, r.yielding = cmd, false, false
log.Printf("mavgpud: started llama-server pid=%d", cmd.Process.Pid)
log.Printf("mavgpud: started %s pid=%d", r.name, cmd.Process.Pid)
go func() {
err := cmd.Wait()
r.mu.Lock()
@@ -84,15 +88,15 @@ func (r *runner) start() error {
r.cmd, r.ready, r.yielding = nil, false, false
r.mu.Unlock()
if yielded {
log.Printf("mavgpud: llama-server stopped, card yielded (%v)", err)
log.Printf("mavgpud: %s stopped, card yielded (%v)", r.name, err)
return
}
log.Printf("mavgpud: llama-server exited: %v", err)
log.Printf("mavgpud: %s exited: %v", r.name, err)
}()
return nil
}
// stop ends llama-server and waits for the VRAM to come back. SIGTERM first so
// stop ends the child and waits for the VRAM to come back. SIGTERM first so
// it unmaps cleanly, SIGKILL after the grace window. Returning before the
// process is gone would let the supervisor report a free card while 14GB is
// still mapped, which is the one lie that would make yielding useless.
@@ -117,11 +121,11 @@ func (r *runner) stop(grace time.Duration) {
}
time.Sleep(100 * time.Millisecond)
}
log.Printf("mavgpud: llama-server did not exit in %s, killing", grace)
log.Printf("mavgpud: %s did not exit in %s, killing", r.name, grace)
_ = syscall.Kill(pgid, syscall.SIGKILL)
}
// refreshReady asks llama-server whether the model is loaded. Called once per
// refreshReady asks the child whether the model is loaded. Called once per
// supervisor tick, never per request.
func (r *runner) refreshReady(ctx context.Context) {
if !r.running() {
@@ -141,6 +145,6 @@ func (r *runner) refreshReady(ctx context.Context) {
r.ready = ok
r.mu.Unlock()
if ok && !was {
log.Printf("mavgpud: model ready")
log.Printf("mavgpud: %s ready", r.name)
}
}