Rename to gpu-turnstile; swap port roles
The proxy now listens on the standard service ports (Ollama :11434, ComfyUI :8188) and the actual services move one port up (:11435, :8189). Metric prefix is now gpu_turnstile_.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# gpulock — GPU arbitration proxy for Ollama + ComfyUI
|
||||
# gpu-turnstile — GPU arbitration proxy for Ollama + ComfyUI
|
||||
|
||||
## Problem
|
||||
|
||||
@@ -16,8 +16,9 @@ at any moment the GPU is in exactly one of three states:
|
||||
- `llm` — N ≥ 1 Ollama inference requests in flight (concurrency allowed)
|
||||
- `image` — exactly one ComfyUI job in flight, Ollama models unloaded
|
||||
|
||||
Clients (LiteLLM, Open WebUI, n8n) point at gpulock instead of at the services.
|
||||
gpulock is transparent for everything that does not touch the GPU.
|
||||
Clients (LiteLLM, Open WebUI, n8n) point at gpu-turnstile instead of at the
|
||||
services. gpu-turnstile is transparent for everything that does not touch the
|
||||
GPU.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@@ -30,15 +31,16 @@ gpulock is transparent for everything that does not touch the GPU.
|
||||
## Architecture
|
||||
|
||||
```
|
||||
LiteLLM / Open WebUI ──► :11435 ─┐ ┌─► Ollama :11434
|
||||
├── gpulock (1 lock) ──┤
|
||||
Open WebUI / n8n ────► :8189 ───┘ └─► ComfyUI :8188
|
||||
LiteLLM / Open WebUI ──► :11434 ─┐ ┌─► Ollama :11435
|
||||
├── gpu-turnstile (1 lock) ──┤
|
||||
Open WebUI / n8n ────► :8188 ───┘ └─► ComfyUI :8189
|
||||
```
|
||||
|
||||
Two listeners, one process, one lock. Each listener is an
|
||||
`httputil.ReverseProxy` to its upstream. Websocket upgrades (ComfyUI `/ws`)
|
||||
and streaming bodies (Ollama NDJSON / SSE) must pass through unbuffered
|
||||
(`FlushInterval = -1`).
|
||||
gpu-turnstile listens on the ports the services normally use; the actual
|
||||
services run one port higher. Two listeners, one process, one lock. Each
|
||||
listener is an `httputil.ReverseProxy` to its upstream. Websocket upgrades
|
||||
(ComfyUI `/ws`) and streaming bodies (Ollama NDJSON / SSE) must pass through
|
||||
unbuffered (`FlushInterval = -1`).
|
||||
|
||||
### Lock semantics
|
||||
|
||||
@@ -58,7 +60,7 @@ are LLM requests and the single "writer" is an image job):
|
||||
|
||||
### Endpoint classification
|
||||
|
||||
Ollama listener (`:11435` → `OLLAMA_URL`):
|
||||
Ollama listener (`:11434` → `OLLAMA_URL`):
|
||||
|
||||
| Path | Handling |
|
||||
|---|---|
|
||||
@@ -66,7 +68,7 @@ Ollama listener (`:11435` → `OLLAMA_URL`):
|
||||
| `POST /v1/chat/completions`, `/v1/completions`, `/v1/embeddings` | LLM lock |
|
||||
| everything else (`/api/tags`, `/api/ps`, `/api/show`, `/api/version`, `/v1/models`, `/api/pull`, …) | pass-through, no lock |
|
||||
|
||||
ComfyUI listener (`:8189` → `COMFY_URL`):
|
||||
ComfyUI listener (`:8188` → `COMFY_URL`):
|
||||
|
||||
| Path | Handling |
|
||||
|---|---|
|
||||
@@ -100,10 +102,10 @@ load time. Off by default.
|
||||
|
||||
| Var | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `LISTEN_OLLAMA` | `:11435` | Ollama-facing listener |
|
||||
| `LISTEN_COMFY` | `:8189` | ComfyUI-facing listener |
|
||||
| `OLLAMA_URL` | `http://127.0.0.1:11434` | upstream |
|
||||
| `COMFY_URL` | `http://127.0.0.1:8188` | upstream |
|
||||
| `LISTEN_OLLAMA` | `:11434` | Ollama-facing listener |
|
||||
| `LISTEN_COMFY` | `:8188` | ComfyUI-facing listener |
|
||||
| `OLLAMA_URL` | `http://127.0.0.1:11435` | upstream |
|
||||
| `COMFY_URL` | `http://127.0.0.1:8189` | upstream |
|
||||
| `UNLOAD_TIMEOUT` | `60s` | wait for Ollama to unload |
|
||||
| `JOB_TIMEOUT` | `15m` | wait for ComfyUI job |
|
||||
| `LLM_WAIT_TIMEOUT` | `10m` | max time an LLM request waits for the lock before 503 |
|
||||
@@ -119,9 +121,9 @@ start (`/api/version`, `/system_stats`); failure is logged, not fatal.
|
||||
`{"state":"idle|llm|image","llm_inflight":N,"image_pending":B}`.
|
||||
- `GET /metrics` on the Ollama listener: Prometheus text format, no external
|
||||
dependency needed:
|
||||
`gpulock_state{state="…"} 1`, `gpulock_llm_inflight`,
|
||||
`gpulock_image_jobs_total`, `gpulock_lock_wait_seconds` (histogram, label
|
||||
`kind="llm|image"`), `gpulock_unload_seconds`.
|
||||
`gpu_turnstile_state{state="…"} 1`, `gpu_turnstile_llm_inflight`,
|
||||
`gpu_turnstile_image_jobs_total`, `gpu_turnstile_lock_wait_seconds`
|
||||
(histogram, label `kind="llm|image"`), `gpu_turnstile_unload_seconds`.
|
||||
- Structured logs (`log/slog`, JSON when `LOG_FORMAT=json`), one line per
|
||||
state transition and per image job phase with `prompt_id`.
|
||||
|
||||
@@ -143,8 +145,8 @@ start (`/api/version`, `/system_stats`); failure is logged, not fatal.
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
gpulock/
|
||||
cmd/gpulock/main.go # wiring, config, listeners
|
||||
gpu-turnstile/
|
||||
cmd/gpu-turnstile/main.go # wiring, config, listeners
|
||||
internal/lock/lock.go # two-mode lock + tests
|
||||
internal/ollama/client.go # ps / unload / warm
|
||||
internal/comfy/client.go # history poll / free
|
||||
@@ -176,30 +178,31 @@ are new.
|
||||
- Go 1.23+, stdlib only. `CGO_ENABLED=0`, `-ldflags="-s -w"`, version from
|
||||
`git describe` injected via `-X main.version=`.
|
||||
- Dockerfile: multi-stage, final image `gcr.io/distroless/static` (or
|
||||
`scratch`), non-root user, `EXPOSE 8189 11435`, `ENTRYPOINT ["/gpulock"]`.
|
||||
`scratch`), non-root user, `EXPOSE 8188 11434`,
|
||||
`ENTRYPOINT ["/gpu-turnstile"]`.
|
||||
- `.gitea/workflows/ci.yml` (Gitea Actions):
|
||||
1. on push and tag: `go vet`, `go test -race ./...`, `golangci-lint` if
|
||||
available in the runner image
|
||||
2. build image with buildx, tags `:sha-<short>` and `:latest` on main,
|
||||
`:<tag>` on tags
|
||||
3. push to the Gitea registry `git.rambossek.at/<owner>/gpulock` using the
|
||||
workflow token (`${{ secrets.GITEA_TOKEN }}` / `gitea.actor`)
|
||||
3. push to the Gitea registry `git.rambossek.at/<owner>/gpu-turnstile`
|
||||
using the workflow token (`${{ secrets.GITEA_TOKEN }}` / `gitea.actor`)
|
||||
- Release: a git tag `vX.Y.Z` produces the versioned image; the Open WebUI
|
||||
compose pins that tag.
|
||||
|
||||
## Deployment (target)
|
||||
|
||||
```yaml
|
||||
gpulock:
|
||||
image: git.rambossek.at/<owner>/gpulock:v0.1.0
|
||||
gpu-turnstile:
|
||||
image: git.rambossek.at/<owner>/gpu-turnstile:v0.1.0
|
||||
environment:
|
||||
OLLAMA_URL: http://<workstation-ip>:11434
|
||||
COMFY_URL: http://<workstation-ip>:8188
|
||||
OLLAMA_URL: http://<workstation-ip>:11435
|
||||
COMFY_URL: http://<workstation-ip>:8189
|
||||
networks: [internal]
|
||||
```
|
||||
|
||||
LiteLLM `api_base` → `http://gpulock:11435`; Open WebUI
|
||||
`COMFYUI_BASE_URL` → `http://gpulock:8189`. Nothing else talks to the
|
||||
LiteLLM `api_base` → `http://gpu-turnstile:11434`; Open WebUI
|
||||
`COMFYUI_BASE_URL` → `http://gpu-turnstile:8188`. Nothing else talks to the
|
||||
workstation directly.
|
||||
|
||||
## Open questions
|
||||
@@ -207,5 +210,5 @@ workstation directly.
|
||||
- Should embedding requests (`/api/embed`, `/v1/embeddings`) count as LLM
|
||||
traffic for the lock? They do in this spec (they hold VRAM); reconsider if
|
||||
RAG indexing starves image jobs for too long.
|
||||
- Whether to add a `POST /gpulock/release` admin endpoint to force-reset the
|
||||
lock without restarting. Cheap to add; decide once it's been stuck once.
|
||||
- Whether to add a `POST /gpu-turnstile/release` admin endpoint to force-reset
|
||||
the lock without restarting. Cheap to add; decide once it's been stuck once.
|
||||
|
||||
Reference in New Issue
Block a user