inference: frank-large returns to halogen, frank-small takes the 3090
Dankclaw's endpoints are re-split. frank-large is the halogen-flash-server quadlet again -- the 2026-09-13 unit file was never touched by the two-day ik_llama.cpp interlude, so the runtime was still in the repo and its image is still in local storage -- retuned to the shape asked for: HALOGEN_CTX 200192 with a 400,384-position pool and two slots, exactly two 200k conversations. frank-small is new: mainline llama.cpp on the RTX 3090 serving ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF at IQ3_S, using the -mtp build (it carries blk.64.nextn.*, which is what lets --spec-type draft-mtp run on the main model's own weights instead of a second 12 GiB copy) plus the BF16 vision projector, as one 131,072-token slot with f16 KV on port 7771. The host is shared now, and that is what the halogen retune pays for: HALOGEN_MAX_TOK 24576 (arena 20.6 -> 16.4 GiB, ~4-5% prefill) and the vision tower dropped (1.97 GiB) hand ~6 GiB back to the host, and the reserve rises to 20 to keep the fit check honest. Vision moved wholesale to frank-small. Retired to scripts/.AGENTTRASH/: the ik trio (env, service, serve script), the Qwen-Sharp template only that runtime read, and four benchmark harnesses bound to retired runtimes. Because retiring a file makes any test that greps it pass by absence, the seams assert the archive exists AND the live path does not.
This commit is contained in:
1 parent
cc29875842
commit
43ec77fee6
19 files changed
+648
-198
No files matched your search
@@ -3,11 +3,13 @@
|
|||||||
# Install path on dankclaw: ~/.config/containers/systemd/frank-large.container
|
# Install path on dankclaw: ~/.config/containers/systemd/frank-large.container
|
||||||
# Unit name: frank-large.service (container name: frank-large)
|
# Unit name: frank-large.service (container name: frank-large)
|
||||||
#
|
#
|
||||||
# The large inference service, since 2026-09-11 the halogen-flash-server
|
# The large inference service. The halogen-flash-server runtime (since
|
||||||
# runtime serving peonist-ai/halogen-qwen3.8-flash-next (Qwen3.8-Flash-Next,
|
# 2026-09-11, restored 2026-09-16 after a two-day ik_llama.cpp hybrid
|
||||||
|
# interlude) serving peonist-ai/halogen-qwen3.8-flash-next (Qwen3.8-Flash-Next,
|
||||||
# w4b .hgn checkpoint + quality overlay sidecar). It replaced the llama.cpp /
|
# w4b .hgn checkpoint + quality overlay sidecar). It replaced the llama.cpp /
|
||||||
# kingjones30-ROCmFPX STRIX deployment because the engine is ~4x faster on
|
# kingjones30-ROCmFPX STRIX deployment because the engine is ~4x faster on
|
||||||
# prefill (the dominant cost of any real prompt) on this exact SKU.
|
# prefill (the dominant cost of any real prompt) on this exact SKU. The RTX
|
||||||
|
# 3090 it briefly shared with frank-large is now frank-small's card.
|
||||||
#
|
#
|
||||||
# SHAPE. The image runs both halves in one container: the engine on container
|
# SHAPE. The image runs both halves in one container: the engine on container
|
||||||
# loopback (:8730, UNAUTHENTICATED — never publish it) and the OpenAI
|
# loopback (:8730, UNAUTHENTICATED — never publish it) and the OpenAI
|
||||||
@@ -39,18 +41,23 @@
|
|||||||
# engine lowers it itself and prints what it settled on. Read that ledger on
|
# engine lowers it itself and prints what it settled on. Read that ledger on
|
||||||
# every restart — it is the ground truth for sizing, not this comment.
|
# every restart — it is the ground truth for sizing, not this comment.
|
||||||
#
|
#
|
||||||
# POOL TRIMMED 2026-09-13: 401,408 positions (~11.0 GiB of KV as reserved by
|
# POOL RETUNED 2026-09-16: 400,384 positions -- exactly two 200,192-token
|
||||||
# the engine), down from the 524,288 the 2026-09-12 doubling deployed. The
|
# conversations -- beside a per-conversation ceiling of 200,192 and two slots.
|
||||||
# doubling fit only at reserve 15, which left the host 7.1 GiB / 310
|
# It replaces the 401,408 the 2026-09-13 trim left behind, which held one full
|
||||||
# contiguous 2 MiB blocks and made the engine print its co-tenant stall
|
# native-262k conversation beside a ~139k one, or three at 131k.
|
||||||
# warning on every boot. MEASURED AT 401,408 (deploy ledger): 68.0 weights +
|
#
|
||||||
# 11.0 KV + 21.0 working = 100.0 GiB held, 9.1 GiB / 1396 contiguous 2 MiB
|
# MEASURED AT 401,408 (2026-09-13 deploy ledger): 68.0 weights + 11.0 KV +
|
||||||
# blocks left, engine need printed ~31.5 vs the modelled ~33.4, and NO
|
# 21.0 working = 100.0 GiB held, 9.1 GiB / 1396 contiguous 2 MiB blocks left,
|
||||||
# stall-warning NOTE. Capacity: one full 262k conversation resident beside a
|
# engine need printed ~31.5 against the modelled ~33.4, and NO stall-warning
|
||||||
# ~139k one, or three concurrent 131k chats -- the fleet's real shape at
|
# NOTE. Fit-check budget here: 121.2 - 67.7 - 20 = 33.5. That boot is the
|
||||||
# three slots. Fit check budget: 121.2 - 67.7 - 15 = 38.5. The 09-12 doubling
|
# baseline the 09-16 retune is measured against: the vision tower (1.97 GiB)
|
||||||
# story (silent clamp at reserve 24, the arena term, the reserve ceiling) is
|
# and roughly 4.2 GiB of arena come back to the host, which is what pays for
|
||||||
# preserved in the budget block below and the runbook.
|
# the co-tenant below.
|
||||||
|
#
|
||||||
|
# CO-TENANT since 2026-09-16: this host also serves frank-small on the RTX
|
||||||
|
# 3090 (llama.cpp, ~12.2 GiB of weights that load through the page cache and
|
||||||
|
# then live in VRAM, plus a CUDA context). It is the first real co-tenant this
|
||||||
|
# service has had, which is why the arena was halved and the reserve raised.
|
||||||
#
|
#
|
||||||
# BOOT NOTE: on the first request after a (re)start the engine can go silent
|
# BOOT NOTE: on the first request after a (re)start the engine can go silent
|
||||||
# for ~45-60 s ("has not answered PING") while it first-touches the freshly
|
# for ~45-60 s ("has not answered PING") while it first-touches the freshly
|
||||||
@@ -67,9 +74,11 @@
|
|||||||
# tower and the tokenizer. The repo is public and ungated, so no token is
|
# tower and the tokenizer. The repo is public and ungated, so no token is
|
||||||
# needed; later starts open no outbound connections at all.
|
# needed; later starts open no outbound connections at all.
|
||||||
#
|
#
|
||||||
# VISION. HALOGEN_VISION_TOWER=1 finds the sidecar beside the checkpoint. This
|
# VISION. Deliberately NOT set since 2026-09-16. The tower is a 1.97 GiB
|
||||||
# is the fleet's only image input since the FLM NPU endpoint was retired on
|
# charge against the same device budget as the pool and the arena, and the
|
||||||
# 2026-09-11; audio input has no replacement.
|
# fleet's image path moved to frank-small (llama.cpp on the RTX 3090, loaded
|
||||||
|
# with --mmproj mmproj-Qwen3.8-27B-BF16). This server refuses image content
|
||||||
|
# parts, and models.yml declares frank-large text-only to match.
|
||||||
#
|
#
|
||||||
# CLIENT CONTRACT (see serverAppConfigs-docs/LLM/halogen-flash-server.md).
|
# CLIENT CONTRACT (see serverAppConfigs-docs/LLM/halogen-flash-server.md).
|
||||||
# There are no server-side sampler defaults: a request that sends no
|
# There are no server-side sampler defaults: a request that sends no
|
||||||
@@ -101,83 +110,84 @@ Volume=/home/dank/models/halogen:/models
|
|||||||
Environment=HALOGEN_DOWNLOAD=peonist-ai/halogen-qwen3.8-flash-next
|
Environment=HALOGEN_DOWNLOAD=peonist-ai/halogen-qwen3.8-flash-next
|
||||||
# Fleet identity: the id at /v1/models, /health and every response body.
|
# Fleet identity: the id at /v1/models, /health and every response body.
|
||||||
Environment=HALOGEN_MODEL_ID=frank-large
|
Environment=HALOGEN_MODEL_ID=frank-large
|
||||||
# Native context of the model. Matches what both clients declare.
|
# Per-conversation ceiling: exactly half the pool, so two conversations of the
|
||||||
Environment=HALOGEN_CTX=262144
|
# full window are always resident. 200,192 rather than a round 200,000 is
|
||||||
# Trimmed 2026-09-13 from the 524,288 the 09-12 doubling deployed: buys
|
# pool/2. The model's native window is 262,144; a conversation past this
|
||||||
# ~3.4 GiB of co-tenant headroom, keeps one full 262k conversation resident
|
# ceiling is refused, never silently truncated. Clients declare 200192 (see
|
||||||
# beside a ~139k one, or three concurrent 131k chats.
|
# the frank-large contextWindow in models.yml).
|
||||||
Environment=HALOGEN_KV_POOL_POSITIONS=401408
|
Environment=HALOGEN_CTX=200192
|
||||||
# Concurrency cap. Each slot is 111.0 MiB of O(1) state; the KV is ONE
|
# The KV pool: 400,384 positions = 2 x 200,192 (the 2026-09-16 retune, down
|
||||||
# 401408-position pool carved into per-request regions, so slots cap how many
|
# from 401,408). ~28 KiB per position including block scratch, so ~11.0 GiB
|
||||||
# requests run at once while the pool binds how many positions exist. The
|
# held -- the 401,408 ledger entry measured exactly 11.0. The two slots still
|
||||||
# slots still OVERSUBSCRIBE the pool (3 x 262144 > 401408), so admission is
|
# OVERSUBSCRIBE the pool for any real request, because admission reserves
|
||||||
# pool-bound -- one full-length conversation plus a ~139k second, or three
|
# prompt + max_tokens: two full-window conversations each holding an output
|
||||||
# at 131k.
|
# budget are pool-bound, and the second waits in arrival order for the first
|
||||||
# LOWERED 4 -> 3 on 2026-09-12: frees ~111 MiB on a host with 7.1 GiB left,
|
# to release its region. Read the `kv pool:` lines on every boot;
|
||||||
# and three users is the real fleet size. A fourth concurrent request now
|
# HALOGEN_KV_POOL_FIT lowers the pool itself if the budget refuses the
|
||||||
# queues behind the three rather than contending.
|
# configured one, and a green health check does not mean the pool you asked
|
||||||
Environment=HALOGEN_KV_SLOTS=3
|
# for is the pool you got.
|
||||||
# Sizing is budget arithmetic, all of it. ONE budget covers
|
Environment=HALOGEN_KV_POOL_POSITIONS=400384
|
||||||
# the KV pool, the prefill arena and the vision tower together:
|
# Concurrency cap: two, one per half of the pool. Each slot is 111.0 MiB of
|
||||||
|
# O(1) state, so this hands ~111 MiB back against the three set on 2026-09-12
|
||||||
|
# for the fleet size of the time.
|
||||||
|
Environment=HALOGEN_KV_SLOTS=2
|
||||||
|
# Sizing is budget arithmetic, all of it. ONE budget covers the KV pool, the
|
||||||
|
# per-call prefill arena and the vision tower together:
|
||||||
#
|
#
|
||||||
# budget = 121.2 host - 67.7 resident weights - reserve
|
# budget = 121.2 host - 67.7 resident weights - reserve
|
||||||
# need = KV (~28 KiB/position) + arena + vision (1.97 measured)
|
# need = KV (~28 KiB/position) + arena + vision
|
||||||
# arena = 3.80 GiB + 8.40 GiB x (HALOGEN_MAX_TOK / 16384)
|
# arena = 3.80 GiB + 8.40 GiB x (HALOGEN_MAX_TOK / 16384)
|
||||||
#
|
#
|
||||||
# The arena term is linear in max_tok, and it reproduces five live restarts
|
# The arena term is linear in max_tok, and it reproduces five live restarts
|
||||||
# AND upstream's three published "on the device" figures to within 0.1 GiB —
|
# AND upstream's three published "on the device" figures to within 0.1 GiB —
|
||||||
# note those published figures are pool+arena totals, not pool alone, which is
|
# note those published figures are pool+arena totals, not pool alone, which is
|
||||||
# the misreading that first made a doubling look affordable here. Every corner
|
# the misreading that first made a doubling look affordable here. Corners this
|
||||||
# this box offered at a reserve of 24:
|
# box offered at a reserve of 24, at the old 262,144-position pool:
|
||||||
#
|
#
|
||||||
# 16384 -> need 28.6 vs 29.5 FITS, 0.9 spare (doubling, 8-9% prefill cost)
|
# 16384 -> need 28.6 vs 29.5 FITS, 0.9 spare (doubling, 8-9% prefill cost)
|
||||||
# 32768 -> need 37.0 vs 29.5 POOL CLAMPED to 262144, deploy still green
|
# 32768 -> need 37.0 vs 29.5 POOL CLAMPED to 262144, deploy still green
|
||||||
# 65536 -> need 46.5 vs 29.5 OOM, will not boot; unit still reads ACTIVE
|
# 65536 -> need 46.5 vs 29.5 OOM, will not boot; unit still reads ACTIVE
|
||||||
#
|
#
|
||||||
# None of those three gave both features; reserve 15 made the doubled pool
|
# LOWERED 32768 -> 24576 on 2026-09-16 for the frank-small co-tenant on this
|
||||||
# fit (38.5 vs 37.0) but at 7.1 GiB of host headroom. On 2026-09-13 the pool
|
# same host. The arena's variable term goes 8.40 x 2 -> 8.40 x 1.5, handing
|
||||||
# came down to 401,408 to restore co-tenant headroom: need ~33.4 vs 38.5.
|
# ~4.2 GiB back to the host at roughly a 4-5 % prefill cost -- the same lever
|
||||||
|
# at 16,384 costs 8-9 %. The vendor's own notes name 24,576 as the largest
|
||||||
|
# arena that allocated with headroom to spare, so this stays inside the range
|
||||||
|
# it has tested.
|
||||||
|
#
|
||||||
|
# The arena is a PER-CALL PREFILL CHUNK, not a prompt ceiling: the
|
||||||
|
# 200,192-token conversation is prefilled in arena-sized pieces, so a smaller
|
||||||
|
# arena means more pieces and a slower prefill, not a smaller conversation.
|
||||||
|
#
|
||||||
# THE POOL, ARENA AND RESERVE ARE A MATCHED SET, NOT independent tunables --
|
# THE POOL, ARENA AND RESERVE ARE A MATCHED SET, NOT independent tunables --
|
||||||
# the fit check's printed ledger on each boot is the arbiter, not these
|
# the fit check's printed ledger on each boot is the arbiter, not these
|
||||||
# comments.
|
# comments.
|
||||||
Environment=HALOGEN_MAX_TOK=32768
|
Environment=HALOGEN_MAX_TOK=24576
|
||||||
# RAM left for the n-gram table the engine reads through the page cache.
|
# RAM the fit check refuses to count on, watched over by the engine's own
|
||||||
# LOWERED 2026-09-12 from 24 (vendor default is 20) to buy the doubled pool at
|
# n-gram lookup table (47.7 GiB, read through the page cache and never held)
|
||||||
# full prefill speed. The fit check's OWN requirement is the line to honour —
|
# and, since 2026-09-16, by frank-small on the same host.
|
||||||
# read from a clamped boot, not modelled:
|
|
||||||
# "524288 positions need ~35.0 GiB (plus 1.5 of margin)" -> budget >= 36.5
|
|
||||||
# budget = 121.2 host - 67.7 weights - reserve
|
|
||||||
# reserve 16 -> 37.5 (1.0 spare); 15 -> 38.5 (2.0 spare, deployed). At the
|
|
||||||
# old 524,288 pool, 18 -> 35.5 CLAMPED it to 262144, so the ceiling there was
|
|
||||||
# 16; at 401,408 (need ~33.4) it loosens, but a higher reserve still buys
|
|
||||||
# nothing.
|
|
||||||
#
|
#
|
||||||
# AT THE DOUBLED POOL THE CEILING WAS 16, AND 16 BOUGHT NOTHING. Tested live at 524,288: 15 -> 16 changed the fit
|
# RAISED 15 -> 20 with the 2026-09-16 retune. This knob feeds the fit-check
|
||||||
# margin only. The server held 103.5 GiB and reported "7.1 GiB left for
|
# budget and is NOT an allocation: raising it cannot hand memory back to the
|
||||||
# everything else" at BOTH settings, because this reserve feeds the fit-check
|
# host, because the actual footprint is weights + KV + arena + vision, none of
|
||||||
# budget and is not an allocation — actual footprint is weights + KV + arena +
|
# which it touches — it can only take the pool away. What it does is keep the
|
||||||
# vision, none of which it touches. A higher reserve therefore cannot give the
|
# fit check honest at the smaller arena. With the arena at 24,576 and no
|
||||||
# host memory back; it can only take the pool away. 15 keeps the larger margin.
|
# vision tower, the engine's printed need falls to roughly 25 GiB against a
|
||||||
|
# budget of 121.2 - 67.7 - 20 = 33.5, leaving ~8 GiB of margin, so the pool is
|
||||||
|
# not clamped.
|
||||||
#
|
#
|
||||||
# Measured at 15 before that test: pool reserved at 524288 with no clamp and no
|
# The 2026-09-13 profile for comparison: 103.5 GiB held and "7.1 GiB left for
|
||||||
# WARNING, and the server held 103.5 GiB. The ledger
|
# everything else" (310 contiguous 2 MiB blocks) at the doubled 524,288 pool,
|
||||||
# reads "host memory left for everything else: 310 contiguous 2 MiB blocks
|
# which is why the pool came down; at 401,408 the same ledger read 9.1 GiB /
|
||||||
# (7.1 GiB total)" — against 2081 blocks / 12.1 GiB at the 24-reserve
|
# 1396 blocks. The 09-16 retune gives back the vision tower (1.97) and ~4.2 of
|
||||||
# baseline. All 28 co-tenant containers stayed up (~4.3 GiB working set).
|
# arena on top of that for the co-tenant, so the expected figure at this boot
|
||||||
|
# is ~15 GiB left. Read the ledger; do not trust this arithmetic.
|
||||||
#
|
#
|
||||||
# The price at 524,288 was that the engine volunteered this note on every boot:
|
# To free host memory, change the POOL or the ARENA, and the reserve then
|
||||||
# when it runs out, BOTH they and this server can stall for minutes at a
|
# becomes free to rise: HALOGEN_KV_POOL_POSITIONS=262144 hands back 3.6 GiB,
|
||||||
# time at full CPU with no disk activity and no output, which is not a
|
# HALOGEN_MAX_TOK=16384 hands back another 4.2. Both are one-line edits the
|
||||||
# crash and needs no restart.
|
# fit check confirms on the next boot.
|
||||||
# That is the failure mode to watch, and it is NOT the legible one predicted
|
Environment=HALOGEN_HOST_RESERVE_GIB=20
|
||||||
# above: a minutes-long stall that looks like a hang is harder to diagnose
|
|
||||||
# than a slow prompt. The 09-13 trim to 401,408 was the first application of
|
|
||||||
# the remedy. If it happens again, cut further:
|
|
||||||
# HALOGEN_KV_POOL_POSITIONS=262144 (hands back another 3.6 GiB), or
|
|
||||||
# HALOGEN_MAX_TOK=16384 (hands back 8.4) with reserve then free to rise to 24.
|
|
||||||
Environment=HALOGEN_HOST_RESERVE_GIB=15
|
|
||||||
# Image input (the sidecar ships inside the weights repo).
|
|
||||||
Environment=HALOGEN_VISION_TOWER=1
|
|
||||||
# Verbose engine logging: the vendor's first-line evidence for load,
|
# Verbose engine logging: the vendor's first-line evidence for load,
|
||||||
# allocation and admission questions. Shipping it on means an incident report
|
# allocation and admission questions. Shipping it on means an incident report
|
||||||
# already has the full startup account instead of needing a restart-into-debug.
|
# already has the full startup account instead of needing a restart-into-debug.
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -60,6 +60,7 @@ SERVICE_ORDER = (
|
|||||||
"lidarr",
|
"lidarr",
|
||||||
"readarr",
|
"readarr",
|
||||||
"frank-large",
|
"frank-large",
|
||||||
|
"frank-small",
|
||||||
"frank-medium",
|
"frank-medium",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ DANKCLAW_SERVICES = {
|
|||||||
"caddy",
|
"caddy",
|
||||||
"homepage",
|
"homepage",
|
||||||
"frank-large",
|
"frank-large",
|
||||||
|
"frank-small",
|
||||||
}
|
}
|
||||||
|
|
||||||
DANKSERVER_SERVICES = {
|
DANKSERVER_SERVICES = {
|
||||||
@@ -390,14 +392,12 @@ def services_for_path(path: str, target: str) -> set[str]:
|
|||||||
name = p.name
|
name = p.name
|
||||||
if name in {"homepage-status.py", "homepage-status.service", "homepage-status.timer"}:
|
if name in {"homepage-status.py", "homepage-status.service", "homepage-status.timer"}:
|
||||||
_add(services, "homepage")
|
_add(services, "homepage")
|
||||||
if name in {"frank-large.env", "frank-large.service", "serve-frank-large.sh"}:
|
if name in {"frank-small.env", "frank-small.service", "serve-frank-small.sh"}:
|
||||||
# The ik_llama.cpp runtime (2026-09-15 cutover) keeps its unit,
|
# llama.cpp frank-small (2026-09-16) keeps its unit, env and serve
|
||||||
# env and serve files here; each one selects frank-large only.
|
# files here; each one selects frank-small only. The ik_llama.cpp
|
||||||
_add(services, "frank-large")
|
# trio that lived here for two days is in scripts/.AGENTTRASH/,
|
||||||
if name == "chat-template.jinja":
|
# which is archive material and plans nothing.
|
||||||
# The shared chat template is read by frank-large's serve script
|
_add(services, "frank-small")
|
||||||
# again; the vllm-radiance runtime on Dankserver ships its own.
|
|
||||||
_add(services, "frank-large")
|
|
||||||
# Deployment/test/benchmark scripts and other repo tooling do not
|
# Deployment/test/benchmark scripts and other repo tooling do not
|
||||||
# restart a runtime service.
|
# restart a runtime service.
|
||||||
return services
|
return services
|
||||||
|
|||||||
+91
-37
@@ -90,7 +90,7 @@ service_is_allowed() {
|
|||||||
dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
||||||
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|\
|
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|\
|
||||||
dankclaw:caddy|dankclaw:homepage|dankclaw:seerr|dankclaw:marreta|\
|
dankclaw:caddy|dankclaw:homepage|dankclaw:seerr|dankclaw:marreta|\
|
||||||
dankclaw:frank-large|\
|
dankclaw:frank-large|dankclaw:frank-small|\
|
||||||
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
|
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
|
||||||
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:plex|dankserver:tautulli|\
|
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:plex|dankserver:tautulli|\
|
||||||
dankserver:prowlarr|dankserver:qbittorrent|dankserver:radarr|\
|
dankserver:prowlarr|dankserver:qbittorrent|dankserver:radarr|\
|
||||||
@@ -965,51 +965,104 @@ deploy_anubis() {
|
|||||||
wait_container_healthy anubis 60 || return 1
|
wait_container_healthy anubis 60 || return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# frank-large runs as a ROOT unit again since 2026-09-15: ik_llama.cpp
|
# frank-large runs as a rootless Quadlet unit again since 2026-09-16: the
|
||||||
# (qwen4exp support merged upstream) replaced the halogen-flash-server
|
# halogen-flash-server runtime is back (it replaced ik_llama.cpp, which had
|
||||||
# rootless Quadlet, and the ik server is a native binary — CUDA on the RTX
|
# replaced it), and the RTX 3090 it shared during that interlude now belongs
|
||||||
# 3090 for dense/attention/KV, the Strix Halo CPU path for the MoE experts.
|
# to frank-small.
|
||||||
# There is no container image to pull and no generated unit to avoid:
|
|
||||||
# relink_unit points /etc/systemd/system/frank-large.service at the repo
|
|
||||||
# file, and boot persistence is the unit's [Install] section.
|
|
||||||
#
|
#
|
||||||
# The halogen Quadlet is retired first. Both managers must never know this
|
# TWO MANAGERS, ONE UNIT NAME. The ik incarnation ran as a ROOT unit whose
|
||||||
# unit name at once (they would race for port 7770): stop the user unit,
|
# file is a symlink into this checkout; the halogen unit is a USER (quadlet)
|
||||||
# poll until it is really gone, move the installed .container into the home
|
# unit. Both must never exist at once — they would race for port 7770 — so the
|
||||||
# trash (never rm), then reload the user manager.
|
# root unit is retired first: stop, disable, move the /etc symlink into the
|
||||||
LLM_LARGE_CHECKPOINT="${LLM_LARGE_CHECKPOINT:-/home/dank/models/mradermacher-Qwen3.8-Flash-Next-Uncensored-i1-Q4_K_M/Qwen3.8-Flash-Next-Uncensored.i1-Q4_K_M.gguf}"
|
# home trash (never `rm`), reload the root manager. This host's `systemctl
|
||||||
|
# disable` has been observed to delete the unit file itself (2026-08-26
|
||||||
|
# incident), so the move tolerates a file that is already gone.
|
||||||
|
#
|
||||||
|
# FIRST START fetches ~127 GB of weights, and /health cannot answer while it
|
||||||
|
# does. wait_for_llm_large_ready recognises that (checkpoint absent) and waits
|
||||||
|
# for the file instead of burning the health budget on a failure that is not
|
||||||
|
# one.
|
||||||
|
LLM_LARGE_CHECKPOINT="${LLM_LARGE_CHECKPOINT:-/home/dank/models/halogen/qwen38-flash-next-w4b.hgn}"
|
||||||
|
|
||||||
retire_quadlet_llm_large() {
|
retire_root_llm_large() {
|
||||||
local trash="$HOME/.AGENTTRASH/20260915-ik-swap/halogen-quadlet"
|
local trash="$HOME/.AGENTTRASH/20260916-halogen-restore/host-units"
|
||||||
systemctl --user stop frank-large.service 2>/dev/null || true
|
run_privileged systemctl stop frank-large.service 2>/dev/null || true
|
||||||
# A stopping quadlet runs podman stop (SIGTERM, then SIGKILL at the
|
run_privileged systemctl disable frank-large.service 2>/dev/null || true
|
||||||
# timeout); llama-family engines never preempt an in-flight generation.
|
if [[ -e /etc/systemd/system/frank-large.service || -L /etc/systemd/system/frank-large.service ]]; then
|
||||||
# Poll until the user unit is really inactive before removing its file.
|
|
||||||
local i
|
|
||||||
for ((i = 0; i < 30; i++)); do
|
|
||||||
systemctl --user is-active --quiet frank-large.service 2>/dev/null || break
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
if [[ -e "$HOME/.config/containers/systemd/frank-large.container" || -L "$HOME/.config/containers/systemd/frank-large.container" ]]; then
|
|
||||||
mkdir -p "$trash"
|
mkdir -p "$trash"
|
||||||
mv -- "$HOME/.config/containers/systemd/frank-large.container" \
|
run_privileged mv -- /etc/systemd/system/frank-large.service \
|
||||||
"$trash/frank-large.container"
|
"$trash/frank-large.service"
|
||||||
echo "retired halogen quadlet -> $trash"
|
echo "retired root unit frank-large.service -> $trash"
|
||||||
fi
|
fi
|
||||||
systemctl --user daemon-reload
|
run_privileged systemctl daemon-reload
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy_llm_large() {
|
deploy_llm_large() {
|
||||||
retire_quadlet_llm_large
|
retire_root_llm_large
|
||||||
if [[ ! -f "$LLM_LARGE_CHECKPOINT" ]]; then
|
# Pull once, then run the localhost/-aliased tag: this host's quadlet path
|
||||||
echo "ERROR: checkpoint absent at $LLM_LARGE_CHECKPOINT" >&2
|
# cannot resolve registry-prefixed refs from local storage at create time
|
||||||
echo "frank-large: download the mradermacher i1-Q4_K_M GGUF first; refusing to start a unit that cannot load." >&2
|
# (same workaround as anubis/hermes), and Pull=never keeps boots off the
|
||||||
|
# registry.
|
||||||
|
local image="ghcr.io/peonist-ai/halogen-flash-server:0.5.8"
|
||||||
|
local alias="localhost/peonist-ai/halogen-flash-server:0.5.8"
|
||||||
|
podman pull "$image"
|
||||||
|
podman tag "$image" "$alias"
|
||||||
|
# The weights volume must exist, and the container needs it read-write:
|
||||||
|
# that is where a first start downloads into.
|
||||||
|
mkdir -p /home/dank/models/halogen
|
||||||
|
install_quadlet frank-large dankclaw/frank-large/quadlet frank-large
|
||||||
|
# No enable: generated units cannot be enabled on this host (v1 quadlet
|
||||||
|
# generator); boot persistence comes from [Install] in the unit file.
|
||||||
|
systemctl --user restart frank-large.service
|
||||||
|
wait_for_llm_large_ready
|
||||||
|
wait_for_health frank-large http://127.0.0.1:7770/health 1200
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bootstrap-aware gate: if the checkpoint is absent the container is fetching
|
||||||
|
# it; wait for the file (the fetch resumes, and the unit restarts it on
|
||||||
|
# failure) rather than gating on a /health that cannot answer yet. Reports
|
||||||
|
# success either way, so a first deploy does not look like a failure while the
|
||||||
|
# engine does exactly what it was told to.
|
||||||
|
wait_for_llm_large_ready() {
|
||||||
|
[[ -f "$LLM_LARGE_CHECKPOINT" ]] && return 0
|
||||||
|
echo "frank-large: checkpoint absent at $LLM_LARGE_CHECKPOINT"
|
||||||
|
echo "frank-large: first start — the container is downloading ~127 GB from"
|
||||||
|
echo "frank-large: peonist-ai/halogen-qwen3.8-flash-next (resumes if interrupted)."
|
||||||
|
local i
|
||||||
|
for ((i = 1; i <= 1200; i++)); do
|
||||||
|
if [[ -f "$LLM_LARGE_CHECKPOINT" ]]; then
|
||||||
|
echo "frank-large: weights landed after approximately $((i * 2))s; gating on /health"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "WARN: frank-large download still in progress after 40m — NOT a failure." >&2
|
||||||
|
echo "WARN: the unit keeps fetching; re-run this deploy (or re-check /health) later." >&2
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# deploy_llm_small — frank-small is mainline llama.cpp on the RTX 3090, as a
|
||||||
|
# ROOT unit: the runtime is a native CUDA binary, so there is no container
|
||||||
|
# image to install and no generated unit to avoid. relink_unit points
|
||||||
|
# /etc/systemd/system/frank-small.service at the repo file and boot persistence
|
||||||
|
# is that unit's [Install] section.
|
||||||
|
#
|
||||||
|
# It is a SEPARATE endpoint on port 7771; frank-large keeps 7770. The 3090 is
|
||||||
|
# not shared: frank-large's halogen engine runs on the gfx1151 iGPU.
|
||||||
|
LLM_SMALL_MODEL="${LLM_SMALL_MODEL:-/home/dank/models/Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp/Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp.gguf}"
|
||||||
|
|
||||||
|
deploy_llm_small() {
|
||||||
|
if [[ ! -f "$LLM_SMALL_MODEL" ]]; then
|
||||||
|
echo "ERROR: model absent at $LLM_SMALL_MODEL" >&2
|
||||||
|
echo "frank-small: fetch ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF (the -mtp build plus the mmproj) first;" >&2
|
||||||
|
echo "frank-small: refusing to start a unit that cannot load." >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
relink_unit frank-large
|
relink_unit frank-small
|
||||||
run_privileged systemctl enable frank-large.service
|
run_privileged systemctl daemon-reload
|
||||||
run_privileged systemctl restart frank-large.service
|
run_privileged systemctl enable frank-small.service
|
||||||
wait_for_health frank-large http://127.0.0.1:7770/health 1200
|
run_privileged systemctl restart frank-small.service
|
||||||
|
wait_for_health frank-small http://127.0.0.1:7771/health 900
|
||||||
}
|
}
|
||||||
|
|
||||||
# deploy_llm_medium — frank-medium runs as a rootless Quadlet since the
|
# deploy_llm_medium — frank-medium runs as a rootless Quadlet since the
|
||||||
@@ -1255,6 +1308,7 @@ deploy_service() {
|
|||||||
seerr) deploy_seerr ;;
|
seerr) deploy_seerr ;;
|
||||||
marreta) deploy_marreta ;;
|
marreta) deploy_marreta ;;
|
||||||
frank-large) deploy_llm_large ;;
|
frank-large) deploy_llm_large ;;
|
||||||
|
frank-small) deploy_llm_small ;;
|
||||||
frank-medium) deploy_llm_medium ;;
|
frank-medium) deploy_llm_medium ;;
|
||||||
hermes|hermes-dashboard|hermes-webui) deploy_hermes ;;
|
hermes|hermes-dashboard|hermes-webui) deploy_hermes ;;
|
||||||
romm|romm-mariadb|romm-redis|mariadb|redis) deploy_romm_stack "$service" ;;
|
romm|romm-mariadb|romm-redis|mariadb|redis) deploy_romm_stack "$service" ;;
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ valid_service() {
|
|||||||
dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|dankclaw:loki|dankclaw:alloy|\
|
dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|dankclaw:loki|dankclaw:alloy|\
|
||||||
dankclaw:node-exporter|dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
dankclaw:node-exporter|dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
||||||
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|dankclaw:seerr|\
|
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|dankclaw:seerr|\
|
||||||
dankclaw:frank-large|dankclaw:frank-npu|\
|
dankclaw:frank-large|dankclaw:frank-small|\
|
||||||
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
|
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
|
||||||
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:plex|dankserver:tautulli|\
|
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:plex|dankserver:tautulli|\
|
||||||
dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\
|
dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Tunables for frank-small (ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF, the IQ3_S
|
||||||
|
# build with the MTP head, plus the BF16 vision projector) on mainline
|
||||||
|
# llama.cpp, Dankclaw's RTX 3090, since 2026-09-16.
|
||||||
|
#
|
||||||
|
# Runtime: llama.cpp master 37b53fd (2026-09-16), CUDA build for sm_86:
|
||||||
|
# cmake -B build-cuda -DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=ON \
|
||||||
|
# -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=86
|
||||||
|
# cmake --build build-cuda --target llama-server -j
|
||||||
|
# GGML_CUDA_FA_ALL_QUANTS is deliberately OFF in that build: it is only needed
|
||||||
|
# for QUANTIZED KV with flash attention, and this service runs f16 KV (below).
|
||||||
|
#
|
||||||
|
# Model: ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF, the `-mtp` build of IQ3_S.
|
||||||
|
# GSQ-RCO assigns a separate quantization type to every tensor from a
|
||||||
|
# gradient-based search under a total size budget, rather than applying one
|
||||||
|
# type throughout; IQ3_S at 3.50 bpw is the release's task-lossless operating
|
||||||
|
# point (AIME25 100.00 and LiveCodeBench v6 85.71 match BF16 exactly,
|
||||||
|
# GPQA-Diamond 0.51 behind, at 11.8 GB against 53.8).
|
||||||
|
#
|
||||||
|
# WHY THAT FILE: the `-mtp` build is ~0.35 GB larger because it carries the
|
||||||
|
# model's Multi-Token Prediction head as GGUF block `blk.64.nextn.*`
|
||||||
|
# (qwen35.nextn_predict_layers = 1). llama.cpp loads it from the SAME file --
|
||||||
|
# `--spec-type draft-mtp` sets load_mtp, and the draft context runs on the main
|
||||||
|
# model's weights (common/common.cpp passes shares_model when there is no
|
||||||
|
# --model-draft; speculative.cpp gives it LLAMA_CONTEXT_TYPE_MTP), so there is
|
||||||
|
# no second 12 GB copy in VRAM. MTP is therefore a property of the FILE, not of
|
||||||
|
# the flags: the plain IQ3_S build has no MTP tensors to load.
|
||||||
|
#
|
||||||
|
# Architecture (GGUF arch qwen35): 64 layers laid out as 16 x (3 x (Gated
|
||||||
|
# DeltaNet -> FFN) + 1 x (Gated Attention -> FFN)) -- 48 linear-attention
|
||||||
|
# layers with CONSTANT-SIZE per-sequence state and 16 full-attention layers,
|
||||||
|
# 4 KV heads x 256 head_dim. Only the attention layers take KV, so a token
|
||||||
|
# costs 2 (K,V) x 4 heads x 256 x 2 B = 64 KiB at f16 (34 KiB at q8_0, 18 KiB
|
||||||
|
# at q4_0), and the DeltaNet layers add ~0.16 GiB per sequence in f32 at any
|
||||||
|
# depth.
|
||||||
|
#
|
||||||
|
# SIZING (RTX 3090, 24576 MiB) -- modelled, and the loader's own ledger
|
||||||
|
# ("KV self size", "compute buffer size") outranks it:
|
||||||
|
# weights 11.29 GiB (12,120,016,960 B)
|
||||||
|
# mmproj 0.87 GiB (931,146,528 B, 27 blocks)
|
||||||
|
# MTP draft KV 0.50 GiB (its context is n_ctx long
|
||||||
|
# over one nextn layer: 4 KiB/token)
|
||||||
|
# DeltaNet state 0.16 GiB (one sequence)
|
||||||
|
# CUDA context + prefill/decode bufs ~1.5 GiB
|
||||||
|
# KV at CTX_SIZE 8.00 GiB (131,072 x 64 KiB)
|
||||||
|
# --------
|
||||||
|
# ~22.3 GiB of 24, ~2 GiB of slack
|
||||||
|
# The binding number is the KV: ~9.5 GiB is left after the fixed terms, which
|
||||||
|
# at 64 KiB/token is ~155k tokens across all slots. The deployed shape is one
|
||||||
|
# slot at 131,072, full f16 precision, no KV quantization risk, no rebuild.
|
||||||
|
# Alternatives inside the same budget: 2 x 65536 at f16; 4 x 65536 or
|
||||||
|
# 1 x 262144 at q8_0, both of which need a rebuild with
|
||||||
|
# -DGGML_CUDA_FA_ALL_QUANTS=ON (without it quantized-KV prefill collapses
|
||||||
|
# ~12x on CUDA -- see the CUDA lesson in AGENTS.md).
|
||||||
|
# --parallel N DIVIDES --ctx-size between slots; it does not multiply. One slot
|
||||||
|
# means a second concurrent request queues behind the first.
|
||||||
|
#
|
||||||
|
# Sampling: greedy by default, which is the fleet contract (a request that
|
||||||
|
# sends no temperature decodes greedy; a client that sends one gets its own).
|
||||||
|
# Qwen's own agentic evaluations used temperature 1.0 / top_p 0.95, and the
|
||||||
|
# harness applies exactly that per model in models.yml (extraBody), the same
|
||||||
|
# way it did for the earlier frank-large runtime.
|
||||||
|
#
|
||||||
|
# WARNING: do not name any variable TEMP, HOST, or GPU_MAX_HEAP_SIZE (HIP reads
|
||||||
|
# those bare vars on the ROCm hosts -- see the 2026-08-13 incident in
|
||||||
|
# AGENTS.md). This box is CUDA, but the rule keeps env files portable.
|
||||||
|
|
||||||
|
# llama.cpp runtime binary: mainline master 37b53fd, CUDA sm_86.
|
||||||
|
LLAMA_BIN=/home/dank/GitPackages/llama.cpp-mainline/build-cuda/bin/llama-server
|
||||||
|
|
||||||
|
# Model package. The -mtp file is required for speculative decoding.
|
||||||
|
MODEL_DIR=/home/dank/models/Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp
|
||||||
|
MODEL_FILE=Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp.gguf
|
||||||
|
MMPROJ_PATH=/home/dank/models/Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp/mmproj-Qwen3.8-27B-BF16.gguf
|
||||||
|
|
||||||
|
# Fleet identity and network. 7771 is this service's own endpoint on Dankclaw;
|
||||||
|
# 7770 stays frank-large's. The alias is the id at /v1/models and what clients
|
||||||
|
# ask for -- the harness entry in models.yml names it.
|
||||||
|
ALIAS=frank-small
|
||||||
|
BIND_ADDR=0.0.0.0
|
||||||
|
LISTEN_PORT=7771
|
||||||
|
|
||||||
|
# One slot at 131,072 tokens with f16 KV (see the sizing note above).
|
||||||
|
CTX_SIZE=131072
|
||||||
|
PARALLEL=1
|
||||||
|
CACHE_TYPE_K=f16
|
||||||
|
CACHE_TYPE_V=f16
|
||||||
|
|
||||||
|
# Batch geometry: llama.cpp's own defaults (2048 logical / 512 physical). The
|
||||||
|
# prefill wall clock at this model size is set by the card, not by ubatch, and
|
||||||
|
# the small ubatch keeps the DeltaNet chunk buffers small. Raise UBATCH_SIZE
|
||||||
|
# only after reading the compute buffer size the loader reports.
|
||||||
|
BATCH_SIZE=2048
|
||||||
|
UBATCH_SIZE=512
|
||||||
|
|
||||||
|
# CPU threads: this service runs entirely on the GPU (NGL=999), so threads
|
||||||
|
# cover tokenization, sampling and the HTTP layer only. Deliberately modest --
|
||||||
|
# this host's CPU is shared with halogen's engine, which is CPU-hungry.
|
||||||
|
NUM_THREADS=8
|
||||||
|
THREAD_BATCH=8
|
||||||
|
|
||||||
|
# Every layer on the 3090, with flash attention.
|
||||||
|
NGL=999
|
||||||
|
FLASH_ATTN=on
|
||||||
|
|
||||||
|
# Speculative decoding from the model's own MTP head (see the model note).
|
||||||
|
# Draft depth stays llama.cpp's default of 3 tokens.
|
||||||
|
SPEC_TYPE=draft-mtp
|
||||||
|
|
||||||
|
# Greedy default (fleet contract); reasoning is extracted into
|
||||||
|
# reasoning_content with an unlimited budget (the model decides).
|
||||||
|
TEMPERATURE=0.0
|
||||||
|
REASONING_FORMAT=deepseek
|
||||||
|
REASONING_BUDGET=-1
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=llama.cpp Qwen3.8-27B GSQ-RCO IQ3_S MTP+vision (dankclaw, frank-small)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=dank
|
||||||
|
Group=dank
|
||||||
|
# CUDA pins its staging buffers, so the process needs the memlock budget even
|
||||||
|
# though the weights themselves are copied into VRAM. Parity with the halogen
|
||||||
|
# quadlet's Ulimit=memlock=-1:-1 and the retired ik unit.
|
||||||
|
LimitMEMLOCK=infinity
|
||||||
|
Environment="FRANK_SMALL_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-small.env"
|
||||||
|
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-small.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Executable
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Serve ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp as frank-small on Dankclaw's
|
||||||
|
# RTX 3090 via mainline llama.cpp.
|
||||||
|
#
|
||||||
|
# Topology: the whole model lives in VRAM (-ngl 999) -- 11.29 GiB of IQ3_S-mtp
|
||||||
|
# weights, 0.87 GiB of BF16 vision projector and ~8 GiB of f16 KV inside the
|
||||||
|
# card's 24 GiB. The host CPU only tokenizes and samples here; this box's CPU
|
||||||
|
# belongs to the halogen engine that serves frank-large.
|
||||||
|
#
|
||||||
|
# MTP: the `-mtp` build carries the NextN block (blk.64.nextn.*), and
|
||||||
|
# `--spec-type draft-mtp` makes llama.cpp load it and run the draft context on
|
||||||
|
# the SAME weights. There is no --model-draft here on purpose: a separate draft
|
||||||
|
# path would load a second 12 GB copy of a 12 GB model. Draft depth is
|
||||||
|
# llama.cpp's default (--spec-draft-n-max 3).
|
||||||
|
#
|
||||||
|
# Vision: --mmproj loads the BF16 projector, so image content parts work. The
|
||||||
|
# fleet's image path moved here from frank-large on 2026-09-16, which is why
|
||||||
|
# halogen no longer sets HALOGEN_VISION_TOWER.
|
||||||
|
#
|
||||||
|
# Sampling: greedy by default (the fleet contract). Clients that send their own
|
||||||
|
# temperature get their own values, and the harness overrides per model from
|
||||||
|
# models.yml. --jinja is pinned even though this build defaults it on: the
|
||||||
|
# model's own template supplies reasoning_effort, preserve_thinking and the
|
||||||
|
# tool-call grammar, and a future build that flips that default must not
|
||||||
|
# silently take tools away.
|
||||||
|
#
|
||||||
|
# Tunables come from frank-small.env unless FRANK_SMALL_ENV_FILE overrides it.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${FRANK_SMALL_ENV_FILE:-$SCRIPT_DIR/frank-small.env}"
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- defaults (used when the env file does not set them) -------------------
|
||||||
|
: "${LLAMA_BIN:=/home/dank/GitPackages/llama.cpp-mainline/build-cuda/bin/llama-server}"
|
||||||
|
: "${MODEL_DIR:=/home/dank/models/Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp}"
|
||||||
|
: "${MODEL_FILE:=Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp.gguf}"
|
||||||
|
: "${MMPROJ_PATH:=$MODEL_DIR/mmproj-Qwen3.8-27B-BF16.gguf}"
|
||||||
|
: "${ALIAS:=frank-small}"
|
||||||
|
: "${BIND_ADDR:=0.0.0.0}"
|
||||||
|
: "${LISTEN_PORT:=7771}"
|
||||||
|
: "${CTX_SIZE:=131072}"
|
||||||
|
: "${PARALLEL:=1}"
|
||||||
|
: "${CACHE_TYPE_K:=f16}"
|
||||||
|
: "${CACHE_TYPE_V:=f16}"
|
||||||
|
: "${BATCH_SIZE:=2048}"
|
||||||
|
: "${UBATCH_SIZE:=512}"
|
||||||
|
: "${NUM_THREADS:=8}"
|
||||||
|
: "${THREAD_BATCH:=8}"
|
||||||
|
: "${NGL:=999}"
|
||||||
|
: "${FLASH_ATTN:=on}"
|
||||||
|
: "${SPEC_TYPE:=draft-mtp}"
|
||||||
|
: "${TEMPERATURE:=0.0}"
|
||||||
|
: "${REASONING_FORMAT:=deepseek}"
|
||||||
|
: "${REASONING_BUDGET:=-1}"
|
||||||
|
|
||||||
|
# Fail loudly rather than half-loading a model into VRAM: a missing file here
|
||||||
|
# is a download that has not finished, and the unit's restart policy would
|
||||||
|
# otherwise turn that into a crash loop that reads like a runtime fault.
|
||||||
|
for path in "$LLAMA_BIN" "$MODEL_DIR/$MODEL_FILE" "$MMPROJ_PATH"; do
|
||||||
|
[[ -e "$path" ]] || { echo "frank-small: missing $path" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
# The UVM device node is created lazily by nvidia-modprobe on the first CUDA
|
||||||
|
# open after boot; make sure it exists before the server needs it.
|
||||||
|
nvidia-modprobe -u -c=0 2>/dev/null || true
|
||||||
|
|
||||||
|
MMPROJ_ARGS=()
|
||||||
|
if [[ -n "${MMPROJ_PATH:-}" && -f "$MMPROJ_PATH" ]]; then
|
||||||
|
MMPROJ_ARGS=(--mmproj "$MMPROJ_PATH")
|
||||||
|
fi
|
||||||
|
|
||||||
|
SPEC_ARGS=()
|
||||||
|
if [[ -n "${SPEC_TYPE:-}" && "$SPEC_TYPE" != "none" ]]; then
|
||||||
|
SPEC_ARGS=(--spec-type "$SPEC_TYPE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$LLAMA_BIN" \
|
||||||
|
--model "$MODEL_DIR/$MODEL_FILE" \
|
||||||
|
"${MMPROJ_ARGS[@]}" \
|
||||||
|
--alias "$ALIAS" \
|
||||||
|
--host "$BIND_ADDR" \
|
||||||
|
--port "$LISTEN_PORT" \
|
||||||
|
--gpu-layers "$NGL" \
|
||||||
|
--flash-attn "$FLASH_ATTN" \
|
||||||
|
--ctx-size "$CTX_SIZE" \
|
||||||
|
--parallel "$PARALLEL" \
|
||||||
|
--batch-size "$BATCH_SIZE" \
|
||||||
|
--ubatch-size "$UBATCH_SIZE" \
|
||||||
|
--threads "$NUM_THREADS" \
|
||||||
|
--threads-batch "$THREAD_BATCH" \
|
||||||
|
--cache-type-k "$CACHE_TYPE_K" \
|
||||||
|
--cache-type-v "$CACHE_TYPE_V" \
|
||||||
|
"${SPEC_ARGS[@]}" \
|
||||||
|
--jinja \
|
||||||
|
--metrics \
|
||||||
|
--temp "$TEMPERATURE" \
|
||||||
|
--reasoning-format "$REASONING_FORMAT" \
|
||||||
|
--reasoning-budget "$REASONING_BUDGET"
|
||||||
+15
-4
@@ -145,11 +145,12 @@ for path in "${CHANGED[@]}"; do
|
|||||||
add_test tests/homepage_caddy.bats
|
add_test tests/homepage_caddy.bats
|
||||||
add_test tests/deployment_scoping.bats
|
add_test tests/deployment_scoping.bats
|
||||||
;;
|
;;
|
||||||
# frank-large moved to a rootless quadlet (halogen-flash-server) on
|
# frank-large is a rootless quadlet again since 2026-09-16 (the
|
||||||
# 2026-09-11; its unit carries the fleet's GPU and podman posture, so
|
# halogen-flash-server restore). Its unit carries the fleet's GPU and
|
||||||
# it gets its own seam rather than the generic fallback below.
|
# podman posture, so it gets its own seam rather than the generic
|
||||||
|
# fallback below.
|
||||||
dankclaw/frank-large/*)
|
dankclaw/frank-large/*)
|
||||||
add_test tests/frank_large_quadlet.bats
|
add_test tests/frank_large_unit.bats
|
||||||
add_test tests/deployment_scoping.bats
|
add_test tests/deployment_scoping.bats
|
||||||
add_test tests/docs_completeness.bats
|
add_test tests/docs_completeness.bats
|
||||||
;;
|
;;
|
||||||
@@ -187,6 +188,16 @@ for path in "${CHANGED[@]}"; do
|
|||||||
add_test tests/deployment_scoping.bats
|
add_test tests/deployment_scoping.bats
|
||||||
add_test tests/docs_completeness.bats
|
add_test tests/docs_completeness.bats
|
||||||
;;
|
;;
|
||||||
|
# frank-small is llama.cpp on the 3090 as a ROOT unit: a native CUDA
|
||||||
|
# binary, so no generated quadlet is involved. Its env, unit and serve
|
||||||
|
# script all live in scripts/, which is why this case has to precede
|
||||||
|
# the generic scripts/*.service and scripts/* fallbacks below.
|
||||||
|
scripts/frank-small.*|scripts/serve-frank-small.sh)
|
||||||
|
add_test tests/frank_small_unit.bats
|
||||||
|
add_test tests/llm_units.bats
|
||||||
|
add_test tests/deployment_scoping.bats
|
||||||
|
add_test tests/scripts_syntax.bats
|
||||||
|
;;
|
||||||
scripts/*.service)
|
scripts/*.service)
|
||||||
add_test tests/llm_units.bats
|
add_test tests/llm_units.bats
|
||||||
add_test tests/scripts_syntax.bats
|
add_test tests/scripts_syntax.bats
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
|||||||
|
|
||||||
@test "deployment planner maps large-model changes to the large model only" {
|
@test "deployment planner maps large-model changes to the large model only" {
|
||||||
run python3 "$PLANNER" --target dankclaw --paths \
|
run python3 "$PLANNER" --target dankclaw --paths \
|
||||||
scripts/frank-large.env scripts/frank-large.service scripts/serve-frank-large.sh
|
dankclaw/frank-large/quadlet/frank-large.container
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[ "$output" = "frank-large" ]
|
[ "$output" = "frank-large" ]
|
||||||
}
|
}
|
||||||
@@ -28,14 +28,18 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
|||||||
! grep -q 'frank_npu\|frank-npu' "$REMOTE_DEPLOY"
|
! grep -q 'frank_npu\|frank-npu' "$REMOTE_DEPLOY"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "chat-template changes plan frank-large: the ik runtime reads the tracked template" {
|
@test "the retired ik chat template plans nothing, and its archive is still here" {
|
||||||
# ik_llama.cpp (2026-09-15 cutover) serves the Qwen-Sharp template from
|
# scripts/chat-template.jinja existed only for the ik_llama.cpp runtime
|
||||||
# the tracked scripts/chat-template.jinja again. The vllm-radiance
|
# (retired 2026-09-16 in favour of halogen-flash-server again). The
|
||||||
# runtime on Dankserver ships its own qwen-fixed-v22.3.jinja out of the
|
# llama.cpp frank-small runtime serves the template the GGUF itself
|
||||||
# pinned clone, so a template edit deploys nothing there.
|
# carries, which is the one with reasoning_effort, preserve_thinking and
|
||||||
|
# the tool-call grammar. Editing the archive must deploy nothing -- and the
|
||||||
|
# archive must still be present, so this cannot pass by the file having
|
||||||
|
# vanished.
|
||||||
|
[ -f "$REPO_ROOT/scripts/.AGENTTRASH/chat-template.jinja.ik-llama-qwen-sharp" ]
|
||||||
run python3 "$PLANNER" --target dankclaw --paths scripts/chat-template.jinja
|
run python3 "$PLANNER" --target dankclaw --paths scripts/chat-template.jinja
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[ "$output" = "frank-large" ]
|
[ -z "$output" ]
|
||||||
|
|
||||||
run python3 "$PLANNER" --target dankserver --paths scripts/chat-template.jinja
|
run python3 "$PLANNER" --target dankserver --paths scripts/chat-template.jinja
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
@@ -51,9 +55,19 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
|||||||
[ "$output" = "frank-medium" ]
|
[ "$output" = "frank-medium" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "the retired small endpoint is absent from the deployment inventory" {
|
@test "the small endpoint is Dankclaw's again, and Dankserver's copy stays retired" {
|
||||||
! grep -q '"frank-small"' "$PLANNER"
|
run python3 "$PLANNER" --target dankclaw --paths \
|
||||||
! grep -qE 'frank-small\) deploy|dankserver:frank-small' "$REMOTE_DEPLOY" "$REPO_ROOT/scripts/deploy.sh"
|
scripts/frank-small.env scripts/frank-small.service scripts/serve-frank-small.sh
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "frank-small" ]
|
||||||
|
|
||||||
|
# frank-small runs on Dankclaw on the RTX 3090; the Dankserver endpoint of
|
||||||
|
# that name was retired with the FreeToken runtime on 2026-09-15 and must
|
||||||
|
# not come back through a planner mapping.
|
||||||
|
run python3 "$PLANNER" --target dankserver --paths \
|
||||||
|
scripts/frank-small.env scripts/frank-small.service scripts/serve-frank-small.sh
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ -z "$output" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "documentation-only changes plan no runtime deployment" {
|
@test "documentation-only changes plan no runtime deployment" {
|
||||||
|
|||||||
+79
-61
@@ -1,86 +1,104 @@
|
|||||||
#!/usr/bin/env bats
|
#!/usr/bin/env bats
|
||||||
# Seam: the frank-large ik_llama.cpp root unit on dankclaw.
|
# Seam: the frank-large halogen-flash-server quadlet on dankclaw.
|
||||||
# ik_llama.cpp replaced the halogen rootless Quadlet on 2026-09-15. This seam
|
# halogen-flash-server replaced ik_llama.cpp again on 2026-09-16, so this
|
||||||
# pins the deployment-policy properties that no reviewer can see by reading
|
# service is once more a rootless Quadlet container on the gfx1151 iGPU. This
|
||||||
# the flags alone: a root unit whose files all live in this repo, the hybrid
|
# seam pins what a flag list cannot show: the boot-persistent podman posture
|
||||||
# placement contract (experts + PLE mmap'd on the CPU path — NEVER the
|
# (both device nodes, memlock, the create-time image contract), the
|
||||||
# CUDA_Host pinned buffer), no pinned CUDA host memory, the single client
|
# matched ctx/pool/slots set that IS the two-conversation promise, the
|
||||||
# port every consumer already targets, the fleet alias, greedy-by-default
|
# co-tenancy budget the arena and reserve are chosen for, the image handoff to
|
||||||
# sampling, vision via the mtmd mmproj, and a deploy handler that retires
|
# frank-small, and a deploy handler that retires the previous ROOT unit without
|
||||||
# the halogen quadlet without deleting it.
|
# deleting it.
|
||||||
|
|
||||||
load helpers
|
load helpers
|
||||||
|
|
||||||
UNIT="$REPO_ROOT/scripts/frank-large.service"
|
QUADLET="$REPO_ROOT/dankclaw/frank-large/quadlet/frank-large.container"
|
||||||
SERVE="$REPO_ROOT/scripts/serve-frank-large.sh"
|
|
||||||
ENV_FILE="$REPO_ROOT/scripts/frank-large.env"
|
|
||||||
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
||||||
|
|
||||||
@test "frank-large unit, serve script and env exist in scripts/" {
|
@test "the frank-large quadlet exists where the deployer installs it from" {
|
||||||
[ -f "$UNIT" ]
|
[ -f "$QUADLET" ]
|
||||||
[ -f "$SERVE" ]
|
|
||||||
[ -f "$ENV_FILE" ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "frank-large unit is boot-persistent, self-healing and unprivileged" {
|
@test "frank-large is boot-persistent, self-healing and container-shaped" {
|
||||||
grep -q '^WantedBy=multi-user.target$' "$UNIT"
|
grep -q '^WantedBy=default.target$' "$QUADLET"
|
||||||
grep -q '^Restart=on-failure$' "$UNIT"
|
grep -q '^Restart=on-failure$' "$QUADLET"
|
||||||
grep -q '^RestartSec=' "$UNIT"
|
grep -q '^RestartSec=' "$QUADLET"
|
||||||
grep -q '^User=dank$' "$UNIT"
|
grep -q '^Pull=never$' "$QUADLET"
|
||||||
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
|
# The localhost/-aliased tag is what create-time resolution needs on this
|
||||||
|
# host; the deployer pulls the ghcr.io ref and re-tags it.
|
||||||
|
grep -q '^Image=localhost/peonist-ai/halogen-flash-server:' "$QUADLET"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "frank-large unit points at the repo serve script and env" {
|
@test "frank-large has the gfx1151 devices and the image's documented posture" {
|
||||||
grep -q 'ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-large.sh$' "$UNIT"
|
grep -q '^AddDevice=/dev/kfd$' "$QUADLET"
|
||||||
grep -q 'FRANK_LARGE_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-large.env' "$UNIT"
|
grep -q '^AddDevice=/dev/dri$' "$QUADLET"
|
||||||
|
grep -q '^PodmanArgs=--ipc=host --security-opt seccomp=unconfined$' "$QUADLET"
|
||||||
|
grep -q '^Ulimit=memlock=-1:-1$' "$QUADLET"
|
||||||
|
# The weights mount must stay read-write: the first start downloads into it.
|
||||||
|
grep -q '^Volume=/home/dank/models/halogen:/models$' "$QUADLET"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "frank-large places experts and PLE on the mmap'd CPU path, never the pinned buffer" {
|
@test "frank-large keeps the fleet alias and publishes only the API port" {
|
||||||
# --cpu-moe routes CPU-overridden weights through the CUDA_Host PINNED
|
grep -q '^Environment=HALOGEN_MODEL_ID=frank-large$' "$QUADLET"
|
||||||
# buffer (108.12 GiB measured on 2026-09-15; it evicted every co-tenant
|
grep -q '^PublishPort=7770:8731$' "$QUADLET"
|
||||||
# into swap and starved sshd into unreachability). The -ot ...=CPU plain
|
# The engine's own listener stays inside the container: it is unauthenticated.
|
||||||
# CPU backend buffer is the mmap-backed path.
|
! grep -q 'PublishPort=.*8730' "$QUADLET"
|
||||||
grep -q -- '-ot "exps=CPU" -ot "per_layer_token_embd=CPU"' "$SERVE"
|
|
||||||
grep -q '^GGML_CUDA_NO_PINNED_WEIGHTS=1$' "$ENV_FILE"
|
|
||||||
# The serve script refuses to run without the no-pinned policy.
|
|
||||||
grep -q 'GGML_CUDA_NO_PINNED_WEIGHTS=1 is mandatory on this host' "$SERVE"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "frank-large serves the ik runtime from the unified-memory hybrid layout" {
|
@test "the KV pool is exactly two conversations of the per-request ceiling" {
|
||||||
grep -q 'ik_llama.cpp/build/bin/llama-server' "$ENV_FILE"
|
local ctx pool slots
|
||||||
grep -q 'mradermacher-Qwen3.8-Flash-Next-Uncensored-i1-Q4_K_M' "$ENV_FILE"
|
ctx="$(sed -nE 's/^Environment=HALOGEN_CTX=([0-9]+)$/\1/p' "$QUADLET")"
|
||||||
grep -q -- '--gpu-layers "$NGL"' "$SERVE"
|
pool="$(sed -nE 's/^Environment=HALOGEN_KV_POOL_POSITIONS=([0-9]+)$/\1/p' "$QUADLET")"
|
||||||
grep -q -- '--flash-attn "$FLASH_ATTN"' "$SERVE"
|
slots="$(sed -nE 's/^Environment=HALOGEN_KV_SLOTS=([0-9]+)$/\1/p' "$QUADLET")"
|
||||||
|
[ -n "$ctx" ] && [ -n "$pool" ] && [ -n "$slots" ]
|
||||||
|
# The deployed promise is two resident conversations: the pool is twice the
|
||||||
|
# ceiling, one slot per conversation. Slots still oversubscribe the pool for
|
||||||
|
# any real request, because admission reserves prompt + max_tokens.
|
||||||
|
[ "$pool" -eq $((ctx * 2)) ]
|
||||||
|
[ "$slots" -eq 2 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "frank-large keeps the fleet identity and the client port" {
|
@test "the prefill arena stays inside the range the vendor tested" {
|
||||||
grep -q '^LISTEN_PORT=7770$' "$ENV_FILE"
|
local max_tok reserve
|
||||||
grep -q '^ALIAS=frank-large$' "$ENV_FILE"
|
max_tok="$(sed -nE 's/^Environment=HALOGEN_MAX_TOK=([0-9]+)$/\1/p' "$QUADLET")"
|
||||||
grep -q -- '--port "$LISTEN_PORT"' "$SERVE"
|
reserve="$(sed -nE 's/^Environment=HALOGEN_HOST_RESERVE_GIB=([0-9]+)$/\1/p' "$QUADLET")"
|
||||||
grep -q -- '--host "$BIND_ADDR"' "$SERVE"
|
[ -n "$max_tok" ] && [ -n "$reserve" ]
|
||||||
|
# 32768 is the vendor default; 65536 cannot boot on this host because the
|
||||||
|
# arena alone outgrows the device budget. The arena is a per-call prefill
|
||||||
|
# chunk, so a smaller value costs prefill speed, not context.
|
||||||
|
[ "$max_tok" -le 32768 ]
|
||||||
|
[ "$max_tok" -ge 16384 ]
|
||||||
|
[ "$reserve" -ge 15 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "frank-large decodes greedy by default (halogen client contract)" {
|
@test "frank-large loads no vision tower: the fleet's image path is frank-small" {
|
||||||
grep -q '^TEMPERATURE=0.0$' "$ENV_FILE"
|
! grep -q 'HALOGEN_VISION_TOWER' "$QUADLET"
|
||||||
grep -q -- '--temp "$TEMPERATURE"' "$SERVE"
|
grep -q -- '--mmproj' "$REPO_ROOT/scripts/serve-frank-small.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "frank-large keeps image input via the mtmd mmproj" {
|
@test "the deploy handler retires the previous ROOT unit without deleting it" {
|
||||||
grep -q '^MMPROJ_PATH=.*mmproj-f16.gguf$' "$ENV_FILE"
|
grep -q 'retire_root_llm_large' "$REMOTE_DEPLOY"
|
||||||
grep -q -- '--mmproj "$MMPROJ_PATH"' "$SERVE"
|
grep -q 'systemctl stop frank-large.service' "$REMOTE_DEPLOY"
|
||||||
}
|
grep -q 'systemctl disable frank-large.service' "$REMOTE_DEPLOY"
|
||||||
|
|
||||||
@test "the deploy handler retires the halogen quadlet without deleting it" {
|
|
||||||
grep -q 'retire_quadlet_llm_large' "$REMOTE_DEPLOY"
|
|
||||||
grep -q 'systemctl --user stop frank-large.service' "$REMOTE_DEPLOY"
|
|
||||||
grep -q 'frank-large.container' "$REMOTE_DEPLOY"
|
|
||||||
grep -q 'HOME/.AGENTTRASH' "$REMOTE_DEPLOY"
|
grep -q 'HOME/.AGENTTRASH' "$REMOTE_DEPLOY"
|
||||||
! grep -qE '(^|[^[:alnum:]_])rm (-[a-zA-Z]+ )?.*frank-large' "$REMOTE_DEPLOY"
|
! grep -qE '(^|[^[:alnum:]_])rm (-[a-zA-Z]+ )?.*frank-large' "$REMOTE_DEPLOY"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "the deploy handler installs the root unit and refuses a missing checkpoint" {
|
@test "the deploy handler installs the quadlet and gates the first start on weights" {
|
||||||
grep -q 'relink_unit frank-large' "$REMOTE_DEPLOY"
|
grep -q 'install_quadlet frank-large dankclaw/frank-large/quadlet frank-large' "$REMOTE_DEPLOY"
|
||||||
grep -q 'systemctl enable frank-large.service' "$REMOTE_DEPLOY"
|
grep -q 'wait_for_llm_large_ready' "$REMOTE_DEPLOY"
|
||||||
grep -q 'checkpoint absent' "$REMOTE_DEPLOY"
|
grep -q 'qwen38-flash-next-w4b.hgn' "$REMOTE_DEPLOY"
|
||||||
! grep -q 'install_quadlet frank-large' "$REMOTE_DEPLOY"
|
grep -q '127.0.0.1:7770/health' "$REMOTE_DEPLOY"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the ik_llama.cpp runtime that held this service is archived, not live" {
|
||||||
|
# Its files must not sit in scripts/ where a stale root unit could still be
|
||||||
|
# pointed at them, and must not be gone either: .AGENTTRASH is the rollback.
|
||||||
|
local retired
|
||||||
|
for retired in frank-large.env.ik-llama.cpp frank-large.service.ik-llama.cpp \
|
||||||
|
serve-frank-large.sh.ik-llama.cpp; do
|
||||||
|
[ -f "$REPO_ROOT/scripts/.AGENTTRASH/$retired" ]
|
||||||
|
done
|
||||||
|
[ ! -e "$REPO_ROOT/scripts/frank-large.env" ]
|
||||||
|
[ ! -e "$REPO_ROOT/scripts/frank-large.service" ]
|
||||||
|
[ ! -e "$REPO_ROOT/scripts/serve-frank-large.sh" ]
|
||||||
}
|
}
|
||||||
Executable
+106
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
# Seam: the frank-small mainline-llama.cpp root unit on dankclaw.
|
||||||
|
# frank-small is the RTX 3090 endpoint created on 2026-09-16, when frank-large
|
||||||
|
# went back to the halogen engine on the iGPU. This seam pins what the flags
|
||||||
|
# alone do not say: boot persistence for a NATIVE runtime (no generated quadlet
|
||||||
|
# is involved), the repo-relative paths the unit executes, the separate client
|
||||||
|
# port, the MTP contract (the head lives in the weights file, and no second copy
|
||||||
|
# of the model is loaded for it), the vision handoff from frank-large, and the
|
||||||
|
# KV budget that has to fit a 24 GiB card beside 12 GiB of weights.
|
||||||
|
|
||||||
|
load helpers
|
||||||
|
|
||||||
|
UNIT="$REPO_ROOT/scripts/frank-small.service"
|
||||||
|
SERVE="$REPO_ROOT/scripts/serve-frank-small.sh"
|
||||||
|
ENV_FILE="$REPO_ROOT/scripts/frank-small.env"
|
||||||
|
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
||||||
|
QUADLET_LARGE="$REPO_ROOT/dankclaw/frank-large/quadlet/frank-large.container"
|
||||||
|
|
||||||
|
@test "frank-small unit, serve script and env exist in scripts/" {
|
||||||
|
[ -f "$UNIT" ]
|
||||||
|
[ -f "$SERVE" ]
|
||||||
|
[ -f "$ENV_FILE" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "frank-small is boot-persistent, self-healing and unprivileged" {
|
||||||
|
grep -q '^WantedBy=multi-user.target$' "$UNIT"
|
||||||
|
grep -q '^Restart=on-failure$' "$UNIT"
|
||||||
|
grep -q '^RestartSec=' "$UNIT"
|
||||||
|
grep -q '^User=dank$' "$UNIT"
|
||||||
|
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "frank-small runs the repo's own serve script with the repo's env" {
|
||||||
|
grep -q '^ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-small.sh$' "$UNIT"
|
||||||
|
grep -q 'FRANK_SMALL_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-small.env' "$UNIT"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "frank-small offloads every layer to the 3090 with flash attention" {
|
||||||
|
grep -q 'llama.cpp-mainline/build-cuda/bin/llama-server' "$ENV_FILE"
|
||||||
|
grep -q '^NGL=999$' "$ENV_FILE"
|
||||||
|
grep -q -- '--gpu-layers "$NGL"' "$SERVE"
|
||||||
|
grep -q '^FLASH_ATTN=on$' "$ENV_FILE"
|
||||||
|
grep -q -- '--flash-attn "$FLASH_ATTN"' "$SERVE"
|
||||||
|
# Prometheus scrapes this endpoint; without --metrics there is no /metrics.
|
||||||
|
grep -q -- '--metrics' "$SERVE"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "frank-small's MTP head comes from the weights file, not a second copy" {
|
||||||
|
grep -q '^SPEC_TYPE=draft-mtp$' "$ENV_FILE"
|
||||||
|
grep -q -- '--spec-type "$SPEC_TYPE"' "$SERVE"
|
||||||
|
# The -mtp build is the one carrying blk.64.nextn.*; the plain IQ3_S has no
|
||||||
|
# MTP tensors at all, so MTP is a property of the file, not of the flags.
|
||||||
|
grep -q '^MODEL_FILE=.*-mtp\.gguf$' "$ENV_FILE"
|
||||||
|
# A separate draft model would load a second 12 GiB copy of a 12 GiB model.
|
||||||
|
! grep -q -- '--model-draft' "$SERVE"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "frank-small carries the fleet's image path, and only it" {
|
||||||
|
grep -q '^MMPROJ_PATH=.*mmproj-Qwen3\.8-27B-BF16\.gguf$' "$ENV_FILE"
|
||||||
|
grep -q -- '--mmproj "$MMPROJ_PATH"' "$SERVE"
|
||||||
|
! grep -q 'HALOGEN_VISION_TOWER' "$QUADLET_LARGE"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "frank-small's KV fits the card beside the weights it loads" {
|
||||||
|
local ctx parallel parallel_slots cache_k cache_v
|
||||||
|
ctx="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
|
||||||
|
parallel="$(sed -nE 's/^PARALLEL=([0-9]+)$/\1/p' "$ENV_FILE")"
|
||||||
|
cache_k="$(sed -nE 's/^CACHE_TYPE_K=(.+)$/\1/p' "$ENV_FILE")"
|
||||||
|
cache_v="$(sed -nE 's/^CACHE_TYPE_V=(.+)$/\1/p' "$ENV_FILE")"
|
||||||
|
[ -n "$ctx" ] && [ -n "$parallel" ] && [ -n "$cache_k" ] && [ -n "$cache_v" ]
|
||||||
|
[ "$parallel" -ge 1 ]
|
||||||
|
# qwen35 spends KV on its 16 full-attention layers only: 2 (K,V) x 4 heads x
|
||||||
|
# 256 dim x 2 B = 64 KiB per token at f16. 11.29 GiB of weights + 0.87 of
|
||||||
|
# mmproj + ~2 GiB of context/compute/MTP leave under 10 GiB of the card's
|
||||||
|
# 24 GiB for KV, and --parallel divides --ctx-size between the slots.
|
||||||
|
[ $((ctx * 64)) -le $((10 * 1024 * 1024)) ]
|
||||||
|
# f16 is the deployed type on purpose: a quantized KV needs the
|
||||||
|
# GGML_CUDA_FA_ALL_QUANTS build option, without which prefill on CUDA
|
||||||
|
# collapses about 12x (see the CUDA lesson in AGENTS.md).
|
||||||
|
[ "$cache_k" = "f16" ] && [ "$cache_v" = "f16" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "frank-small runs on its own port beside frank-large's" {
|
||||||
|
grep -q '^LISTEN_PORT=7771$' "$ENV_FILE"
|
||||||
|
grep -q '^ALIAS=frank-small$' "$ENV_FILE"
|
||||||
|
grep -q -- '--port "$LISTEN_PORT"' "$SERVE"
|
||||||
|
grep -q -- '--host "$BIND_ADDR"' "$SERVE"
|
||||||
|
# The two endpoints cannot share a port, and only the halogen unit publishes
|
||||||
|
# 7770.
|
||||||
|
grep -q '^PublishPort=7770:8731$' "$QUADLET_LARGE"
|
||||||
|
! grep -q 'PublishPort=.*7771' "$QUADLET_LARGE"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the deploy handler installs the root unit and refuses a missing model" {
|
||||||
|
grep -q 'deploy_llm_small' "$REMOTE_DEPLOY"
|
||||||
|
grep -q 'relink_unit frank-small' "$REMOTE_DEPLOY"
|
||||||
|
grep -q 'systemctl enable frank-small.service' "$REMOTE_DEPLOY"
|
||||||
|
grep -q 'systemctl restart frank-small.service' "$REMOTE_DEPLOY"
|
||||||
|
grep -q 'Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp' "$REMOTE_DEPLOY"
|
||||||
|
grep -q 'refusing to start a unit that cannot load' "$REMOTE_DEPLOY"
|
||||||
|
grep -q '127.0.0.1:7771/health' "$REMOTE_DEPLOY"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "frank-small's serve script refuses to start on a half-finished download" {
|
||||||
|
grep -q 'frank-small: missing' "$SERVE"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user