Manage Docker containers as engines — Berth-owned start/stop + keep-durably-out when the card is claimed
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 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分
- マージ済み PR(30日)
- 225
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
T-Crypt/berth のほかの issue
-
難易度 5/5 1週間以上 初心者へのやさしさ 42/100
T-Crypt/berth#204 · コメント 11 件 ·
メンテナーはふだん 1 日以内に返信
-
berthd --gpu-backend amd exits at startup on a desktop session: "process VRAM exceeds card usage"オープン
難易度 4/5 1〜2日 初心者へのやさしさ 48/100
T-Crypt/berth#196 · コメント 8 件 ·
メンテナーはふだん 1 日以内に返信
-
Lend part of a card from a running engine and get it back, without a park or a reload対応中かも @T-Crypt が 7 日前に担当しました。 オープン
T-Crypt/berth#189 · コメント 27 件 · リアクション 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
Idea: Proxmox support in every layout (testers wanted)対応中かも @sharmis-tp が 4 日前に担当しました。 オープンenhancement help wanted
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
T-Crypt/berth#128 · コメント 2 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
メンテナーはふだん 1 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
lognorman20/monaco#7428 ·
-
docs(prompts): IF BLOCKED workflow-scope branch says to comment on the issue on jira/local trackersオープンagent-butler-finding bug
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
jordansmall/spindrift#6238 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
microcks/microcks-cli#608 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug from-studio
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
esengine/DeepSeek-Reasonix#14868 ·
メンテナーはふだん 1 日以内に返信