Reads the same PDH counters as Task Manager (\GPU Engine(*)\Utilization Percentage, locale-independent via PdhAddEnglishCounterW), which cover graphics work under WDDM — games are caught without an exe list and the offender is named. Windows-only; a missing or failing counter disables the path with one log line. dwm (the desktop compositor) joins the default ignore list.
195 lines
9.2 KiB
Go
195 lines
9.2 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// sampleEntry is one line pair in the sample env file: a comment and the
|
|
// (usually commented-out) KEY=VALUE line.
|
|
type sampleEntry struct {
|
|
name string
|
|
value string
|
|
comment string
|
|
active bool // rendered uncommented
|
|
}
|
|
|
|
// appVerComment documents APP_VER wherever it is rendered.
|
|
const appVerComment = `Version to run: "dev" disables updates, "stable" tracks the latest release, or pin an exact release like v0.1.7`
|
|
|
|
// sampleEntries lists every setting in sample order. logFile activates the
|
|
// LOG_FILE line (Windows install); empty keeps it commented like the rest.
|
|
// CFG_VER and APP_VER are not entries — they head the file, always active.
|
|
func sampleEntries(logFile string) []sampleEntry {
|
|
return []sampleEntry{
|
|
{"LISTEN_OLLAMA", ":11434", "Listen address for Ollama-compatible clients (gpu-turnstile poses as Ollama here)", false},
|
|
{"LISTEN_COMFY", ":8188", "Listen address for ComfyUI clients (gpu-turnstile poses as ComfyUI here)", false},
|
|
{"OLLAMA_URL", "http://127.0.0.1:11434", "Ollama upstream URL; setting it enables the Ollama consumer (default: empty = disabled)", false},
|
|
{"COMFY_URL", "http://127.0.0.1:8188", "ComfyUI upstream URL; setting it enables the ComfyUI consumer (default: empty = disabled)", false},
|
|
{"WARM_MODEL", "", "Optional model to reload after an image job (default: empty = none)", false},
|
|
{"COMFY_CMD", `"C:\ComfyUI\.venv\Scripts\python.exe" ComfyUI\main.py --port 8188`, "Spawn and supervise ComfyUI on demand: the first request starts it, it stops after COMFY_IDLE_TIMEOUT to free VRAM (default: derived from COMFY_DIR; both empty = unmanaged)", false},
|
|
{"COMFY_DIR", `C:\ComfyUI`, "Root of a standard ComfyUI venv install (.venv + main.py): setting it alone supervises ComfyUI with the derived launch command; also the working directory for COMFY_CMD", false},
|
|
{"COMFY_IDLE_TIMEOUT", "5m", "Stop the managed ComfyUI after this long without requests or jobs (frees VRAM)", false},
|
|
{"COMFY_START_TIMEOUT", "2m", "How long a request waits for the managed ComfyUI to come up", false},
|
|
{"GAME_PROCS", "cyberpunk2077.exe,hl2.exe", "While a listed process runs, the GPU counts as held by it: requests wait, Ollama unloads, managed ComfyUI stops (default: empty = disabled)", false},
|
|
{"GPU_FOREIGN_VRAM_MB", "1024", "Also treat the GPU as held when a process not in GPU_IGNORE_PROCS uses more VRAM than this (needs nvidia-smi; 0/empty = disabled)", false},
|
|
{"GPU_FOREIGN_UTIL_PCT", "30", "Also treat the GPU as held when a process not in GPU_IGNORE_PROCS uses more than this percent of the GPU 3D engine (Windows Task-Manager counters; catches games without an exe list; 0/empty = disabled)", false},
|
|
{"GPU_IGNORE_PROCS", "ollama,ollama app,ollama_llama_server,python,pythonw,dwm", "Process names never counted as foreign GPU users (ComfyUI runs under python; dwm is the desktop compositor)", false},
|
|
{"GAME_POLL_INTERVAL", "15s", "How often game/VRAM detection runs (nvidia-smi polls keep the GPU awake; don't go below ~10s)", false},
|
|
{"UNLOAD_TIMEOUT", "60s", "How long to wait for Ollama to unload a model", false},
|
|
{"JOB_TIMEOUT", "15m", "Maximum time to wait for a ComfyUI job", false},
|
|
{"LLM_WAIT_TIMEOUT", "10m", "Max time an LLM request waits for the GPU before being answered 503 (wait mode)", false},
|
|
{"LLM_BUSY_MODE", "wait", `How blocked LLM requests are handled: "wait" holds them, "reject" fails them immediately`, false},
|
|
{"LLM_BUSY_STATUS", "503", "HTTP status for rejected LLM requests in reject mode (400-599, e.g. 429)", false},
|
|
{"BUSY_RETRY_AFTER", "30", "Seconds sent as Retry-After on busy responses (both modes)", false},
|
|
{"LOGLEVEL", "warn", `Log verbosity: debug, info, warn, error; "info" logs every request (LOG_LEVEL works too)`, false},
|
|
{"LOG_FORMAT", "text", `Log format: "text" or "json"`, false},
|
|
{"LOG_FILE", logFile, "Append logs to this file instead of stderr (a Windows service has no console)", logFile != ""},
|
|
{"UNLOAD_POLL_INTERVAL", "500ms", "/api/ps poll interval while unloading", false},
|
|
{"HISTORY_POLL_INTERVAL", "1s", "/history/<id> poll interval while a job runs", false},
|
|
{"PROBE_TIMEOUT", "5s", "Probe timeout for the startup probe and the periodic health check", false},
|
|
{"HEALTH_INTERVAL", "30s", "How often enabled upstreams are probed; status changes are logged", false},
|
|
{"FREE_TIMEOUT", "30s", "Timeout for the POST /free call after an image job", false},
|
|
{"WARM_TIMEOUT", "2m", "Timeout for the warm-model reload after an image job", false},
|
|
{"SHUTDOWN_TIMEOUT", "10s", "Graceful shutdown timeout on SIGINT/SIGTERM", false},
|
|
{"BACKOFF_INITIAL", "1s", "First retry wait when an upstream connection fails", false},
|
|
{"BACKOFF_MAX", "60s", "Cap for the exponential retry backoff", false},
|
|
{"PROMPT_CAPTURE_LIMIT", "65536", "Bytes of the /prompt response buffered to find prompt_id (pass-through unaffected)", false},
|
|
{"AUTO_UPDATE", "true", "Poll the releases API for signed updates", false},
|
|
{"UPDATE_INTERVAL", "6h", "Auto-update check interval", false},
|
|
{"UPDATE_REPO", "https://git.rambossek.at/PUBLIC/gpu-turnstile", "Repository to check for releases", false},
|
|
{"UPDATE_ASSET", "gpu-turnstile.exe", "Release asset to download", false},
|
|
}
|
|
}
|
|
|
|
// SampleEnv renders a sample .env file covering every setting, each with a
|
|
// comment line. Everything is commented out — so all defaults apply —
|
|
// except the CFG_VER/APP_VER header and LOG_FILE when logFile is non-empty
|
|
// (a Windows service has no console). CFG_VER records the version that
|
|
// wrote the file so installs — and startups after an update — can upgrade
|
|
// it.
|
|
func SampleEnv(version, logFile string) string {
|
|
// CFG_VER is always a concrete vX.Y.Z — never "dev". A dev build
|
|
// stamps v0.0.0, which sorts older than any release, so the next
|
|
// release install upgrades the file and stamps a proper version.
|
|
cfgVer := version
|
|
if cfgVer == "" || cfgVer == "dev" {
|
|
cfgVer = "v0.0.0"
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "CFG_VER=%s\n", cfgVer)
|
|
b.WriteString("# Config format reference, written by the installer — do not edit.\n")
|
|
b.WriteString("# The installer uses it to append newly added settings on updates.\n\n")
|
|
b.WriteString("# gpu-turnstile configuration\n")
|
|
b.WriteString("# KEY=VALUE lines; \"#\" starts a comment. Every setting below is at its\n")
|
|
b.WriteString("# default and commented out — remove the \"#\" to change it.\n")
|
|
b.WriteString("# At least one of OLLAMA_URL / COMFY_URL must be set for the proxy to start.\n\n")
|
|
writeEntry(&b, sampleEntry{"APP_VER", "stable", appVerComment, true})
|
|
for _, e := range sampleEntries(logFile) {
|
|
writeEntry(&b, e)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func writeEntry(b *strings.Builder, e sampleEntry) {
|
|
fmt.Fprintf(b, "# %s\n", e.comment)
|
|
if e.active {
|
|
fmt.Fprintf(b, "%s=%s\n\n", e.name, e.value)
|
|
} else {
|
|
fmt.Fprintf(b, "#%s=%s\n\n", e.name, e.value)
|
|
}
|
|
}
|
|
|
|
// SyncSample upgrades an installer-written env file: when its CFG_VER says
|
|
// it was written by an older gpu-turnstile, every setting the file does not
|
|
// mention — commented or not — is appended at the end, and CFG_VER is
|
|
// updated to version. Files without CFG_VER (hand-written or foreign),
|
|
// up-to-date files and "dev" builds are returned unchanged; changed reports
|
|
// whether the returned content differs.
|
|
func SyncSample(data, version, logFile string) (string, bool) {
|
|
if version == "" || version == "dev" {
|
|
return data, false
|
|
}
|
|
values, err := ParseEnvFile(strings.NewReader(data))
|
|
if err != nil || values["CFG_VER"] == "" {
|
|
return data, false // not installer-written; the caller decides
|
|
}
|
|
if compareVersions(values["CFG_VER"], version) >= 0 {
|
|
return data, false // same or newer
|
|
}
|
|
|
|
present := make(map[string]bool)
|
|
for _, line := range strings.Split(data, "\n") {
|
|
line = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "#"))
|
|
if name, _, ok := strings.Cut(line, "="); ok {
|
|
present[strings.TrimSpace(name)] = true
|
|
}
|
|
}
|
|
|
|
lines := strings.Split(data, "\n")
|
|
for i, l := range lines {
|
|
if strings.HasPrefix(strings.TrimSpace(l), "CFG_VER=") {
|
|
lines[i] = "CFG_VER=" + version
|
|
break
|
|
}
|
|
}
|
|
out := strings.Join(lines, "\n")
|
|
if !strings.HasSuffix(out, "\n") {
|
|
out += "\n"
|
|
}
|
|
|
|
var b strings.Builder
|
|
if !present["APP_VER"] {
|
|
fmt.Fprintf(&b, "\n# Added by gpu-turnstile %s:\n", version)
|
|
writeEntry(&b, sampleEntry{"APP_VER", "stable", appVerComment, true})
|
|
}
|
|
for _, e := range sampleEntries(logFile) {
|
|
if !present[e.name] {
|
|
fmt.Fprintf(&b, "\n# Added by gpu-turnstile %s:\n", version)
|
|
writeEntry(&b, e)
|
|
}
|
|
}
|
|
out += b.String()
|
|
return out, out != data
|
|
}
|
|
|
|
// compareVersions orders two vX.Y.Z version strings. Unparseable versions
|
|
// (including "dev") sort before any release.
|
|
func compareVersions(a, b string) int {
|
|
pa, oka := parseVersion(a)
|
|
pb, okb := parseVersion(b)
|
|
if oka != okb {
|
|
if oka {
|
|
return 1
|
|
}
|
|
return -1
|
|
}
|
|
for i := range pa {
|
|
if pa[i] != pb[i] {
|
|
if pa[i] > pb[i] {
|
|
return 1
|
|
}
|
|
return -1
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func parseVersion(v string) ([3]int, bool) {
|
|
var out [3]int
|
|
v = strings.TrimPrefix(strings.TrimSpace(v), "v")
|
|
parts := strings.Split(v, ".")
|
|
if len(parts) != 3 {
|
|
return out, false
|
|
}
|
|
for i, p := range parts {
|
|
n, err := strconv.Atoi(p)
|
|
if err != nil || n < 0 {
|
|
return out, false
|
|
}
|
|
out[i] = n
|
|
}
|
|
return out, true
|
|
}
|