The 320k pool (163840 per conversation over two slots) costs ~4.3 GiB of KV, and the BF16 projector (888 MiB) lands on ROCm0. Fit at the deployed point, computed, not estimated: ROCm0 29.19 / ROCm1 26.04 GiB of 31.86 -- 2.67 / 5.82 GiB free, both above the 1.5 GiB floor. 36:29 is one layer over the 35:30 balance on purpose: at this context the balance point is 34:31 (0.11 GiB gap), and the operator moved a layer to ROCm0 to buy headroom on the drafter card. The gap is 3.15 GiB by construction, so the seam's slack is that gap rounded up (3225 MiB) with the trade written down, and the ctx cap moves 262144 -> 327680 with the rope caveat (the GGUF carries no scaling; positions past the trained 262144 extrapolate on the base rope). Vision: NO_MMPROJ=0, the serve script now refuses to start without the projector when vision is on (a missing file is a deploy error, not a silent text-only server), the download script fetches and byte-verifies it, and the deploy handler's preflight checks for it. The projector is already on the host, byte- and sha256-verified against the Hub.
313 lines
16 KiB
Bash
313 lines
16 KiB
Bash
# Tunables for frank-medium (ajgazin/Swift-Qwen3.8-27B-Uncensored-Dynamic-MTP
|
|
# UD-Q8_K_XL + z-lab/Qwen3.8-27B-DFlash2-GGUF Q8_0 drafter), mainline llama.cpp
|
|
# on Dankserver, 2x AMD Radeon AI PRO R9700 (gfx1201). Since 2026-09-16 this
|
|
# is the SOLE LLM runtime on the box: the vllm-radiance replica pair and its
|
|
# routing proxy are retired.
|
|
#
|
|
# Hardware: two R9700 (Navi 48, RDNA4, 32,624 MiB each), no XGMI, different
|
|
# IOMMU groups -- every cross-card byte goes over PCIe. The Raphael iGPU
|
|
# (gfx1036/Radeon 610M) enumerates as HIP device 2 and is hidden below so
|
|
# nothing can drift onto shared system RAM.
|
|
#
|
|
# WHY LAYER SPLIT AND NOT TENSOR (operator call, 2026-09-16): tensor parallel
|
|
# needs a cross-card allreduce per layer, and this host cannot dispatch one.
|
|
# RCCL's kernels carry a hidden hostcall-buffer parameter, and ROCr refuses it
|
|
# on a device without PCIe atomics -- the second card sits behind the B650
|
|
# chipset bridges (AtomicOpsCap: Routing-, read-only), so every collective that
|
|
# touches it dies with hipErrorIllegalState. Measured on this box 2026-09-15
|
|
# (llama-bench, pp4096 / tg128, f16 KV, fa on):
|
|
#
|
|
# single R9700 1160.9 / 19.42
|
|
# layer 2x 1991.2 / 18.76 (859.1 / 17.64 at depth 32768)
|
|
# tensor 2x 271.8 / 26.26 (no RCCL: butterfly fallback, -7x prefill)
|
|
#
|
|
# Layer split distributes layers in order; only the activation at the boundary
|
|
# crosses the wire, once per forward pass. That is the "pipeline parallelism"
|
|
# this service wanted, and it needs no collective at all.
|
|
#
|
|
# Runtime: mainline llama.cpp master fb27a525 (2026-09-16), gfx1201, built by
|
|
# scripts/build-llama-rdna4.sh against a private ROCm 7.14.0a20260624 toolchain
|
|
# that is bundled next to the binary (RPATH $ORIGIN/../lib, no LD_LIBRARY_PATH).
|
|
# The ref is pinned by TWO features: DFlash2 (ggml-org/llama.cpp#27342 -- the
|
|
# drafter is detected from its GGUF) and qwen35 MTP. Do not float it.
|
|
#
|
|
# Model: ajgazin/Swift-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF @ UD-Q8_K_XL,
|
|
# 29.29 GiB, arch qwen35, Unsloth Dynamic 3.0 (per-tensor types from Unsloth's
|
|
# own quant of the same size, on Unsloth's importance matrix). Abliteration is
|
|
# orcarouter's single refusal direction projected out of 131 tensors; the vision
|
|
# tower loads from mmproj-BF16.gguf (see NO_MMPROJ). The file also
|
|
# carries the model's MTP head (blk.64.nextn.*), so a file-level switch to
|
|
# --spec-type draft-mtp is available without a second copy in VRAM.
|
|
#
|
|
# Drafter: z-lab/Qwen3.8-27B-DFlash2-GGUF @ Q8_0, 1.92 GiB: 5 layers, hidden
|
|
# 5120, a block-diffusion proposal (block_size 8) under a 2048-token sliding
|
|
# window. Decoding stays lossless -- the target verifies every drafted token.
|
|
# The window is what makes it cheap HERE: its KV is bounded at 2048 positions
|
|
# (~40 MiB) instead of spanning the context (see SIZING).
|
|
#
|
|
# Architecture (GGUF arch qwen35): 64 layers as 16 x (3 x (Gated DeltaNet ->
|
|
# FFN) + 1 x (Gated Attention -> FFN)) -- 48 linear-attention layers whose
|
|
# per-sequence state is CONSTANT-SIZE, 16 full-attention layers at 4 KV heads x
|
|
# 256 head_dim. A token therefore costs 2 x 4 x 256 x 2 B = 64 KiB of KV at f16
|
|
# in the attention layers only, plus ~0.16 GiB per sequence of f32 DeltaNet
|
|
# state at any depth.
|
|
#
|
|
# SIZING, measured (rocm-smi, 2026-09-16). Weights split across the two cards;
|
|
# the drafter AND its KV ride the second. The loader's compute buffers are part
|
|
# of what is seen below -- they are allocated at load, not per request, and they
|
|
# scale with --ubatch-size:
|
|
#
|
|
# CTX split batch slots drafter GPU 0 free GPU 1 free note (ubatch 1536)
|
|
# 262144 1,1 2048 1 DFlash2 8.64 GiB 4.43 GiB day's start
|
|
# 262144 39,26 6144 2 DFlash2 2.42 GiB 7.20 GiB
|
|
# 262144 38,27 6144 2 DFlash2 3.85 GiB 5.77 GiB
|
|
# 262144 37,28 6144 2 DFlash2 4.28 GiB 5.34 GiB
|
|
# 262144 36,29 6144 2 DFlash2 4.71 GiB 4.91 GiB
|
|
# 262144 35,30 6144 2 DFlash2 5.14 GiB 4.48 GiB
|
|
# 262144 40,25 6144 2 DSpark 1.87 GiB 1.86 GiB (retired)
|
|
#
|
|
# ...and the same deployed shape re-measured at ubatch 512, after llama-bench
|
|
# showed 512 is the throughput optimum and costs 3.71 GiB less in total (see the
|
|
# batch block below):
|
|
#
|
|
# 262144 36,29 6144 512 2 DFlash2 6.01 GiB 7.32 GiB ubatch only
|
|
# 262144 35,30 6144 512 2 DFlash2 6.44 GiB 6.90 GiB <- deployed
|
|
#
|
|
# ONE -ts STEP IS ONE LAYER, AND LAYERS ARE NOT THE SAME SIZE. This is a hybrid
|
|
# stack -- 16 x (3 Gated DeltaNet + 1 Gated Attention) blocks -- so moving a
|
|
# DeltaNet layer shifts ~0.43 GiB while an attention layer shifts ~1.43 GiB
|
|
# (0.45 of weights plus ~1.0 GiB of its own f16 KV at 262144). Both step sizes
|
|
# were measured, which is why the balance is found by measurement rather than by
|
|
# interpolating between splits.
|
|
#
|
|
# 262144 is the window the model ships (n_ctx_train) and it fits with room, so
|
|
# the fleet gets the whole thing; the arithmetic that used to cap this endpoint
|
|
# at 114688 was the vLLM predecessor's, not this runtime's. If a future change
|
|
# adds VRAM (a longer drafter, quantized KV, more slots) re-read the numbers
|
|
# rather than assuming, and prefer CTX_SIZE over --gpu-layers.
|
|
#
|
|
# WHY THE SPLIT IS 35:30. Three effects decide it, and all three are measured:
|
|
# * THE DRAFTER AND ITS OUTPUT LAYERS RIDE ROCm1. It carries the drafter's
|
|
# weights, the draft graph and the last target layers, so it takes fewer
|
|
# than half the layer count.
|
|
# * THE UBATCH BUFFERS COST ROCm1 MORE THAN ROCm0, and shrinking them pays
|
|
# ROCm1 back more: 1536 -> 512 freed 1.30 GiB on ROCm0 but 2.41 GiB on
|
|
# ROCm1 at this shape. Read the other way, that is exactly why the smaller
|
|
# batch moved the balance toward ROCm1 -- and cost ROCm0 one layer.
|
|
# * THE LAYER GRANULARITY IS THE FLOOR ON HOW WELL ANY SPLIT CAN BALANCE. One
|
|
# -ts step is one layer -- ~0.43 GiB DeltaNet, ~1.43 GiB attention -- so the
|
|
# reachable minimum gap is comparable to one step rather than to zero. 35:30
|
|
# sits at 0.46 GiB, i.e. one DeltaNet layer's worth, against 1.31 GiB at
|
|
# 36:29.
|
|
# It also leaves the most room: 6.44 / 6.90 GiB free clears the operator's 1.5
|
|
# GiB-per-card floor by ~5 GiB, so no headroom has to be bought out of CTX_SIZE
|
|
# and the full window stays.
|
|
#
|
|
# A FAULT TO WATCH. A 39:26 split, at 1x batch / 1 slot, died on its eighth
|
|
# request: the amdgpu driver logged SDMA0 UTCL2 page faults and the engine
|
|
# surfaced `ROCm error: an illegal memory access was encountered` -> ggml_abort,
|
|
# i.e. a fault in the cross-card boundary copy, this layer split's hot path. It
|
|
# has not reproduced (36:29 served this file's verification battery and the e2e
|
|
# gate, and 35:30 is the deployed shape), but the signature is worth recognising
|
|
# -- it is NOT an OOM, and no amount of headroom prevents it.
|
|
#
|
|
# WARNING: do not name any variable TEMP, HOST, or GPU_MAX_HEAP_SIZE (HIP reads
|
|
# those bare vars; 2026-08-13 incident). llama.cpp takes --host/--port/--temp
|
|
# as flags only.
|
|
|
|
# llama-server: mainline fb27a525 built for gfx1201 by scripts/build-llama-rdna4.sh.
|
|
LLAMA_BIN=/home/dank/GitPackages/llama.cpp-dflash2/build-rdna4-rocm714/bin/llama-server
|
|
|
|
# Payload, fetched with scripts/download-frank-medium-gguf.sh.
|
|
MODEL_DIR=/home/dank/models/Swift-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF
|
|
MODEL_FILE=Swift-Qwen3.8-27B-Uncensored-Dynamic-MTP-UD-Q8_K_XL.gguf
|
|
# The BF16 vision projector, same repo as the target. llama-server autoloads
|
|
# any mmproj*.gguf beside the model; the serve script refuses to start without
|
|
# it when vision is enabled, so a missing file is a deploy error, not a
|
|
# text-only server that pretends to be multimodal.
|
|
MM_PROJ_FILE=mmproj-BF16.gguf
|
|
DRAFT_MODEL_DIR=/home/dank/models/Qwen3.8-27B-DFlash2-GGUF
|
|
DRAFT_MODEL_FILE=Qwen3.8-27B-DFlash2-Q8_0.gguf
|
|
|
|
# Fleet identity. 7770 is the fleet's dankserver endpoint; the alias is the id
|
|
# at /v1/models and what every client asks for.
|
|
ALIAS=frank-medium
|
|
BIND_ADDR=0.0.0.0
|
|
LISTEN_PORT=7770
|
|
# Device placement. HIP_VISIBLE_DEVICES hides the Raphael iGPU (HIP device 2)
|
|
# so nothing can drift onto shared system RAM; DEVICES is what llama.cpp splits
|
|
# across, and neither of the two cards has an XGMI link to the other.
|
|
HIP_VISIBLE_DEVICES=0,1
|
|
DEVICES=ROCm0,ROCm1
|
|
SPLIT_MODE=layer
|
|
|
|
# 36:29 -- one layer over the 35:30 balance, moved to buy headroom on ROCm1,
|
|
# the card that carries the drafter, its KV, the output layers and the heavier
|
|
# compute buffers, now that the context grew to 327680 (see below). Fit at
|
|
# the deployed point (36:29, ctx 327680, batch 6144 x ubatch 512, two slots,
|
|
# DFlash2 + ngram-mod, mmproj on ROCm0): ROCm0 29.19 / ROCm1 26.04 GiB of
|
|
# 31.86, i.e. 2.67 / 5.82 GiB free and a 3.15 GiB gap BY CONSTRUCTION -- the
|
|
# balance point at this context is 34:31 (0.11 GiB gap) and the operator chose
|
|
# headroom on the drafter card over balance. See SIZING above for the
|
|
# measurement table; one -ts step is one LAYER, worth ~0.43 GiB (DeltaNet) or
|
|
# ~1.43 GiB (attention), so do not move it without re-measuring.
|
|
TENSOR_SPLIT=36,29
|
|
|
|
NGL=999
|
|
# Auto-fit has no business resizing a context we set explicitly.
|
|
FIT=off
|
|
FLASH_ATTN=on
|
|
|
|
# 327680 positions over TWO slots, so each conversation gets 163840 --
|
|
# --parallel DIVIDES --ctx-size, so more slots would shrink every
|
|
# conversation rather than grow the cache. The checkpoint's trained window is
|
|
# 262144 and the GGUF carries NO rope scaling (freq_base 1e7, no YaRN keys),
|
|
# so positions beyond 262144 extrapolate on the base rope: it runs, and 1.25x
|
|
# is a modest ask of it, but the extra 65k is untrained territory.
|
|
CTX_SIZE=327680
|
|
PARALLEL=2
|
|
|
|
# MEASURED 2026-09-16 with scripts/bench-batch-ubatch.sh on this box: llama-bench,
|
|
# a 64k prefill plus a 128-token decode with 64k of KV already resident, 3 reps,
|
|
# one invocation per cell, the deployed split and f16 KV. --ubatch-size is the
|
|
# physical batch the graph is built at -- and the only one of the two knobs that
|
|
# costs VRAM:
|
|
#
|
|
# batch ubatch prefill tok/s GPU 0 / GPU 1 free
|
|
# 6144 1536 1164.96 +- 2.30 10.28 / 11.71 GiB
|
|
# 6144 768 1135.03 +- 1.09 11.29 / 13.28 GiB
|
|
# 6144 512 1192.93 +- 0.91 11.63 / 13.81 GiB <- deployed
|
|
# 6144 384 1068.04 +- 0.79 11.80 / 14.07 GiB
|
|
# 6144 256 1045.98 +- 1.69 11.97 / 14.33 GiB
|
|
# 4096 1536 1161.90 +- 0.96 10.28 / 11.71 GiB
|
|
# 2048 1536 1170.23 +- 0.86 10.28 / 11.71 GiB
|
|
# 2048 512 1193.88 +- 1.01 11.63 / 13.81 GiB
|
|
#
|
|
# THE PLATEAU STARTS AT 512. 512 -> 1536 buys ~2 % of prefill -- inside the
|
|
# run-to-run spread, and the 512 cells were the faster pair in BOTH batch
|
|
# columns -- while costing 1.35 GiB on ROCm0 and 2.10 GiB on ROCm1, so the
|
|
# deployment takes the cheap end. BELOW 512 the SMs are no longer saturated:
|
|
# 384 and 256 fall ~11 % and ~12 %. The logical batch is free either way --
|
|
# 2048, 4096 and 6144 land within 2 % of each other at a fixed ubatch and use
|
|
# the SAME VRAM, because the compute buffers are sized by ubatch alone.
|
|
#
|
|
# The prefill figure is a property of the SHAPE, not of this knob: the same
|
|
# sweep with no depth in front of it (depth 0) runs 1543 tok/s at ubatch 256 and
|
|
# 1723 at 1536 -- the plateau is in the same place, and the ~32 % between the two
|
|
# tables is what attending over 64k of resident KV costs. Decode is 15.88 tok/s
|
|
# at depth 65536 on EVERY ubatch (17.80 at depth 0), so this knob cannot touch
|
|
# generation at all.
|
|
#
|
|
# 8192/2048 stays untested under this drafter (the "2020 MiB pp buffer fails on
|
|
# ROCm1" note was measured under DSpark, when it had 0.08 GiB free), and there is
|
|
# no longer a reason to want it: batch buys nothing measurable here.
|
|
BATCH_SIZE=6144
|
|
UBATCH_SIZE=512
|
|
|
|
# Threads cover tokenization, sampling and HTTP only: every layer is on a GPU.
|
|
NUM_THREADS=8
|
|
THREAD_BATCH=8
|
|
|
|
# f16 K/V: an agentic checkpoint is the wrong place to spend KV precision.
|
|
CACHE_TYPE_K=f16
|
|
CACHE_TYPE_V=f16
|
|
# Context checkpoints: 0 is the build's default -- no per-slot KV snapshots, so
|
|
# a re-sent or rewound conversation re-prefills instead of rewinding. Stated
|
|
# explicitly so the tunable stays visible rather than merely absent.
|
|
CTX_CHECKPOINTS=0
|
|
CACHE_RAM=8192
|
|
|
|
# Sampling defaults for requests that send none -- the values the vLLM
|
|
# predecessor served on this endpoint (`--override-generation-config`), so a
|
|
# client sees no change across the runtime swap. The model card's own
|
|
# recommendation is temp 1.0 / top_p 0.95; the harness sets its own per model in
|
|
# models.yml, so this is the floor, not the contract.
|
|
TEMPERATURE=0.7
|
|
TOP_P=0.95
|
|
TOP_K=20
|
|
MIN_P=0.0
|
|
PRESENCE_PENALTY=0.0
|
|
|
|
# Reasoning on, projected into reasoning_content (deepseek format) with no
|
|
# budget: the model decides how long to think.
|
|
REASONING=on
|
|
REASONING_FORMAT=deepseek
|
|
REASONING_BUDGET=-1
|
|
|
|
# Chat template: empty = the one embedded in the GGUF (Qwen3.8's own).
|
|
CHAT_TEMPLATE_FILE=
|
|
CHAT_TEMPLATE_KWARGS='{"enable_thinking":true,"preserve_thinking":true}'
|
|
|
|
# Speculative decoding: the DFlash2 block-diffusion drafter PLUS the draftless
|
|
# n-gram matcher. DFlash2's GGUF carries block_size 8, so at most 7 draft
|
|
# tokens per verification step (llama.cpp asserts n_max <= block_size - 1).
|
|
# The drafter rides ROCm1, the card that ends up holding the target's last
|
|
# layers.
|
|
SPEC_TYPE=draft-dflash,ngram-mod
|
|
SPEC_DRAFT_N_MAX=7
|
|
SPEC_DRAFT_NGL=999
|
|
SPEC_DRAFT_DEVICE=ROCm1
|
|
SPEC_DRAFT_TYPE_K=f16
|
|
SPEC_DRAFT_TYPE_V=f16
|
|
|
|
# MTP (the model's own head) vs the drafter -- MEASURED 2026-09-16 (temperature
|
|
# 0, 1k prompt / 512 out; the autoregressive baseline and both MTP depths were
|
|
# run against the deployed configuration in one session, same binary, same
|
|
# prompts):
|
|
#
|
|
# none 17.7 tok/s card1 26.07 GB
|
|
# draft-mtp n=3 32.4 tok/s 58 % accepted card1 29.35 GB
|
|
# draft-mtp n=7 26.7 tok/s 33 % accepted card1 29.65 GB
|
|
# draft-dflash 7 43.5 tok/s 41 % accepted card1 29.42 GB
|
|
#
|
|
# All four produced IDENTICAL greedy text, so verification is lossless whichever
|
|
# proposer runs. DFlash2 is worth 2.46x over autoregressive and 34 % over the
|
|
# best MTP; deeper MTP drafting costs more than it returns (n=7 accepts 33 % of
|
|
# 147 proposals, n=3 accepts 58 % of 72). The MTP head is not free either:
|
|
# selecting it loads blk.64.nextn.* and adds ~3.3 GB to the card that holds it,
|
|
# against 2.06 GB for the drafter's file. Do not switch this without
|
|
# re-measuring.
|
|
#
|
|
# THE TWO CANNOT BE CHAINED. `--spec-type draft-mtp,draft-dflash` dies at load:
|
|
# context type MTP requested but model doesn't contain MTP layers
|
|
# failed to create MTP context
|
|
# A chain has ONE draft context, and MTP builds against whatever model that
|
|
# context holds -- here the DFlash2 file, which has no MTP layers. MTP's head is
|
|
# only loaded when MTP IS the draft model ("creating MTP draft context against
|
|
# the target model ..."), so a chain of the two is a structural dead end, not a
|
|
# tuning question. See serverAppConfigs-docs/LLM/frank-medium-llamacpp-dflash2.md.
|
|
|
|
# ngram-mod: the draftless n-gram matcher (CPU hash table in host RAM, no VRAM,
|
|
# no model file, no draft context). It joins the chain unconditionally --
|
|
# unlike MTP, which needs a draft context and therefore cannot share one with
|
|
# DFlash2. Verified on the loaded server: with ngram-mod as the ONLY spec type,
|
|
# a prose prompt produces ZERO drafts (plain autoregressive) while a
|
|
# prompt-echo prompt accepts 100 % of them, so its drafts are unmistakable.
|
|
#
|
|
# WINDOW MEASURED 2026-09-16 (chain at each window, decode tok/s):
|
|
#
|
|
# n_min/n_max prose 1k/512 prompt-echo 1.5k/400
|
|
# 48/64 43.9 200.6 <- deployed
|
|
# 16/32 36.0 161.0
|
|
# 8/16 36.2 113.8
|
|
#
|
|
# The long window is the right one, and not only because it wins on echo: a
|
|
# SHORT window makes the matcher propose on short fragments of ordinary prose,
|
|
# where the proposals are rejected and the verification still costs -- which is
|
|
# the "ngram hurts" result the fleet measured on other payloads twice before.
|
|
# With 48/64 the chain is byte-identical to DFlash2 alone on prose (same 119
|
|
# drafts / 49 accepted) and on the identity prompt (70/53).
|
|
#
|
|
# Do not shorten these without re-running scripts/bench-spec-ab.py: the
|
|
# no-penalty property is what makes the chain safe as a default.
|
|
SPEC_NGRAM_N_MATCH=24
|
|
SPEC_NGRAM_N_MIN=48
|
|
SPEC_NGRAM_N_MAX=64
|
|
|
|
# Vision enabled: llama-server autoloads mmproj-BF16.gguf from MODEL_DIR
|
|
# (888 MiB, lands on ROCm0 -- the fit above charges it there). frank-small
|
|
# (Dankclaw, RTX 3090) remains the fleet's dedicated image endpoint; this
|
|
# makes frank-medium multimodal too, which is why the seam flips from
|
|
# "text only" to "vision loads with the model".
|
|
NO_MMPROJ=0
|