internal/autogen/audiocppdev.go
Rama main · réplica de lectura del repositorio original.
package autogen
// Which adapter an audio.cpp model loads onto.
//
// audiocpp_server takes `--backend <cuda|hip|vulkan|metal|cpu> --device <index>`
// and, left alone, takes device 0 of whatever backend it was built for. That is
// wrong on any box where the integrated GPU enumerates first, which is the
// normal case for a Vulkan build on a laptop or on an AMD desktop whose CPU has
// graphics: the model loads onto shared system memory and runs at a fraction of
// the speed, with nothing in the log saying so. audio.cpp v0.8.0 publishes no
// ROCm build, so on AMD the Vulkan listing IS the device list and this is the
// only lever.
//
// The listing has its own shape, which is why none of backenddev.go's parser is
// reused:
//
// available_devices=3
// Vulkan:0 "AMD Radeon RX 7900 XTX" [GPU]
// Vulkan:1 "AMD Radeon(TM) Graphics" [IGPU]
// CPU:0 "..." [CPU]
// select with: --backend <cuda|hip|vulkan|metal|cpu> --device <index>
//
// Two properties of that shape do the work here. It carries an explicit KIND
// tag, so a discrete GPU is read off the binary's own judgement rather than
// guessed from the name (a heuristic that would have to know every iGPU marketing
// string). And it is GLOBAL - one run lists every backend - so the probe is
// memoized per exe and filtered by flavour afterwards, costing one 0.15s run per
// generate no matter how many audio models are discovered.
//
// The refusal rule from backenddev.go carries over unchanged: a listing that
// cannot be parsed, a binary that will not run, or a backend with no discrete
// GPU in it yields NO flag, leaving audio.cpp's own default in place. Emitting a
// guessed index would be worse than emitting nothing, because a wrong --device
// is a hard launch failure where a missing one is merely the old behaviour.
import (
"context"
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
// audioCppDevice is one row of audiocpp_server --list-devices.
type audioCppDevice struct {
Backend string // "Vulkan", "CUDA", "CPU", ... as printed
Index int // the number --device takes
Name string
Kind string // "GPU" | "IGPU" | "CPU", upstream's own tag
}
// Vulkan:0 "AMD Radeon RX 7900 XTX" [GPU]
var audioCppDeviceRe = regexp.MustCompile(`^\s*([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(\d+)\s+"([^"]*)"\s*\[([A-Za-z]+)\]`)
func parseAudioCppDevices(out string) []audioCppDevice {
var devs []audioCppDevice
for _, line := range strings.Split(out, "\n") {
m := audioCppDeviceRe.FindStringSubmatch(strings.TrimRight(line, "\r"))
if m == nil {
continue
}
idx, err := strconv.Atoi(m[2])
if err != nil {
continue
}
devs = append(devs, audioCppDevice{
Backend: m[1],
Index: idx,
Name: strings.TrimSpace(m[3]),
Kind: strings.ToUpper(m[4]),
})
}
return devs
}
var (
audioCppDevMu sync.Mutex
audioCppDevCache = map[string][]audioCppDevice{}
)
// audioCppListDevices runs `exe --list-devices` once per binary. Memoized on the
// same exe|size|mtime key ListBackendDevices uses (so a backend UPDATE re-probes)
// and drawing on the same shared, never-refilled probe budget, so an audio
// backend that hangs cannot spend a generate's whole allowance on its own.
// Failures are cached: they are a property of the binary, not of the call.
func audioCppListDevices(exe string) []audioCppDevice {
if strings.TrimSpace(exe) == "" {
return nil
}
key := exe
if st, err := os.Stat(exe); err == nil {
key = fmt.Sprintf("%s|%d|%d", exe, st.Size(), st.ModTime().UnixNano())
}
audioCppDevMu.Lock()
cached, hit := audioCppDevCache[key]
audioCppDevMu.Unlock()
if hit {
return cached
}
devs := probeAudioCppDevices(exe)
audioCppDevMu.Lock()
audioCppDevCache[key] = devs
audioCppDevMu.Unlock()
return devs
}
func probeAudioCppDevices(exe string) []audioCppDevice {
budget := takeProbeBudget()
if budget <= 0 {
return nil
}
started := time.Now()
defer func() { spendProbeBudget(time.Since(started)) }()
ctx, cancel := context.WithTimeout(context.Background(), budget)
defer cancel()
cmd := exec.CommandContext(ctx, exe, "--list-devices")
hideConsole(cmd)
cmd.Env = probeEnv()
// The listing and the ggml banner land on different streams, and which is
// which has moved between releases. Read the pair.
out, err := cmd.CombinedOutput()
if err != nil && len(out) == 0 {
return nil
}
return parseAudioCppDevices(string(out))
}
// audioCppDeviceBackendMatches reports whether a listing row belongs to the
// --backend value we are about to emit. hip and rocm are the same backend under
// two names; audio.cpp accepts "hip" and has printed both.
func audioCppDeviceBackendMatches(row, flavour string) bool {
r, f := strings.ToLower(row), strings.ToLower(flavour)
if r == f {
return true
}
isHip := func(s string) bool { return s == "hip" || s == "rocm" }
return isHip(r) && isHip(f)
}
// audioCppDeviceIndex picks the --device index for a flavour, or reports false
// when the flag should be left off.
//
// Off in four cases, each for its own reason:
// - flavour "" (a hand-entered exe with no recorded variant): we do not know
// which backend the index would be numbered within.
// - flavour "cpu": there is nothing to place.
// - the backend lists ONE device: there is no choice to make, and a flag that
// cannot change the outcome is just another thing that can be wrong.
// - no row tagged [GPU]: the only devices are integrated or the probe failed.
// Refuse rather than fall back to index 0, which is what we were trying to
// avoid naming in the first place.
func audioCppDeviceIndex(exe, flavour string) (int, bool) {
if f := strings.ToLower(strings.TrimSpace(flavour)); f == "" || f == "cpu" {
// Checked before the probe, not inside pickAudioCppDevice: there is no
// point paying for a listing whose answer we already know.
return 0, false
}
return pickAudioCppDevice(audioCppListDevices(exe), flavour)
}
// pickAudioCppDevice is the choice itself, split out so it can be tested against
// a recorded listing without running a binary.
func pickAudioCppDevice(devs []audioCppDevice, flavour string) (int, bool) {
flavour = strings.ToLower(strings.TrimSpace(flavour))
if flavour == "" || flavour == "cpu" {
return 0, false
}
var inBackend []audioCppDevice
for _, d := range devs {
if audioCppDeviceBackendMatches(d.Backend, flavour) {
inBackend = append(inBackend, d)
}
}
if len(inBackend) < 2 {
return 0, false
}
for _, d := range inBackend {
if d.Kind == "GPU" {
return d.Index, true
}
}
return 0, false
}
// audioCppDeviceArg is what the emitter calls: the override wins over the probe
// and skips it entirely, so pinning a device by hand also costs nothing. A
// negative override means "emit nothing", which is how the editor's knob is
// turned back off without having to know what the probe would have said.
func audioCppDeviceArg(exe, flavour string, ov *Override) (string, bool) {
if ov != nil && ov.AudioDevice != nil {
if *ov.AudioDevice < 0 {
return "", false
}
return fmt.Sprintf("--device %d", *ov.AudioDevice), true
}
if idx, ok := audioCppDeviceIndex(exe, flavour); ok {
return fmt.Sprintf("--device %d", idx), true
}
return "", false
}