- config.yml: default/commit -> frank-medium (dankserver); task/tiny/smol/vision -> frank-small (dankclaw-3090); vision xhigh - models.yml: add llamacpp (dankclaw-3090) provider; frank-large preserve_thinking + context notes - niri: focus-ring/border/shadow layout, hot-corners off, re-add noctalia.kdl include; blankpad eDP-1 scale 1.0 - memories: flush rollouts; retire frank_large + marreta_authentik skills; add auth-chain/kv-pool/quadlet-log skills - managed-skills: 40+ new fleet runbook skills - agent/skills: import built-in skill set
4.8 KiB
name, description
| name | description |
|---|---|
| llamacpp-multigpu-layer-split-cutover | Cut a fleet LLM endpoint over to llama.cpp layer-split across multiple GPUs when cross-GPU collectives are impossible (RCCL/PCIe-atomics wall), including the build recipe, the bundling trap that fails on the first request, DFlash2/MTP wiring, and the measurement battery that proves the split and the drafter are real. |
llama.cpp layer-split cutover (no-collective multi-GPU)
Use when one model must serve from TWO+ cards and the box cannot dispatch a
cross-card collective (second card behind a chipset bridge with
AtomicOpsCap: Routing-, so ROCr refuses RCCL's hostcall-buffer kernels).
Decide with one line before designing anything:
sudo lspci -vv -s <card2> | grep AtomicOpsCtl # ReqEn- ⇒ no collectives, ever
ReqEn- kills vLLM entirely — tensor parallelism needs an allreduce and
pipeline parallelism needs an RCCL send/recv of the hidden state
(vllm/distributed/parallel_state.py: metadata on gloo, tensors on the device
backend). llama.cpp's --split-mode layer needs no collective: layers are
distributed in order, only the boundary activation crosses PCIe once per
forward pass.
1. Pin the runtime by FEATURE, not by "latest"
Write down which features the deployment needs, then find the ref that has all
of them and pin the 40-hex sha in the build script. On 2026-09-16 that was
DFlash2 (ggml-org/llama.cpp#27342, detected from the draft GGUF:
selector_top_k > 0, spec type stays draft-dflash) and qwen35 MTP. The
ROCmFPX/r9700-toolbox images/fork tips predate both — verify by listing the
built image:
ls <venv>/lib/python*/site-packages/vllm/v1/worker/gpu/spec_decode/ # dflash/ vs dflash2/
2. Build self-contained for one arch
- Private ROCm toolchain (TheRock nightly) with
ROCM_PATH/HIP_PATHexported,-DCMAKE_HIP_ARCHITECTURES=<gfx only>(a sibling arch links, loads and then segfaults),-DGGML_HIP_RCCL=OFFwhen there is no atomics path,-DGGML_NATIVE=OFF. - Bundle the runtime into
build/liband set--set-rpath '$ORIGIN/../lib'. - TRAP —
cp -r SRC DST/flattens whenDSTdoes not exist. rocBLAS and hipBLASLt look for device code at a FIXED depth (../lib/rocblas/library,../lib/hipblaslt/library/<arch>). Flattened, the engine loads every GiB of weights, answers/health, and dies on the first request withCannot read .../TensileLibrary.dat: No such file or directory for GPU arch.mkdir -p DST && cp -r SRC/. DST/.
3. Deploy as an env + serve script + unit trio
One env file holds every tunable with the sizing arithmetic in comments; the
serve script defaults each var, refuses to start when the runtime or payload is
missing, and builds an args array (spec flags assembled conditionally so the
same script serves draft-dflash with a file and draft-mtp in-model). Root
systemd unit: User=<deploy user>, LimitMEMLOCK=infinity,
Restart=on-failure, WantedBy=multi-user.target, plus an
Environment=…_ENV_FILE=<repo path> so the repo file is the single source.
Deploy handler must: refuse when runtime/payload is missing; retire the previous
runtime's units (into trash, never rm); wait for the port to be released
(a retired reverse proxy or container holds it); then enable/restart and gate on
/health.
DRLaf: --split-mode layer --device ROCm0,ROCm1 --tensor-split 1,1,
--parallel 1, f16 KV, --fit off. Pin the drafter with --spec-draft-device
to the card holding the target's last layers. --spec-draft-n-max must be
<= the draft's block_size - 1 (DFlash2: 8 ⇒ 7).
4. Prove it, do not assert it
- The split is real: raise
--ctx-sizeby N tokens and watchrocm-smi— a layer split adds ~N·(bytes/token)/2 to EACH card (both cards carry the KV for their own attention layers), and neither card's TOTAL can equal the model file, which is the arithmetic that rules out replication. - The drafter is live:
timings.draft_n/draft_n_acceptedper request, and the cumulativellamacpp:spec_decode_num_drafts_total— the flags can be right while the draft model silently failed to load. The loader's init line (block_size=,n_extract=,mask_token_id=) identifies the drafter family. - Watch for a 500 on
reasoning_effort: Qwen3.8-family chat templates accept only low/medium/xhigh and RAISE onhigh; map every harness level or subagents 500 on their first request. - Re-point the monitors in the same change: a runtime swap changes the
metric namespace (
vllm:→llamacpp:), and rules/dashboard panels naming series that no longer exist draw nothing forever without erroring. Retire what cannot be expressed (llama.cpp exports no KV-fill gauge and no latency histogram) and name the consequence instead (queue depth, spec-decode-off). - One slot is a concurrency policy, not a bug:
--parallelDIVIDES the context.