187 lines
10 KiB
INI
187 lines
10 KiB
INI
# Quadlet unit: frank-medium (rootless, boot-persistent).
|
|
#
|
|
# Install path on dankserver: ~/.config/containers/systemd/frank-medium.container
|
|
# Unit name: frank-medium.service (container name: frank-medium)
|
|
#
|
|
# The dankserver LLM endpoint (:7770, alias frank-medium): the vllm-radiance
|
|
# stack (the 0.13.0-e060895 image, PR #51) serving
|
|
# hugypufy/Swift-Qwen3.8-27B-PARO-MXFP6 (ParoQuant MXFP6, W4A8, 25.16 GiB)
|
|
# on the 2x Radeon AI PRO R9700 (gfx1201) as ONE tensor-parallel process
|
|
# (TP=2, R4D one-shot all-reduce) with the z-lab DFlash2 external drafter
|
|
# (SPEC=7) and UNQUANTIZED bf16 KV (KV_DTYPE=auto).
|
|
# The entrypoint is QUANTIZATION-AWARE: it reads the model's weight
|
|
# quantization from config.json and serves the plain FP8 path (no MXFP4
|
|
# patches, --quantization=fp8, ROCM_AITER unified attention), the MXFP4
|
|
# patch-and-serve path, or the ParoQuant MXFP6 stack (PR #51's launch6_p
|
|
# kernel). A model swap is now: change SNAP (+ SPEC_METHOD/SPEC), restart.
|
|
# The MXFP4 checkpoint (just1moremodel/Qwen3.8-27B-Uncensored-MXFP4-awq, the
|
|
# 2026-09-20 to 2026-09-21 serving shape) is the on-disk rollback target.
|
|
#
|
|
# SHAPE. The launcher the upstream repo ships binds the server with
|
|
# --network=host (vLLM takes --host 0.0.0.0 --port 7770 directly), which is
|
|
# what this unit does too — no PublishPort. The fleet contract is unchanged:
|
|
# :7770, served model name frank-medium (SERVED_NAMES), OpenAI /v1.
|
|
#
|
|
# TP=2 IS INSTALLABLE HERE NOW, and it won on measurement. Until 2026-09-20
|
|
# this unit was archived un-installed because the second card sat behind B650
|
|
# chipset bridges advertising `AtomicOpsCap: Routing-`, which stopped RCCL
|
|
# kernel dispatch. The board swap (ProArt B850-Creator WiFi NEO) put both
|
|
# cards on CPU GPP root ports — Gen5 x8 each, every bridge Routing+, both
|
|
# endpoints ReqEn+ — and the collective-free R4D path runs at TP=2 without
|
|
# complaint. The conversation battery (scripts/bench-conversation.py, salted,
|
|
# same 10 prompts, 2026-09-20) measured this shape against the incumbent:
|
|
# decode geomean 95.8 t/s vs 45.7, mean TTFT 0.21 s vs 0.72 s, and 259 t/s
|
|
# aggregate against 4 concurrent streams vs 71.7. Evidence and the llama-side
|
|
# caveats: serverAppConfigs-docs/LLM/frank-medium-radiance-mxfp4.md and
|
|
# Dual-R9700-Cooperation.md.
|
|
#
|
|
# DEVICES. /dev/kfd + /dev/dri through the amdgpu/KFD stack; HIP/ROCR device
|
|
# indices 0,1 (the two R9700s). The 2 GiB Raphael iGPU enumerates as index 2
|
|
# and is excluded both by the index pin and by the upstream MIN_GPU_MIB=8192
|
|
# logic this unit replaces. --privileged --ipc=host --security-opt
|
|
# seccomp=unconfined --cap-add SYS_PTRACE is the upstream launcher's exact
|
|
# podman posture.
|
|
#
|
|
# MOUNTS. /models (rw checkpoints), the HF cache (tokenizer files),
|
|
# /cache (Triton/inductor/aiter/vLLM compile cache — first start compiles for
|
|
# several minutes, later starts reuse), /patches (the pinned radiance clone —
|
|
# its patches are applied at EVERY container start by the entrypoint), /r4d
|
|
# (the patched libr4d build; the image's shipped r4d.so NaNs the
|
|
# gated-delta-net and must never be used), and the entrypoint + healthcheck
|
|
# scripts and the chat template from this checkout (mounted read-only
|
|
# straight out of /home/dank/serverAppConfigs — deploy.sh git-pulls the
|
|
# checkout before dispatching).
|
|
#
|
|
# IMAGE. The unit runs the localhost/-aliased tag: this host's podman quadlet
|
|
# path cannot resolve registry-prefixed refs from local storage at create
|
|
# time (same as frank-large on dankclaw). deploy-remote.sh pulls the
|
|
# docker.io ref and re-tags it before the restart. Pull=never keeps boots off
|
|
# the registry.
|
|
#
|
|
# KEEP IN SYNC: the radiance clone is pinned (e060895...); if it is updated,
|
|
# re-diff dankserver/frank-medium/radiance-entrypoint.sh against the new
|
|
# serve-mxfp4.sh container command and bump the pin deliberately.
|
|
#
|
|
# DO NOT name an environment variable TEMP, HOST or GPU_MAX_HEAP_SIZE: the
|
|
# HIP runtime reads bare TEMP for its JIT cache path (2026-08-13 incident).
|
|
[Unit]
|
|
Description=frank-medium — vllm-radiance ParoQuant MXFP6 Swift Qwen3.8-27B on 2x R9700 (gfx1201)
|
|
After=network-online.target
|
|
|
|
[Container]
|
|
ContainerName=frank-medium
|
|
Image=localhost/radiance-vllm:0.13.0-e060895
|
|
# vLLM binds 0.0.0.0:7770 on the host network directly; no PublishPort.
|
|
AddDevice=/dev/kfd
|
|
AddDevice=/dev/dri
|
|
GroupAdd=keep-groups
|
|
# The upstream launcher's exact podman posture (rootless; podman-only flags
|
|
# like --replace are not needed under systemd management).
|
|
PodmanArgs=--privileged --ipc=host --security-opt seccomp=unconfined --cap-add SYS_PTRACE --network=host
|
|
# The two R9700s. The Raphael iGPU (index 2, 2 GiB) stays out: shared system
|
|
# memory is not a compute target.
|
|
Environment=ROCR_VISIBLE_DEVICES=0,1
|
|
Environment=HIP_VISIBLE_DEVICES=0,1
|
|
# Drafting: the external z-lab DFlash2-FP8 drafter (SPEC_METHOD=dflash). The
|
|
# plain-FP8 checkpoint (rollback) carries its own single-layer MTP head, but
|
|
# DFlash2 beat MTP self-speculation on decode, so the drafter serves either.
|
|
# Chat template: the Qwen-Sharp variant (peculiar-ragdoll v22.5.0) — the terse
|
|
# system prompt (opt out per-request with chat_template_kwargs {"terse":
|
|
# false}), message.reasoning support (vLLM's thinking field), and the
|
|
# tightened tool-call rendering. The predecessor (qwen-sharp-v22.4.0.jinja,
|
|
# mounted beside it) remains as the rollback target: flip CHAT_TEMPLATE.
|
|
Environment=SNAP=/models/Qwen3.8-27B-PARO-MXFP6
|
|
Environment=DRAFTER=/models/Qwen3.8-27B-DFlash2-FP8
|
|
Environment=CHAT_TEMPLATE=/templates/qwen-sharp-v22.5.0.jinja
|
|
# Fleet identity: the id at /v1/models and in every response body. Every
|
|
# client (Hermes, the harness, services.json) points at frank-medium.
|
|
Environment=SERVED_NAMES=frank-medium
|
|
Environment=PORT=7770
|
|
# Fixed 2-card tensor parallel: the tested configuration of the upstream
|
|
# repo; the R4D one-shot all-reduce kernel serves it natively.
|
|
Environment=TP=2
|
|
# Measured production batch shape (upstream README): MAXSEQS 8 peaks
|
|
# aggregate throughput at 573 t/s. MAXLEN is the served window (see KV_DTYPE).
|
|
Environment=MAXSEQS=8
|
|
Environment=CHUNK=8192
|
|
Environment=MAXLEN=262144
|
|
# MAXLEN is the whole pool shared across the MAXSEQS slots: 8 concurrent
|
|
# sessions split 262,144 tokens between them, so a full-window request
|
|
# excludes the others until it finishes (vLLM preempts rather than refuses).
|
|
# The llama deployment it replaced advertised 327,680 across 2 slots.
|
|
#
|
|
# VISION WORKS (verified 2026-09-21: a solid red 48x48 PNG is named "Red").
|
|
# config.json sets language_model_only=true and there is no separate mmproj,
|
|
# but the Qwen3_5ForConditionalGeneration architecture carries the vision tower
|
|
# through the Gemma4 merge path and it is not MXFP4-quantized, so vLLM serves
|
|
# image input on either checkpoint. harness models.yml keeps frank-medium
|
|
# input: [text, image].
|
|
# 0.97, down from 0.98: the startup free-memory check (free >= util x 31.86
|
|
# GiB) sat ~15 MiB inside the threshold on the 2026-09-26 boot (free 31.21
|
|
# GiB vs required 31.22 GiB) — the worker's own HIP context plus the
|
|
# amdgpu per-boot baseline is ~690 MiB at check time and that drifts a few
|
|
# tens of MiB between boots, so 0.98 crash-looped on every restart. 0.97
|
|
# buys ~160 MiB of headroom at the cost of ~5k KV tokens on the
|
|
# auto-sized pool.
|
|
Environment=GPU_UTIL=0.97
|
|
# Speculative decoding: DFlash2 external drafter at depth 7. A/B tested
|
|
# against MTP self-speculation (depth 8) on this FP8 checkpoint: DFlash2
|
|
# wins on decode (100.0 vs 85.9 t/s geomean, 287.1 vs 179.7 t/s burst-4)
|
|
# despite a lower accept length (4.24 vs 5.66) — the small separate drafter
|
|
# steps faster than the target's own MTP layer applied autoregressively.
|
|
Environment=SPEC_METHOD=dflash
|
|
Environment=SPEC=7
|
|
# KV cache: 0 = profile the shape (the documented plain-FP8 path's default;
|
|
# the upstream docker-compose leaves the pin commented out). The measured pin
|
|
# for this shape is 15989735424 (14.89 GiB, upstream, 2x R9700 32GB); after
|
|
# the first healthy start, recalibrate with calibrate-kv.sh to use the extra
|
|
# VRAM on the 34.2 GiB cards.
|
|
Environment=KV_MEM=0
|
|
# KV cache dtype: auto = UNQUANTIZED bf16 KV. The experiment: fp8 KV stacked on
|
|
# MXFP4's already-quantized W4A8 weights compounds attention precision loss, so
|
|
# clean KV should recover output quality. MXFP4's freed weights (19.37 vs 30.9
|
|
# GB) buy the pool back — the full 262,144 native window still fits unquantized
|
|
# (~290k-token f16 pool > 262,144). Flip to fp8 for the measured 943k-token pool.
|
|
Environment=KV_DTYPE=auto
|
|
# Checkpoints (rw: nothing downloads at runtime, but vLLM mmaps the shards;
|
|
# keep parity with the launcher's rw mount).
|
|
Volume=/home/dank/models:/models
|
|
# Tokenizer files ride the HF cache; HF_HUB_OFFLINE=1 is set in the
|
|
# entrypoint.
|
|
Volume=/home/dank/.cache/huggingface:/root/.cache/huggingface
|
|
# Compile cache. Keyed on model + stack version + every graph-changing knob;
|
|
# never share it across configurations (upstream rule).
|
|
Volume=/home/dank/.radiance-cache-mxfp6-013-pq-nqft-fp8s-gnq:/cache
|
|
# The pinned radiance clone: patches applied at every container start.
|
|
Volume=/home/dank/GitPackages/radiance-vllm-mxfp4:/patches:z
|
|
# The patched libr4d (pin b9e42ab + repo extras, key b9e42ab-rx6). The
|
|
# image's shipped r4d.so predates the GDN overflow fix and NaNs this model.
|
|
Volume=/home/dank/.cache/radiance-libr4d/b9e42ab-rx6:/r4d:z
|
|
# Entrypoint + healthcheck from this checkout, mounted read-only.
|
|
Volume=/home/dank/serverAppConfigs/dankserver/frank-medium/radiance-entrypoint.sh:/usr/local/bin/frank-medium-entrypoint.sh:ro
|
|
Volume=/home/dank/serverAppConfigs/dankserver/frank-medium/frank-medium-healthcheck.py:/usr/local/bin/frank-medium-healthcheck.py:ro
|
|
Volume=/home/dank/serverAppConfigs/dankserver/frank-medium/qwen-sharp-v22.4.0.jinja:/templates/qwen-sharp-v22.4.0.jinja:ro
|
|
Volume=/home/dank/serverAppConfigs/dankserver/frank-medium/qwen-sharp-v22.5.0.jinja:/templates/qwen-sharp-v22.5.0.jinja:ro
|
|
# The entrypoint must be the container command; the image's own entrypoint
|
|
# (which the patched script execs at the end) must not run unpatched.
|
|
PodmanArgs=--entrypoint /usr/local/bin/frank-medium-entrypoint.sh
|
|
# 200 only once the engine serves; during first-start compilation the
|
|
# container is legitimately unhealthy. HealthCmd is split on spaces into an
|
|
# argv array by the podman 5.x v1 generator, hence the file-based probe.
|
|
HealthCmd=python3 /usr/local/bin/frank-medium-healthcheck.py
|
|
HealthInterval=60s
|
|
HealthTimeout=30s
|
|
HealthRetries=3
|
|
HealthStartPeriod=1800s
|
|
Pull=never
|
|
|
|
[Service]
|
|
# First start compiles Triton/inductor kernels (several minutes) before the
|
|
# engine loads ~9.5 GiB of weights per card.
|
|
TimeoutStartSec=1800
|
|
Restart=on-failure
|
|
RestartSec=10
|
|
|
|
[Install]
|
|
WantedBy=default.target
|