Manage Docker containers as engines — Berth-owned start/stop + keep-durably-out when the card is claimed
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 25/100
调研方向
Start by reading issue #96 and the existing engine configuration and lease drain/grant flow; the payload does not name source files or tests. Decide how container lifecycle commands, Compose services, restart policies, and unload-only behavior fit the engine model, then implement and test the chosen design. Done means a container can be kept stopped while another tenant holds the card and is accounted for through its engine entry.
由索引模型根据 Issue 内容生成。
描述
The problem
Berth's value comes from owning the card's admission: a grant, a drain, a claim.
But it can only govern workloads it can control — either a child process it launched
(berth run) or, since the design being queued for #96, an engine that exposes its own
load_url/unload_url.
A Docker container that allocates VRAM on its own schedule is in the blind spot. Concretely,
on my box the immich-pet-tagger container (a scheduled image-embedding service,
restart: unless-stopped) loads ~2 GB of CUDA models on every startup and on schedule (in example every ~20 min) and unloads them when the cycle ends. Berth sees that VRAM — it shows up in the stowaway list — but has no control channel to it. Two consequences, both measured:
-
The card is not gated. An unleased container will load its models the moment its own
scheduler fires, regardless of what else is using the card. With the 27 B model fully
resident (15,330 MiB used / 664 MiB free — the hardest state on this 16,303 MiB card), a
live manual trigger produced this (2 s cadence):t VRAM event 11:13:12 15,330 trigger; 27 B serving an active 58,907-token prompt 11:13:22–11:14:14 15,483 → 15,567 peak tagger's YOLO (2 s) + CLIP (45 s) load; 27 B yields ~0.75 GB 11:14:15 — STATSclean, no OOM, no errors11:14:18 11,043 (4,953 free) tagger models released; 27 B still displaced (~4.3 GB in RAM) 11:14:55 15,301, util 90–92 % 27 B re-paginated in <2 s It's self-healing — but "coexists by displacement" is not the same as "the card is
protected." For a gaming session or a long LLM batch job (OCR, transcribe, tag-a-disk)
the operator would want the opposite: *while the card is held by another tenant, this workload
stays out of VRAM. That would be the value of Berth in this scenario. * There is no configuration
that gives that today; the stowaway list is report-only.
While self-healing it still causes effectively system lock-up for a while (unresponsive desktop, stuttering,
possible timeouts of certain jobs and Berth would be here valuable solution to remove manual lifecycle
management. -
Manual start/stop is still needed. Killing the
berth runwrapper releases only the
lease; it does not stop the container (daemon-owned,unless-stopped,compose up
without-donly attaches logs). So the operator is back to hand-runningdocker stop/
docker startto keep a stowaway workload out — exactly the manual step this feature removes.
The precedent this builds on (llama-swap + #96)
The llama-swap wiring works because the drain is the engine's own exit: Berth holds the
lease and, on a competing grant, calls llama-swap's own unload endpoint
(/api/models/unload/<model>) — it never signals the process. The 27 B unloads, VRAM frees,
the next engine is admitted.
Issue #96 generalizes this and the maintainer has confirmed the shape: Berth never signals a
process it did not start; it uses the engine's own way out (a load_url/unload_url on the
[[engine]] entry, plus a match list so the engine's VRAM is counted as the engine's, not a
stowaway).
A Docker container fits that design exactly — its "own way out" is the container runtime's
documented lifecycle:
- unload =
docker stop <name>(ordocker compose -f <file> stop <svc>) — sends
SIGTERM, the container's graceful path, releases the CUDA context and its VRAM. - load =
docker start <name>(ordocker compose -f <file> up -d <svc>) — brings it back.
docker stop is a graceful stop, not a kill -9; the container can trap it and clean up
(unload models), so it is the container's own exit. Berth invokes the runtime's lifecycle API —
it does not signal an arbitrary PID. This is the same principle as the llama-swap endpoint, with
a shell command standing in for the HTTP URL.
One load-bearing Docker behavior that makes this a stable knob (per the documented restart
policies): with restart: unless-stopped, a container that is explicitly stopped via the
CLI/API is not auto-restarted. So when Berth runs docker stop, the container stays stopped
until Berth (or the operator) starts it again. That is what lets Berth durably keep a workload
out of the card while a competing tenant holds it — instead of a transient stop that the
daemon immediately reverts. (Contrast restart: always, which would revert an explicit stop;
see "Open questions".)
How this could work
Extend the [[engine]] entry so an engine's load/unload can be commands, not only HTTP
URLs — with a kind = "docker" / kind = "compose" preset that fills them in. Then a container
is governed like any other engine.
Sketch (there may be better ways to do it via docker desktop API?):
[[engine]]
name = "pet-tagger"
kind = "docker" # or "compose"; sets load/unload to the lifecycle below
# --- docker lifecycle (the "own way out") ---
name_docker = "immich-pet-tagger"
# load -> docker start immich-pet-tagger (or: docker compose -f F:/AI/immich-pet-tagger/docker-compose.yml up -d immich-pet-tagger)
# unload -> docker stop immich-pet-tagger (or: docker compose ... stop immich-pet-tagger)
# --- accounting (the #96 "match") ---
match = ["immich-pet-tagger"] # process / container identity whose VRAM is the engine's, not a stowaway
needs_mib = 2560 # measured peak (see evidence); drives admission math
# --- optional ---
# ready = "..." # readiness probe after load (e.g. the app's HTTP port)
Behaviour that follows:
- Per-container load/unload controlled by Berth. On a grant, Berth runs
load(start the
container); on a drain (a higher-priority grant, or a whole-card claim), Berth runsunload
(docker stop), waits for the driver to show the memory free, and only then releases the
lease / admits the next tenant. Symmetric to llama-swap. - Admission gating (the "keep out while busy" the operator actually wants). While a
whole-cardclaim/hold(e.g. a game) or a higher-priority lease is active, a drained Docker
engine stays stopped — Berth does not restart it on its own schedule, because the card is
held. On release, it reloads only if its own lease/claim is granted. This is what makes
"card busy with gaming/OCR → tagger stays out of VRAM" actually hold, rather than
"coexists by displacement." matchaccounting. The container's VRAM is attributed to its engine entry (counted as the
engine's, withneeds_mib), not buried in the stowaway list — so the stowaway list stays
honest and the admission math sees the real number.
The design deliberately reuses #96's invariants:
-
Berth never signals a process it didn't start — it runs the container runtime's own lifecycle.
-
If
unloadis refused or the memory doesn't come down, Berth waits for the next boundary and
reports the lease stuck; it never grants the card on top of a loaded model.I am not yet sure how it would ideally work in practice, but due to different nature of ML containers
- some of them load models on startup some only lazy-load - it would be useful to have "unload-only" option
- where Berth evicts a certain container, but does not restart it when memory is free.
Evidence
- Displacement coexistence, hardest state (unleased): peak 15,567 MiB < 16,303 card, no OOM,
no lockup, no stuck state; the 27 B kept serving through the whole scan window; ~4.3 GB of its
weights went to RAM for the ~65 s scan; re-pagination <2 s. So the coexistence is safe — the
missing piece is the gate, which this feature adds. - Leased run works end-to-end:
berth run --as pet-tagger --priority foreground --needs-mib ...drained the 27 B lease (<5 s), granted the tagger, ran a clean poll cycle (VRAM peak
2,098 MiB), and released on wrapper exit — no stuck leases. So the broker path that this
feature reuses (drain → grant → release) is already proven; we're extending what can be an
engine (a container) and how it's drained (its lifecycle). needs_mibmeasured: peak 2,098 MiB during a clean cycle → configure2560.- Manual trigger exists:
POST http://<container>:8000/api/scan(port not host-mapped; call
viadocker exec) — used to force a cycle for the measurements above.
Environment
Windows 11, Docker Desktop (WSL2 backend), RTX 5070 Ti 16,303 MiB (governed), berth 0.3.x.
Containers run inside the WSL2 VM; the GPU is passed through via the NVIDIA runtime, so a stopped
container's CUDA context and VRAM are released back to the driver. (Other backends should be
equivalent for the lifecycle semantics.)
Open questions
- Restart policy.
unless-stopped(explicit stop holds) is the policy under which Berth's
docker stopis a stable "keep out" knob. Should the entry require/verify a non-always
policy, or should Berth treat "container came back without myload" as a stowaway and
re-drain it? - Lease semantics for a service. The tagger is a long-running scheduled service, not a
run-to-completion job. Is the intended model "lease = container running (Berth keeps it up
while granted, stops it on drain)" rather than "lease = one scan"? - Command vs. Docker preset. Do you want
load_cmd/unload_cmd(generic, any engine with a
start/stop) plus akind="docker"sugar, or a dedicated container entry? Either reuses #96's
match+ stuck-drain logic. - Compose granularity.
docker compose stop <svc>preserves the container (and state/volumes);
downremoves it. Presumingstop/start(notdown) is the right option. - is docker CLI even the best lever here - maybe there is a batter one I don't know in docker ecosystem? Especially one that would interact with app INSIDE the container.
Note on scope / relation to #96
Not a duplicate of #96. #96 is about adopting an engine Berth did not start (Strata) via its
HTTP API. This is the Docker-container instantiation of the same "engine's own way out"
design, where the "way out" is the container runtime's lifecycle instead of an HTTP endpoint. If
load_url/unload_url ships first, this is just the command-based sibling of it.
- 主要语言
- Go
- 星标
- 226
- 派生
- 20
- 平均合并
- 51 分钟
- 30 天内合并 PR
- 225
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
T-Crypt/berth 的其他 Issue
-
难度 5/5 一周以上 新手友好度 42/100
维护者通常 1 天内回复
-
berthd --gpu-backend amd exits at startup on a desktop session: "process VRAM exceeds card usage"未关闭
难度 4/5 1-2 天 新手友好度 48/100
维护者通常 1 天内回复
-
Lend part of a card from a running engine and get it back, without a park or a reload可能已有人在做 @T-Crypt 于 6 天前认领。 未关闭
T-Crypt/berth#189 · 26 条评论 · 1 个 reaction · 已指派 1 人 ·
维护者通常 1 天内回复
-
Idea: Proxmox support in every layout (testers wanted)可能已有人在做 @sharmis-tp 于 3 天前认领。 未关闭enhancement help wanted
难度 5/5 一周以上 新手友好度 25/100
T-Crypt/berth#128 · 2 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
enhancement
难度 5/5 一周以上 新手友好度 25/100
维护者通常 1 天内回复
相似的 Issue
-
难度 1/5 1 小时以内 新手友好度 72/100
slavakurilyak/awesome-ai-agents#888 ·
维护者通常 1 天内回复
-
enhancement nav-pilot
难度 2/5 1-3 小时 新手友好度 88/100
维护者通常 1 天内回复
-
kind/enhancement
难度 2/5 1-3 小时 新手友好度 64/100
维护者通常 1 天内回复
-
feat(gmail): make gmail get --format raw text output the exact message, like drafts get --format raw未关闭clawsweeper:needs-product-decision clawsweeper:no-new-fix-pr clawsweeper:source-repro impact:ux-friction issue-rating: 🦞 diamond lobster P3
难度 2/5 1-3 小时 新手友好度 78/100
openclaw/gogcli#1276 · 1 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
-
Seed a JRNYAx price before the last close so asset-detail S1.3 doesn't fail from 08:00Z to the open未关闭bug tests
难度 2/5 1-3 小时 新手友好度 83/100
lognorman20/monaco#7231 ·