Repository navigation
Berth v0.4.0-rc.7
Pre-release
Pre-release
·
92 commits
to main
since this release
A pre-release for testing. Production installs stay on the latest release; berth update never offers a pre-release.
Changed
- berthd and
berth service installgovern every card by default (--gpu all), where they took card 0. On a
machine with one card nothing changes. On a machine with several, berthd now also watches the other cards, and
a GPU process there without a lease shows as a stowaway on its card. Name an engine's card with--gpu, or keep
the old behavior withberth daemon --gpu 0. A unit installed earlier keeps the--gpuit was written with.
Fixed
berth wire llama-swap --write --updatekeeps the flags you added to a model's line by hand, such as
--needs-mib 14000or--wait 700s, and rewrites only the flags wire writes. It replaced the whole line and
dropped them without a word (#231).berth wire llama-swapon Windows finds the lines it wrote before. It recognised only a binary namedberth
after a/and read every backslash as an escape, so a second--writewithF:\AI\berth\berth.exewrapped
each model again, and an update would have mangled a Windows path in a flag added by hand.- A drain of a llama-swap model waits for the request in flight before it unloads the model. The line
berth wirewrites looked for/slotsat llama-swap's root, which has none, so the drain found no boundary and the
unload cut the request off.berth wirenow adds--drain-slots <api>/upstream/<model>, where llama-swap serves
each model's slots.berth run --drain-slotsnow keeps the URL's path and is checked before the unload URL is
posted; it used only the URL's host before, which also defeated a hand-written path. Rewire withberth wire llama-swap --config <file> --write --update.--updatealso rewrites flags you added to the line by hand (#231).
azrael-229 found it (#172). - On a machine with more than one card, the gateway checks the card an engine runs on. A background engine
waited for every card to be free, so a lease or a stowaway on gpu1 kept a background engine on gpu0 from ever
starting. A queued ComfyUI job checked every card the same way. The engine page and the board listed every card's
holders under each card, and the board now names each card (card). An engine's card is the one itsgpunames,
or the first card when it names none. berth eventsnames the card on each line when berthd governs more than one, asberth statusdoes with its
CARD column. A lease across several cards printed the same line once per card, with no way to tell them apart.
On a machine with one card the lines are unchanged.--format jsoncarries the card on every event.berth run --gputakes a card's PCI address when berthd picked its cards by index, as--gpu all(the
default) and--gpu 3do. berthd recorded an address only for a card it was told to govern by address, so
--gpu 0000:09:00.0was refused with "no card 0000:09:00.0 on this node". TacoTakumi found it on rc.4 (#189).- berthd uses less CPU when idle. The claims watcher reads the process table every 2 s, and each scan now
takes a third of the syscalls: 2.0 ms instead of 7.2 ms for 534 processes. The AMD probe reads the fdinfo of a
process's GPU device files only, not of every open file, and walks the processes again only when the card's used
memory moves or every 10 s. cygnal measured the AMD probe at about 15% of a core on a desktop with a browser and
Electron apps (#196). berth status,berth runand the other commands find the user service's socket from a shell without
XDG_RUNTIME_DIR, such as ssh. They looked in/tmp/berth-<uid>.sockwhile berthd listened in
/run/user/<uid>/berth.sock, and reported berthd unreachable. WhenXDG_RUNTIME_DIRis unset, Berth now uses
/run/user/<uid>if it exists (#196).- The gateway forwards a request for a parked llama-swap model to llama-swap, and the model's front (
berth run --front) wakes it. With one llama-swap model parked, the gateway counted all of llama-swap as parked and answered
every llama-swap model with a 404 until llama-swap unloaded the parked one. A chat to a parked NInfer now wakes it
in about 6 s instead of failing for up to five minutes. berth run --frontpicks the engine's port from 20000-29999. It took a port the kernel had just handed out and
released, and the engine binds it only once its lease is granted, which can be minutes later. In that time any
outgoing connection could take the port, and the engine failed to start. Ports below the ephemeral range are never
handed out on their own, and the range stops below Kubernetes' NodePorts.- A custom park hook (
park_url,resume_url) sends{}withContent-Type: application/json. It sent an
empty body, which Strata's/v1/unloadand/v1/loadrefuse with 415, so a Strata entry could not park through
them. The built-in vLLM and SGLang calls and the Berth park protocol are unchanged (#96). - A resumed lease that is still waiting for the card counts against it only for the residual it holds, not for the
whole card it asked for. Its weights are in RAM until its grant lands. Aberth holdbehind a resumed ComfyUI
waited 1m44s with nothing to drain, because berthd still charged it ComfyUI's whole card. Such a waiter is now
granted beside the residual, or the parked engine gives way first, exactly as beside any parked engine. - The harbor library shows SERVED BY and EST. VRAM for a llama-swap model whose ID differs from its GGUF file's
stem, such asqwen-3.8-27bforQwen3.8-27B-UD-IQ4_XS. Berth reads the model file from thecmdin
llama-swap's/runningand matches the library row on that file before it tries the name (#197). - The AMD probe no longer charges a process for purgeable VRAM, the buffers the kernel may reclaim. A GNOME
desktop on the same card reported more resident VRAM than the card used, so berthd refused every reading and
exited at startup. Purgeable bytes now stay in the card's baseline beside shared ones (#196). berth service install,uninstallandstatusprint the error when they fail. They panicked instead, for
example under a shell with no user-session D-Bus, wheresystemctl --usercannot answer (#196).- Park admission counts the RAM a parked engine holds: the anon and shmem of its berth scope's cgroup, not its VRAM
footprint. A parked ComfyUI was counted at 17.5 GiB while it held about 2.5 GiB, because its weights sit in page
cache, which the kernel counts as available. Admission also subtracted parked RAM a second time from a reading that
had already dropped by it. Both refused parks that fit. The next park of an engine now costs what its last park
added: the RAM it grew by while it parked, since what it held hot was already out of the reading. A park still in
flight now counts against the next one, so two engines parking at once cannot claim the same free RAM. A run with
no berth scope still counts its footprint (#193). - A lease that parks a second time gets the whole release grace for its own park call. It ran from the previous
park's call, so a park that takes longer than one tick (a 23 GBcuda-checkpointtakes 5 s) was given up on at
once, and the engine was left checkpointed while berthd waited for it to drain. - A drain that arrives while the run is still parking the engine (
berth stop, or berthd giving up on the park) is
kept and acted on when the park ends, instead of dropped. berth stopstops a parked or parking engine. It answered "no running engine" before.- A lend from a whole-card holder works on a card with a baseline, such as a driver's own memory. berthd counted
the holder at the card's total, which already held the baseline, and then took the baseline off the card again.
Once the card confirmed the lend, the waiter was still short by the baseline and berthd drained the holder it
had just lent from, and a lend that did land was never asked back. A whole-card holder now counts what the card
can hand out. Found and measured by TacoTakumi on a 4060 Ti with a 121 MiB baseline (#189). - A lend where the engine gives a little less than asked, and the card's own free memory makes up the rest, now
grants the waiter. berthd confirmed the lend from the driver's reading but granted from the engine's number, so the
waiter was short by the gap and berthd drained the holder. Seen live with Strata on an RTX 4090: asked 3072 MiB,
Strata freed 2939 MiB in cache segments with 406 MiB already free, and berthd drained it. The books now credit the
room the card shows free (#189). - A lend survives an engine that takes a while to answer. Strata finishes the answer it is streaming before it gives
memory back, and berthd gave up on a lend 30 s after the ask whileberth runwaited 60 s for the engine, so a
long answer turned the lend into a drain. berthd now waits the run's 60 s for the answer, and the 30 s grace for
the card to show the memory free counts from the answer.
Added
- On a Proxmox VE host, a GPU process inside an LXC container shows as a stowaway named by its container,
lxc:101 (ollama), where it showed as a bare PID. Berth reads the VMID from the process's cgroup (/lxc/101/...)
and the name from thehostnamein/etc/pve/lxc/101.conf. That file is readable by root andwww-dataonly,
so a berthd running as a normal user showslxc:101alone. LXC'slxc-startmonitor is not a container, and
Docker inside an LXC still resolves to the Docker container (#128). berth wire llama-swap --wait DURwrites--waiton every model's line, so a batch request can queue
behind a long foreground lease instead of giving up after berthd's default 120 s (#231).berth run --as NAMEwith no command runs thestartof NAME's entry inberth.toml, as the gateway does, so
a configured engine no longer needs its command typed again. An entry matched only through--groupnever lends
itsstart, because that is the group's engine (llama-swap), not this one. azrael-229 asked for it (#232).[squatters] allowinberth.tomllists process names that are never squatters, asberth daemon --allow
does. A unit or scheduled task fromberth service installhad no way to pass--allow, so on a desktop a
browser or a terminal kept the card from ever being free and background engines never started. The flag still
replaces the list when given (#172).- One lease across several cards:
berth run --gpu gpu0,gpu1, orgpu = "gpu0,gpu1"on an engine entry. berthd
grants the lease on every named card at once, only when each one fits, and holds no card while it waits for
another. The engine starts withCUDA_VISIBLE_DEVICESlisting the cards' UUIDs in the order named, and its
memory on those cards counts as the lease's instead of showing as stowaways. A drain on any card stops the engine,
its exit frees every card, and a berthd restart re-attaches it to all of them under the same lease. A lease on
several cards cannot park or lend yet (#189). - An engine runs on the card it names:
berth run --gpu gpu1, orgpu = "gpu1"on its[[engine]]entry (a
PCI address works too). The lease goes to that card and stays there across a berthd restart. The engine starts
withCUDA_VISIBLE_DEVICESset to the card's UUID, which berthd reads fromnvidia-smiat startup, unless the
environment already sets it. A gateway start passes the entry's card on. A run that names no card goes to the
first card and its environment is left alone, so an engine split across two cards keeps both. - One berthd can govern several cards (experimental).
berth daemon --gpu allopens every card it finds, and
--gpualso takes a comma list of indices and NVIDIA PCI addresses (--gpu 0,0000:41:00.0). Each card keeps its
own leases, queue, drains and parks, so a foreground request on gpu0 never drains a holder on gpu1. The cards share
one event log, one set of lease IDs and one park budget. A card is namedgpuplus its index, or by its address
when it is in a VM at startup. A running game holds every card, because Berth can't tell which card it will use.
Alerts read the hottest and fullest card and name it.berth statusadds a CARD column to the lease, waiting and
stowaway tables when there is more than one card. The first card keeps the plain state files, and each other card
writes its own (usage.gpu1.json). - berthd runs its card as part of a node, the groundwork for one berthd governing every card on a machine. The
status JSON names each GPU, lease, waiter and stowaway's card (card), every event names the card it happened
on, and a client may name its card when it asks for one (cardon the socket); a request that names none goes to
the first card, as before. The node keeps one event sequence and one set of lease IDs for the machine. Nothing
changes yet for a berthd with one card. berth runwarns when a container it names on a lease has restart policyalways, with--containeror
--compose. Docker restarts such a container when its daemon starts, even after a drain stopped it, so it comes
back on the card without a lease after a reboot. docs/engines.md says which policy fits (#172).- Pre-releases for testers. Work for the next release lands on the
nextbranch and is taggedvX.Y.Z-rc.N, which
publishes as a GitHub pre-release. The README's download link,go install ...@latestandberth updatestay on
the latest release. Testing a pre-release covers installing one and going back. berth config checkwarns when[park]turns eviction off:floor_gib = 0means Berth never stops a parked
engine when RAM runs low, and apressureof 100 or more is past anything/proc/pressure/memoryreports.
--strictfails on both (#193).- Engines with no offload call of their own (llama.cpp, NInfer, any llama-swap model) park in RAM through NVIDIA's
cuda-checkpoint:berth run --park checkpoint --front 127.0.0.1:${PORT}with--port {{BERTH_PORT}}in the
engine's command. The front takes the requests llama-swap sends, waits for the ones in flight before the
checkpoint, holds new ones while the engine sleeps, and asks berthd for the card on the first, so no request
hangs on a checkpointed engine. A 23.1 GB NInfer comes back in 3.3 s instead of a 14 s reload.park = "checkpoint"on an engine entry is refused: a front is one run's address. docs/design.md is the model this and
the coming lends, multi-card leases and networked nodes follow. - Experimental: an engine that can shrink a cache lends part of its card to a sized waiter instead of being drained for it:
lend = "hook"on the entry, withlend_urlfor an engine that serves the lend routes
(contrib/LEND-PROTOCOL.md), and Strata's/v1/vrambuilt in. The holder keeps
serving while the borrower builds beside it. Berth grants the borrower only once a card read taken after the
engine answered shows the memory free, and the lend stops counting against the holder at that read, never at
the engine's word. The holder is asked for its memory back once the borrower's is measured gone, and only when
the card has room for it again. A lend the engine refuses, or one the card has not confirmed within
--release-grace(30 s), falls through to the park or drain that would have run anyway. Nothing is asked of a
run that did not declare lend calls, of a pinned holder or a hold, or on an APU. This is the broker's decision
and the wrapper's calls, and the wrapper keeps the ledger of what its run has lent. A lend the engine did not
answer in time (its503) is treated as refused for that waiter, and the wrapper reads the engine's ledger
(GET /vram/state) for 30 s in case it lands, then asks for that memory back at once. After a
berthd restart the run re-attaches naming its open lend, so the new daemon counts the holder's budget less the
lend and asks for the take-back once the card has room, instead of blocking waiters behind memory that is gone.
lease.lending,lease.lent,lease.returnedandlease.lend_failedname it on the event stream, and
docs/gateway.md's Lending section states the rules. - A Docker Compose project can run as one engine:
compose = "/abs/path/compose.yml"on an entry, or
berth run --compose PATH. The lease covers every container in the project, found with
docker compose -f PATH ps -q --allon every poll (podman when docker does not answer), so a container that
restarts under a new ID comes back to the lease. A drain runscompose stop -t <grace>, neverdown, which would
delete containers and networks you own. The start command stays yours, anddocker compose -f PATH upfits.
berth config checkrefusescomposetogether withcontainer, and a relative path.
Container engines now work on Windows too, where--containerwas accepted and ignored. WSL2 reports no
per-container VRAM. So while one container engine is the only one on the card, Berth charges it the WSL VM's
measured GPU row whenever that row is larger than itsneeds_mib. With two container engines Berth cannot split
the row, so each counts itsneeds_miband the row stays a stowaway, counted once. Sizeneeds_mibfrom your
heaviest run. One operator's photo tagger measured 2.1 GiB on a small scan and 6.7 GiB on its biggest (#172).
Inberth status --format jsonand the harbor, a lease'scontainerandcontainer_nameare replaced by
containers, a list of full container IDs. Aberth runstarted before an upgrade still attaches its container.