Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
29857a0
Merge pull request #270 from T-Crypt/T-Crypt/main-into-next
T-Crypt Oct 30, 2026
a7af653
feat(run): start the engine scope under a configured systemd slice
Oct 30, 2026
15f1303
Merge pull request #221 from Bacongineer/slice-scope
T-Crypt Oct 31, 2026
8f2f67b
feat(cli): name the card for berth hold and berth vfio run
T-Crypt Oct 31, 2026
180bc39
Merge pull request #271 from T-Crypt/T-Crypt/hold-vfio-gpu
T-Crypt Oct 31, 2026
882937c
feat(claims): hold only the cards a claim's gpu names
T-Crypt Oct 31, 2026
f7e5bb8
feat(harbor): rewrite the About tab and fix header clipping
T-Crypt Oct 31, 2026
d35e1c9
fix(harbor): scroll to a linked section after the first status
T-Crypt Oct 31, 2026
f7dd086
feat(harbor): list the claims matching now on the card tab
T-Crypt Oct 31, 2026
8c23d21
Merge pull request #272 from T-Crypt/T-Crypt/claim-cards
T-Crypt Oct 31, 2026
f8d20bf
Merge branch 'next' into T-Crypt/harbor-about-polish
T-Crypt Oct 31, 2026
350a6d3
Merge pull request #273 from T-Crypt/T-Crypt/harbor-about-polish
T-Crypt Oct 31, 2026
7cda188
Merge branch 'next' into T-Crypt/harbor-claims
T-Crypt Oct 31, 2026
98fd3c5
Merge pull request #274 from T-Crypt/T-Crypt/harbor-claims
T-Crypt Oct 31, 2026
3632013
test(run): an engine Berth stopped must not stay latched
T-Crypt Oct 31, 2026
ea6264f
fix(run): report an engine the run stopped as finished
T-Crypt Oct 31, 2026
ed35aaf
Merge pull request #277 from T-Crypt/T-Crypt/latch-clears-on-berth-stop
T-Crypt Oct 31, 2026
c04a3bc
test(gateway): a card's base URL reaches the engine on that card
T-Crypt Oct 31, 2026
52d6a8b
feat(gateway): one base URL per card
T-Crypt Oct 31, 2026
6135a6a
Merge pull request #278 from T-Crypt/T-Crypt/gateway-card-urls
T-Crypt Oct 31, 2026
963c8c0
docs: v0.4.1 contents, the v0.5 plan, and stale version references
T-Crypt Oct 31, 2026
b020ff1
Merge remote-tracking branch 'origin/next' into T-Crypt/docs-v04x-ref…
T-Crypt Oct 31, 2026
231220f
docs(roadmap): mark the first v0.5 slice merged
T-Crypt Oct 31, 2026
3006f9f
Merge pull request #279 from T-Crypt/T-Crypt/docs-v04x-refresh
T-Crypt Oct 31, 2026
9f25c42
test(gateway): a plain request picks the cheapest card for its model
T-Crypt Oct 31, 2026
92fbad4
feat(gateway): place a plain request on the cheapest card
T-Crypt Oct 31, 2026
83ed405
test: a cordoned card grants only holds and the gateway skips it
T-Crypt Oct 31, 2026
7446921
feat: berth cordon and uncordon a card
T-Crypt Oct 31, 2026
8a9524f
Merge pull request #280 from T-Crypt/T-Crypt/gateway-card-placement
T-Crypt Oct 31, 2026
91f9109
Merge remote-tracking branch 'origin/next' into T-Crypt/cordon-card
T-Crypt Oct 31, 2026
cdd41ca
Merge pull request #281 from T-Crypt/T-Crypt/cordon-card
T-Crypt Oct 31, 2026
3b2ec44
test(gateway): the board names each engine's card
T-Crypt Oct 31, 2026
9eb5b21
feat(gateway): a card picker on the board and the /gui/ page
T-Crypt Oct 31, 2026
bd0d8bc
Merge pull request #282 from T-Crypt/T-Crypt/board-card-picker
T-Crypt Oct 31, 2026
2ad45bb
chore(release): v0.4.1
T-Crypt Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,66 @@ has a matching GitHub release whose body comes from its section below.

## [Unreleased]

## [v0.4.1] - 2026-11-01

Each card on a machine with several now has its own gateway URL, `/cards/CARD/v1`, and one model can be served
on more than one card. A plain `/v1` request goes to the card that costs least to reach. `berth cordon gpu1` takes
a card out of rotation while the others serve, and the gateway's `/gui/` page names each engine's card.
`berth hold --gpu`, `berth vfio run --gpu` and `gpu = ...` on a `[[claim]]` hold only the cards they name. An
engine can run under a systemd slice, the harbor shows the claims that match now, and an engine Berth stopped
no longer shows as unresponsive.

### Added

- `GET /v1/berth/board` names the cards each engine's entry runs on (`cards`), and each card's base URL on the
gateway (`base`) and whether it is cordoned. On a node with several cards, the gateway's `/gui/` page names each
engine's card, so launching the copy of a model on gpu1 picks that card from a phone.
- `berth cordon CARD` stops new grants on a card, holds aside, while the node's other cards serve; `berth uncordon
CARD` lifts it. What holds the card keeps running. Waiters say the card is cordoned, `berth status` marks it
`CORDONED`, and the gateway treats it as out of reach: no background engine starts there and a ComfyUI job for it
queues. A berthd restart clears the cordon.
- Each card has its own base URL on the gateway, `/cards/CARD/v1`, named by the card's name or PCI address. A
request through it only reaches engines on that card, and `GET /cards/CARD/v1/models` lists only theirs. One
model can now be served on several cards, one engine entry per card, and `berth config check` still refuses two
entries that serve it on one card.
- A plain `/v1` request for a model served on several cards goes to the card that costs least to reach: the engine
that is up, then one parked in RAM, then a free card, then a card whose holder drains, and last a card a VM or a
pinned lease keeps. Cards of the same rank go to the first entry in `berth.toml`.
- `gpu = "gpu1"` on a `[[claim]]` holds only the cards it names, as `berth run --gpu` names them, on a node with
several cards. Engines on the other cards keep running, and the gateway starts background engines there. A
claim without `gpu` still holds every card. `berth config check` refuses a bad card name and a `gpu` on a
`track` claim. A name the node does not have is logged when berthd starts, and that claim holds every card.
- `berth hold --gpu CARD` and `berth vfio run --gpu CARD` name the card on a node with several. `hold` takes a
comma list for one hold on several cards, and `vfio run` takes one card. Before, both always took the node's
first card. Without `--gpu` they still do.
- `slice = "NAME.slice"` on an engine entry, or `berth run --slice NAME.slice` (which wins), starts the engine's
transient scope under that systemd slice on a Linux user manager: the slice unit's resource policy, CPU, memory
and swap limits, applies to the engine and everything it forks, not only to the wrapper that asked for the card.
`berth config check` refuses a slice name no unit can take. An entry with no slice starts its scope exactly as
before, on every platform.
- The harbor's card tab has a CLAIMS panel: the claim rules matching now, as `berth claims` lists them. A hold
names its lease or says it is waiting, a track gives the card memory it names, and on a machine with several
cards each row names its card. Before, a tracked app such as OBS did not show in the harbor at all.

### Changed

- The harbor's About tab is rewritten for v0.4. It leads with the node's version and links to the docs,
changelog, issues and source, adds a section on machines with several cards, and drops the list of planned
features that had shipped or changed. Its sections are cards in two columns on a wide screen, with the same
section rail as the other tabs.
- The harbor header no longer clips the ADVANCED switch or cuts its tagline off: below 1600 px the tagline
and the "& REQUESTS" half of the engines tab are hidden. On a phone an alert's message wraps instead of
ending in an ellipsis, with its time on its own line.

### Fixed

- An engine that Berth stopped no longer shows as unresponsive (LATCHED) in `berth status` and the harbor. A
drain, `berth stop`, a wake that failed or a stop signal from a supervisor such as llama-swap ends the engine on
purpose, and it now shows as offline, as an engine that exits 0 already did. An engine that exits non-zero on
its own still shows as unresponsive.
- A harbor link to a section, such as `#sec-library` from the engines tab's rail, opened the card tab on a
reload. It now opens the tab that holds the section and scrolls to it.

## [v0.4.0] - 2026-10-29

One berthd now governs every card on a machine. `berth run --gpu gpu1` puts an engine on one card,
Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ claim. When another engine holds it, Berth:
The numbered markers on the timeline are those four steps. A process that holds VRAM without a lease
shows up as a stowaway: Berth reports its PID and memory and leaves it running.

> **Status: alpha, v0.4.0.** Runs daily on an RTX 4090 workstation beside llama-swap, NInfer,
> **Status: alpha, v0.4.1.** Runs daily on an RTX 4090 workstation beside llama-swap, NInfer,
> Strata, ComfyUI and games, and testers run it on Windows. The [docs](docs/README.md) cover what works and has tests; the
> [roadmap](docs/roadmap.md) lists the phases and what is still missing.

Expand Down Expand Up @@ -108,7 +108,8 @@ line goes away.
stowaway report.
- **Your own rules.** `[[claim]]` in `berth.toml` covers anything else: a training run, a
render farm job, a cracking session, by name, command line or launcher.
- **`berth hold`** frees the card for a call or a game you start yourself. Ctrl-C gives it back.
- **`berth hold`** frees the card for a call or a game you start yourself. Ctrl-C gives it back. On a node with
several cards, `--gpu` names the ones it takes.
- **`berth run --pinned`** for an app or a training run that must never be stopped mid-work, on one card or
several. See [Training runs](docs/leasing.md#training-runs).

Expand Down Expand Up @@ -220,7 +221,13 @@ sits reserved while another is awaited. `CUDA_VISIBLE_DEVICES` lists each card's
on any of the cards stops the engine, and its exit frees every card. A lease on several cards cannot park or lend
yet: a drain stops it the usual way.

The gateway judges each engine against the cards it runs on. "Any card that fits" is not built yet.
The gateway judges each engine against the cards it runs on, and each card has its own base URL on the gateway,
`/cards/gpu1/v1`, so a client can choose the card ([One URL per card](docs/gateway.md#one-url-per-card)). A plain
`/v1` request for a model served on several cards goes to the cheapest card to reach: a parked copy, then a free
card, then one whose holder drains.

`berth hold --gpu gpu1`, `berth vfio run --gpu gpu1` and `gpu = "gpu1"` on a `[[claim]]` take only the cards they
name, so the other cards keep serving.

## Quick start

Expand Down
2 changes: 1 addition & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,6 @@ to undo.
| A2, shipped in v0.4 (experimental) | lends on one card through the protocol, with a stub engine | a sized borrower runs beside a lending holder; the return waits for the driver |
| B, shipped in v0.4 | one berthd for every card; leases with one share per card | a two-card engine leases both; the cards of a mixed box schedule apart |
| C, planned | parks and lends per share on multi-card leases | Maya on five cards lends from one card while the other four serve (#189) |
| D, v0.5 | gateways joined over the tailnet | a request to one node runs on another and the leases stay per node |
| D, v0.6 | gateways joined over the tailnet | a request to one node runs on another and the leases stay per node |

A stage that finds the model wrong changes this page first.
72 changes: 70 additions & 2 deletions docs/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,57 @@ curl -s http://127.0.0.1:9799/v1/berth/stop -H "Authorization: Bearer $(berth
free, held, draining, in a VM or failing its probe), then every engine in `berth.toml` with its state (up, offline,
waiting, starting, failed), whether it can be switched to (`launch`) or stopped (`stoppable`), and its models with a
`loaded` flag from the engine's own reader. `cards` lists every card the node governs, each with its name
on the node (`card`, such as `gpu1`) and the holders on that card alone.
on the node (`card`, such as `gpu1`), its base URL on this gateway (`base`, `/cards/gpu1/v1`), `cordoned` when
`berth cordon` closed it, and the holders on that card alone. Each engine names the cards its entry runs on
(`cards`), so a board for several cards can put each engine under its card.

## One URL per card

On a node with several cards, each card has its own base URL on the gateway: `/cards/CARD/v1`, where `CARD` is
the card's name on the node (`gpu1`) or its PCI address. A request through it only reaches engines whose entry
runs on that card, and `GET /cards/CARD/v1/models` lists only their models. Chat, completions, embeddings and
`/v1/berth/wait` take the prefix.

One model can be served on several cards, one engine entry per card, each on its own port:

```toml
[[engine]]
name = "qwen-gpu0"
type = "llamacpp"
url = "http://127.0.0.1:8080"
gpu = "gpu0"
start = ["llama-server", "-m", "/models/qwen.gguf", "--port", "8080"]
serves = ["qwen"]

[[engine]]
name = "qwen-gpu1"
type = "llamacpp"
url = "http://127.0.0.1:8081"
gpu = "gpu1"
start = ["llama-server", "-m", "/models/qwen.gguf", "--port", "8081"]
serves = ["qwen"]
```

A client that works on gpu1 sets its base URL to `http://127.0.0.1:9799/cards/gpu1/v1` and keeps the model name.
`berth config check` still refuses two entries that serve one model on the same card, or where either names no
card.

A request without the prefix picks the card itself. An engine that is up and lists the model answers first.
Otherwise the gateway takes the card that costs least to reach, in this order:

1. the card where the model's engine is parked in RAM;
2. a free card: no lease, waiter, stowaway or active claim, and not in a VM;
3. a card whose holder drains the usual way;
4. a card a VM, a pinned lease (a game, a hold) or a cordon keeps, where the request waits.

Two cards of the same rank go to the entry that comes first in `berth.toml`.

- A card the node does not have answers `404` with the node's cards.
- A cordoned card ([Cordon a card](leasing.md#cordon-a-card)) still takes requests through its URL, and they wait
in line.
- A model no engine on that card serves answers `404` naming the card.
- An engine berthd finds that has no entry in `berth.toml` belongs to no card here, so only the plain `/v1` URL
reaches it.

## Run an engine whenever the card is free

Expand Down Expand Up @@ -315,7 +365,7 @@ idle_after = "60s" # how long the card must stay free first (def
- A card back from a VM is a free card, so a background engine also picks up after a VFIO session.
- On a machine with more than one card, the card is the one the entry's `gpu` names (the first card when it names
none). A lease, a waiter or a stowaway on another card does not keep it out. A running claimed app does, because a
claim holds every card.
claim holds every card unless its `gpu` names other cards.
- A background engine needs `start`, runs at `batch` only, and needs no `serves`.

An engine whose work needs a sidecar runs the same way as a Compose project. Set `compose` instead
Expand Down Expand Up @@ -385,6 +435,8 @@ Point the bookmark (or a home-page tile) at `http://127.0.0.1:9796/gui/comfyui`
- `/gui/` lists every engine in `berth.toml` with its live state. Any engine with a `start` command gets a Launch
button there, with or without a `gui` URL, so a phone on the tailnet can switch the card to Strata or a
llama-server too. An engine with no web UI shows **Running** once it answers.
- On a node with several cards, each row in `/gui/` also names its card. With one engine per card for a model
([One URL per card](#one-url-per-card)), the row you launch is the card you pick.

<p align="center"><img src="https://raw.githubusercontent.com/T-Crypt/berth/main/assets/preview-wake.png" alt="The gateway's wake page for comfyui: comfyui is offline and the card is free, with a Launch comfyui button" width="560"></p>

Expand All @@ -398,6 +450,22 @@ loopback, accepts loopback Host names plus the gateway's `--allow-host` names (f
front), and takes a start only as a POST from its own page, so another site cannot make a visitor's
browser drain the card.

## Run an engine under a resource-policy slice

`slice = "NAME.slice"` on an engine entry (or `berth run --slice NAME.slice`, which wins) places the engine's
transient scope under that slice: the slice unit's CPU, memory and swap limits cover the engine and every child
it forks, where the wrapper's own unit limits cover only the wrapper. The slice unit is your file: for example
a `[Slice]` drop with `MemorySwapMax = 0` keeps a model-resident engine from paging at all. Berth refuses a slice
name no unit can take before the engine starts; entries with no slice behave exactly as before, on every platform.

```toml
[[engine]]
name = "llm"
type = "llamacpp"
url = "http://127.0.0.1:8080"
slice = "llm.slice"
```

## Drain a server at its request boundary

An engine with `/slots` but no unload URL (Strata, a bare llama-server) would otherwise get SIGTERM mid-request when
Expand Down
16 changes: 11 additions & 5 deletions docs/harbor.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,10 @@ question an operator actually asks.
in RAM), the queue rows, the container a run is in, and the exact terminal order
for the lease beside it as a copy button (`berth stop ENGINE`, `berth resume
ENGINE`, `berth reclaim LEASE`).
- **The berths, the machine, stowaways and no ticket** answer "what else is on the
card and the host, and who holds memory without a ticket?"
- **The berths, the machine, claims, stowaways and no ticket** answer "what else is on the
card and the host, and who holds memory without a ticket?" The claims panel lists the claim
rules matching now, as `berth claims` does: a hold names its lease or says it is waiting, and a
track gives the card memory it names. Idle rules are only counted.
- **Several cards.** When berthd governs more than one card, each card has its own berth panel that lists
only the leases and stowaways on that card. Each lease line names the model its engine reports
loaded, and a lease on several cards adds its card list (`on gpu0..gpu4`). A board row and a stowaway name their card (`gpu1`), a lease
Expand All @@ -170,8 +172,10 @@ question an operator actually asks.
recent requests and context residency** answer the questions behind the request
log, and the storage panel (advanced) says what the harbor's own store keeps.

**The ABOUT tab** is this page's own documentation: the modes, the shortcut keys,
and what every state word and figure means.
**The ABOUT tab** is this page's own documentation: the node's version, links to these docs and the
changelog, several cards, the modes, the shortcut keys, and what every state word and figure means. A
section rail jumps between its sections, as on the other tabs. A section link, from any tab's rail,
opens the tab that holds the section when you share or reload it.

## Telemetry (built in, no other tool needed)
- **GPU:** memory, utilisation, temperature, power against its limit, fan, SM and memory
Expand All @@ -185,7 +189,9 @@ and what every state word and figure means.
"no window / no slot data", never an error. An engine that was up and stops
answering is shown as **unresponsive** (latched), distinct from one that never ran. A
`berth run` engine that exits 0 shows as offline, since a batch job that finishes stops
answering on purpose. One that exits with any other code stays unresponsive.
answering on purpose. So does one that Berth stopped: a drain, `berth stop`, a failed wake,
or a stop signal from its supervisor. One that exits with any other code on its own stays
unresponsive.
- **Requests:** a 24-hour request log from llama-swap and Strata (a Strata entry needs
`type = "strata"`, and each request lands under the model its `/health` names); requests per minute, p50 and p95
latency, prefill time, time per output token, cache and re-ingest share, a token ledger
Expand Down
2 changes: 1 addition & 1 deletion docs/interfaces.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Interfaces

- **CLI:** one command tree with one output contract. `berth status`, `berth events -f`,
`berth run`, `berth hold`, `berth stop`, `berth notify`, `berth claims`, `berth pull`, `berth verify`, `berth models`, `berth gateway`,
`berth run`, `berth hold`, `berth stop`, `berth cordon|uncordon`, `berth notify`, `berth claims`, `berth pull`, `berth verify`, `berth models`, `berth gateway`,
`berth wire llama-swap`, `berth engine check`, `berth config check`, `berth config show`,
`berth export`, `berth key`, `berth skill install|show`, `berth update`, `berth version`,
and for the node itself `berth daemon`, `berth harbor`, `berth service install|uninstall|status`,
Expand Down
21 changes: 19 additions & 2 deletions docs/leasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
- **Several cards.** One berthd governs every card by default. A foreground request on one card never drains a
holder on another. `berth run --gpu gpu1` puts a lease on one card, and `--gpu gpu0,gpu1` holds one lease on
both (see [Training runs](#training-runs)). A running game holds every card, because Berth cannot tell which
card it will use. A lease on several cards cannot park or lend yet.
card it will use. A `[[claim]]` with `gpu = "gpu1"` holds only the cards it names. A lease on several cards
cannot park or lend yet.
- **Fail-open, never silent.** If the daemon is down, `berth run` still starts the engine
but records it as an *unprotected run* that status shows (`--fail-closed` refuses).
- **Survives a daemon restart.** A running engine stays up when berthd restarts or is
Expand Down Expand Up @@ -106,6 +107,7 @@
cmdline = 'train\.py' # regex; on Windows it sees the exe's full path
action = "hold" # or "track"
linger = "30s"
gpu = "gpu1" # hold only these cards (names or PCI addresses, comma list); default every card

[claims]
builtin = false # turn the built-in pack off
Expand All @@ -117,12 +119,27 @@
`reaper SteamLaunch AppId=...`, so the REAPER DAW, also named `reaper`, holds nothing.
- **`berth hold [--for 2h] [--reason game]`** takes the card for you. It is a pinned lease at
the top priority, so nothing drains it, and it ends on Ctrl-C, after `--for`, or when the
process dies.
process dies. On a node with several cards it takes the first one. `--gpu gpu1` holds another
card, and `--gpu gpu0,gpu1` holds both under one lease.
- **`berth run --pinned`** gives an app a lease that is never drained (it keeps the card until
it exits).
- A hold cannot free memory held by something outside Berth: that memory is still reported,
as contested.

## Cordon a card

On a node with several cards, `berth cordon gpu1` stops new grants on that card while the others keep serving, for
maintenance or before a VM takes it. Name the card by its name or its PCI address.

- What holds the card keeps running. A cordon drains nothing: `berth stop ENGINE` does that.
- Waiters stay in line, and their reason says the card is cordoned. A hold (`berth hold`, a game claim) is still
granted.
- The gateway treats the card as out of reach. A plain `/v1` request goes to another card that serves its model,
a background engine for the card does not start, and a ComfyUI job for it queues.
- `berth status` marks the card `CORDONED`, and `berth events` records `card.cordoned` and `card.uncordoned`.
- `berth uncordon gpu1` lets new work in, and the card's waiters are scheduled at once. A berthd restart also
clears the cordon.

## Training runs

A training run that loses its cards loses everything since its last checkpoint. Run it under one pinned lease:
Expand Down
Loading
Loading