design: harbormaster v0.1 architecture + state-machine sketch #1
Labels
No labels
epic/in-progress
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/harbormaster#1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What harbormaster is
A Go daemon that arbitrates VRAM on a single GPU host between two coexisting workloads — an LLM served by Ollama, and an image generator served by ComfyUI — without forcing the operator to choose one or the other at config time. Both workloads stay continuously available (HTTP daemons up); only their VRAM residency is gated by harbormaster's state machine.
The motivating problem: a single 24 GiB consumer GPU can't host both a useful LLM (14–22 GiB) and a useful image generation model (Flux at ~17 GiB) simultaneously. Existing tooling (Ollama, ComfyUI, LiteLLM, gpustack, Triton) targets adjacent but different problems. There is no published solution for hybrid LLM-and-image-gen VRAM arbitration with hysteresis-based switching on a single host.
The target deployment is a single GPU host on a small home server, where one operator (or a small group of agent processes) sends requests to both workloads sporadically, and round-robin or static partitioning would produce a worse experience than intelligent VRAM swapping.
Design anchors
These are the load-bearing observations from Caymans Admin's investigation 2026-06-19. Each one rules out a class of designs and points at the right shape.
1. ComfyUI lazy-loads its weights
ComfyUI's systemd service stays at ~386 MiB VRAM when its HTTP daemon is up but no inference job is running. The 17 GiB number only materializes when a KSampler node executes a workflow. Implication: harbormaster does not need to manage ComfyUI's process lifecycle (start/stop the systemd service). It just needs to gate when inference requests are executed. The ComfyUI daemon stays up always; harbormaster either lets a queued workflow run (paying the load-cost on entry) or holds it until the LLM is evicted.
This is the single biggest design simplification. An earlier sketch assumed
systemctl --user start/stop comfyuias the heavy switch action; that's wrong. The right shape is a request-queue-with-policy in front of ComfyUI's API, not a process supervisor.2. Ollama's
keep_aliveknob is the LLM-side switchOllama supports a per-request
keep_aliveparameter that overrides the system-wideOLLAMA_KEEP_ALIVEdefault. Harbormaster uses this to control when the LLM stays VRAM-resident:keep_alive: -1(or a long duration) — model stays hot in VRAM between requests.keep_alive: 0— model unloads immediately after the response. VRAM is freed for ComfyUI's queue.prompt: ""warmup request withkeep_alive: -1to repopulate VRAM before the user's next visible turn lands.3. State machine with policy-as-data
The demotion rule ("swap LLM out for image gen when …") will accumulate dimensions over time: queue depth threshold, max-wait-time threshold, time-of-day overrides, per-caller priority hints, batch-job vs interactive-job distinctions. Bake the state machine kernel into Go; load the policy from a config file (TOML or YAML). Don't hardcode thresholds as Go constants.
Proposed state set for v0.1:
Transitions are policy-driven (queue thresholds, wait timeouts, grace deadlines), not hardcoded. State transitions are atomic from the API caller's perspective — a chat request landing during
LLM_LOADED_SWAPPING_TO_IMAGEqueues with an estimated wait time rather than racing.4. Grace-period extends on each new image request
Iterative image work — tweaking prompts, trying alternatives — generates request bursts, not isolated calls. If the grace period was fixed-duration-from-last-swap, the user would face a full cold reload on every prompt iteration. Instead: the grace deadline resets on each new image request received while in
IMAGE_LOADED_GRACE. A user doing 10 minutes of prompt iteration pays the cold-reload cost once at session start, not 10 times.The operator's framing on this is verbatim: "Generated images frequently need to be tweaked around for a bit, or other ideas need to be tried out, and it would be cumbersome to wait a full hour each time." The grace-resets-on-activity shape is what makes the UX bearable.
5. Don't cancel mid-stream chat for image demotion
If a streamed chat completion is mid-response when an image request crosses the demotion threshold, let the chat finish. Cancellation introduces visible mid-response truncation which is the worst possible UX, and the chat completion is bounded (~20s at gpt-oss:20b's 170 tok/s for a long response). Image requests wait their fair share.
v0.1 HTTP API shape (sketch)
OpenAI-compatible chat endpoint plus a ComfyUI-shaped image endpoint, both terminating at harbormaster which proxies to the actual workload:
The
/v1/stateendpoint is for harbormaster's own observability — a Grafana panel can poll it. Not strictly required for v0.1 but cheap to include.OpenAI-compatibility on the chat side means aichat (or any other OpenAI-compatible client) can point at harbormaster's URL transparently. The Cabin Boy chamber's aichat config switches from
http://caymans:11434/v1/...(direct Ollama) tohttp://caymans:<hmport>/v1/...(harbormaster) once harbormaster is live, with no client-side changes beyond the URL.Config schema sketch (TOML)
Thresholds are conservative defaults intended for a single-user homelab. The operator-stakes-tuning loop is just "edit toml, restart harbormaster."
Open questions for v0.1
Not blockers — design questions to settle while building, or to defer to v0.2:
priorityfield on incoming requests so a low-priority batch job can be reordered behind interactive traffic? Probably no for v0.1; add when we have an actual second caller class.Naming history
The project name
harbormasterwas settled 2026-06-19 from Caymans Admin's proposed shortlist (gpud,gpuswap,gpubroker,holster,harbormaster). Reasons it won: (a) naval-coherent with the rest of the alcatraz crew (Bosun, Pilot, Carpenter, Surveyor, Lookout, Shipwright, Quartermaster, Herald, Cabin Boy — and now Harbormaster); (b) semantically right — the harbormaster controls who uses the harbor (the GPU); (c)quartermasteris taken; (d) reads cleanly at the CLI (harbormaster --config foo.toml).Substrate references
ollamapool from the chamber-side is the no-throttle case that #580's design needs to model.What this issue is for
This is the design substrate-of-record for harbormaster's v0.1. As decisions are made, append-amend below (or in linked PRs / issues). When v0.1.0 ships, this issue closes with a synthesis link to the actual code+docs that implement what's above.
— Bosun (filing 2026-06-19)