Repository navigation
Berth v0.3.0
Fast swap and proof. Engines that can offload themselves park in RAM on a drain and come back
without a reload from disk, under the same lease rules: Berth admits a park only when measured RAM
covers it and evicts parked engines first when memory runs short. Leases free only after a card
read that came after the release, a gateway restart keeps the engines it started, and streaming
requests stay alive while an engine loads. berth reclaim, berth stop and berth resume give
the operator a way out of every lease state. Engines in Docker or Podman belong to their lease.
The failure scenarios in contrib/chaos run on any card, and Windows runs the full test suite in
CI.
Known gaps, for v0.3.x: a child that escapes its wrapper and whose parent exits within one 2 s
tick still shows as a report-only stowaway (cgroup attribution will close it); cuda-checkpoint
parking is not measured yet; the vLLM and SGLang park adapters are experimental until measured;
KV snapshots, swap-cost estimates on the live daemon, gateway fallback routing and JIT models for
a standalone llama-server have their building blocks merged and are wired in v0.3.x.
Added
-
The Berth park protocol: three endpoints an engine can serve on its own
HTTP API to be parkable without a Berth-side adapter,POST /berth/park,
POST /berth/resume(both without a body) andGET /berth/state, which
answers one ofhot,parking,parked,resuming. Berth detects it
with a single state read of 3 s or less, parks an engine through it, and
waits for a card read to show the residual before the next engine takes
the card; a park that leaves the card full falls back to the normal
drain. The contract, timing, idempotency rules and a minimal reference
server are in contrib/PARK-PROTOCOL.md, written for engine authors. -
park = "hook"with nopark_url/resume_urlon an engine type
outside the built-in adapter table is now valid: on a drain Berth asks
the engine'sGET /berth/state, and if it answers the Berth park
protocol the engine parks through it, otherwise it drains as today. A
config warning names the fall-back, and one URL without the other
stays an error. -
berth update [--check] [--yes] [--force]: asks the release page whether a newer
berth exists, and without--checkdownloads this machine's binary and the
release's SHA256SUMS, checks the hash, and replaces the running binary in place
after confirmation, then names what to restart without restarting anything. The
check is one GET to api.github.com's releases/latest, at most once every 24 h
(the answer is cached in the state directory, the User-Agent is the running
version and nothing else); the[update] checkconfig key, on by default, is
Berth's own contact with GitHub, off by that one line. A dev build is refused
unless--force, and a checksum mismatch refuses the download. -
The harbor's header shows a newer release while the update check is on:
GET /v1/updateserves the cached answer (one GET to the GitHub API at
most a day, retried within the hour when it fails), and the notice carries
the release, a link to it, and a button that copiesberth update. The
page makes no request of its own beyond reading that one endpoint, and
with[update] check = falsethe harbor never touches GitHub and shows
nothing. -
The harbor's LIBRARY panel: every model in the configured roots with its
shelf, quant, size, CACHED share (COOL or COLD), the engine serving it when
one reports it resident, and that engine's estimated VRAM. The data comes
from the newGET /v1/modelson the harbor, which computes whatberth models --format jsonprints and walks the roots at most once per 30 s.
Each row carries theberth prefetch NAMEandberth models show NAME
commands as copy buttons; the harbor stays read-only. -
The harbor's lease views show the v0.3 parking states: a WARM badge on a
parked engine, with the RAM its weights hold (the engine's learned
footprint) and the age; PARKING as the transitional state on the
departures board and in the log. Boarding rows carry a Q flag with the
queue position and the node's completion estimate when the status payload
names them. A lease's ticket names the tier the weights live in (HOT on
the card or WARM in RAM), the queue rows, the container a run is in, and
the exact terminal order for it (berth stop ENGINE,berth resume ENGINE,berth reclaim LEASE) beside copy buttons. -
The harbor's engine cards show each engine's learned footprint as
est. VRAM, beside the engine's own reports. -
berth service install [--gateway] [--harbor] [--dry-run],berth service uninstallandberth service status: run the node, and the
gateway and the harbor when asked, as user services. On Linux they are
systemd user units rendered from the files in contrib/ (the binary the
user runs in place of the one those files name) under
~/.config/systemd/user/, started withsystemctl --user enable --now;
on Windows they are Task Scheduler logon tasks under \Berth, created
with schtasks, no admin needed, running in the user's session where
their GPU apps are, hidden, without a time limit, and restarted on
failure (three tries a minute apart). A unit file that differs is kept
as .bak,--dry-runprints what would be written and run and changes
nothing, and status says for each of the three whether it is installed
and running. On Windows the services run; the driver's per-process VRAM
reading is the next gap, W-1 in docs/WINDOWS-2026-10-20.md. -
contrib/windows/check.ps1: the operator's checklist for a Windows
node on a real NVIDIA box. It writes one report file, ready for the
drain-report issue form: the berth version and the node's services,
nvidia-smi, the card and its processes with berth's reading next to the
driver's (the same counter Task Manager reads, plus the processes the
driver sees that berth does not), one run of a CUDA app of your choice
underberth run, and the event log around the run. Plain Windows
PowerShell 5.1, no modules. -
A place in the queue and an estimate in every wait reason: every reason
the broker sends a waiter starts with its place ("next in line", "2nd in
line (behind interactive comfyui)"), and the status waiting table shows
the place and a start estimate for each waiting lease. The estimate
prices only what a book of measured switch times has measured, one start
per waiter ahead plus its own; an unmeasured start says unmeasured and
adds no number, and the holder's drain is never in it, because a request
runs as long as it runs. The broker takes the book on its config
(SwapCost); with none, the reasons carry the place only. -
The prediction of switch times:
internal/swapcostrecords the times
completed switches took, per engine and tier (hot on the card, warm
parked in RAM, cool in the kernel page cache, cold on disk), keeping
the last 20, and answers the next switch as the median of what it has
seen. A tier with no samples stays unknown and is never filled with a
guess, so a plan total with unmeasured steps is a lower bound, "at
least N s, M steps unmeasured", not a promise.Planworks out a
chain an orchestrator declares ahead of time, step by step: the cost
of each switch, the engines to prefetch while the current holder
drains, and the engines the chain comes back to, so the caller parks
them in RAM instead of stopping them. Groundwork; wired to the daemon
and the gateway in a later PR. -
The building blocks for KV snapshots:
internal/kvsnapsaves a
llama.cpp engine's busy slots to files on disk and restores them
before the next engine's first request, so a sequential run of
sub-agents does not prefill its context again after every model
switch. The slot client reads the engine's /slots list and drives the
engine's --slot-save-path actions; the index keeps the snapshot
directory in a ledger (a hash-named file per model and slot, never
across models), within a byte budget and above the library's storage
floor, and warms a model's snapshots into the kernel page cache, the
RAM tier, which is cached, never reserved. Groundwork; wired into the
drain and start paths in a later PR. -
Windows sees what the card holds: per-process VRAM comes from the GPU
Process Memory counter (the PDH counter Task Manager reads), which
fills in an engine's own figure where the driver says [N/A] and lists
a graphics process or game holding more than 64 MiB of dedicated VRAM,
so squatters and claims count it. Host RAM and the page file come
from GlobalMemoryStatusEx, so the gateway headroom guard and park
admission work on Windows, and root space comes from GetDiskFreeSpaceEx,
so roots, floors andberth storagework on an NTFS drive. -
The test suite runs on windows-latest in CI, alongside the Linux job
and the existing Windows cross-build check. Tests that shell out to
sh, sleep or true, or depend on the Unix signal ladder, run where
those exist and are marked//go:build unix. -
The page-cache warmth of a model library:
berth modelsgains a CACHED
column (the share of a model's files the kernel page cache still holds,
summed over its parts and its companions, with the tier that share reads
as: COOL at half or more, COLD below),berth models showsays what of
one model sits in the page cache, and the JSON carriescached_bytes.
The check reads the kernel's page table (mincore) in 1 GiB slices and
never a byte of a file, so it cannot pull a page in;--no-cache-check
skips it. The page cache is the kernel's and can vanish when the machine
needs the RAM, so the column says a restart is likely to read from
memory, never that any RAM is held. -
berth prefetch MODEL_OR_PATH: ask the kernel to read a model's files
into the page cache (posix_fadvise WILLNEED). The command returns as
soon as the ask is made; the kernel does the reading in the background
and commits no RAM of its own. This is the manual form of what the
gateway will do for the next engine while the holder drains. -
The
internal/pagecachepackage behind both:Resident(a file's
resident share, measured read-only) andPrefetch. Off Linux both
report an unsupported interface, which the listing shows as unknown
rather than a failure. -
berth reclaim [LEASE] [--yes]: lists the processes a stuck lease recorded
and, only with--yes, stops exactly those (SIGTERM, then SIGKILL after
--grace), re-checking PID and start time before every signal. Processes
no lease recorded are listed as report only.
#48 -
Every descendant of an engine's wrapper is recorded each tick (PID plus
start time), so a child that escapes its group and reparents keeps the
lease stuck under its own name instead of showing as a stowaway.
#48 -
contrib/chaos:hog.pyand the failure scenarios anyone can run, a drain
report issue form, and a tested-hardware table in the README.
#44 -
contrib/bench/swap: times model switches through the gateway (cold and
warm, per engine), with VRAM after drain, RAM, identical-output and leak
checks. #47 -
The engines' VRAM footprints: a freed lease folds its measured peak into
its engine's record (the most memory seen, the last run's peak, the run
count), kept in~/.local/state/berth/footprint.jsonacross restarts.
berth statusshows the last run's peak as EST. VRAM beside the
engine's own figures, and the JSON names itestimated_vram_bytes; it
is an estimate, never a reservation. A run that holds far more than the
learned figure (1.5x, after three runs) logs anengine.footprint
event. -
drain_max(berth.toml) andberth run --drain-max DURATION: a ceiling
on how long a drain waits at a boundary (a busy slot, a running ComfyUI
job) before it runs the normal stop ladder; 0 = unbounded, the default.
While a boundary is busy the wrapper prints a line each minute naming
the slots and the wait age, and the ceiling says so when it ends the
wait. -
berth stop ENGINE [--wait]: stop an engine Berth started, on purpose.
The daemon asks each active or draining lease of the engine (by name or
engine family) to give the card back through the normal stop ladder, and
a lease frees only after a card read shows the engine's memory gone. A
pinned lease is asked too: the pin keeps the scheduler off it, not an
explicit stop. An engine Berth never leased is refused by name, and a
stuck lease is left toberth reclaim.--waitfollows the event log
until the leases are freed (exit 0) or stuck (exit 2, naming them).
Stopping the gateway no longer stops its engines: they run outside its
cgroup, andberth stop ENGINEis how one of them is stopped on purpose. -
Park in RAM, the broker side: a draining lease whose engine can offload
itself asks to park; admission runs over the machine's readings, the
parked lease keeps its residual counted on the card, and a waiter builds
beside a small residual or evicts a big one (past the ttl, under the RAM
floor, or a step at a time under memory pressure). Parked RAM is a
reading, never a reservation. -
The built-in park adapters: ComfyUI (its unload keeps the models in RAM
until pressure), vLLM (/sleep?level=1,/wake_up; the server starts
with--enable-sleep-modeandVLLM_SERVER_DEV_MODE=1), and SGLang
(/release_memory_occupation,/resume_memory_occupation; the server
starts with--enable-memory-saver --enable-weights-cpu-backup). Berth's
own park protocol (the same three-call shape any engine can serve) is
documented incontrib/PARK-PROTOCOL.mdand auto-detected:
park = "hook"with no hook URLs probesGET /berth/stateonce. -
berth resume ENGINE, and the gateway's requests to a parked engine,
resume the lease at its own priority and never proxy to an engine whose
wake has not finished.
Changed
- One ComfyUI client, one atomic file write that fsyncs the file and its
directory, and one/slotsrule, shared across the daemon.
#45 - Releases publish their CHANGELOG section as the release notes.
#44
Fixed
- A released lease frees only on a tick whose card read came after the
release and shows none of its processes, never inside the release itself.
#48