Files
LLM-Server/docs/architecture.md
T
haylanandClaude Bot 51684fb7a4 feat(llm): add --spec-draft-p-min 0.5 to DFlash2 spec args
DFlash2 truncates its draft when selector confidence drops below p_min,
cutting wasted verify work on long, low-acceptance thinking output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:53:27 +01:00

7.8 KiB

Architecture

Source of truth: docker-compose.yml → each service below. This page explains why; flags, values and defaults live in the source. If they disagree, the source wins.

Terms as defined in CONTEXT.md.

Every workload calls the gateway (omniroute) with its own API key. The gateway forwards each call over a provider connection to the one backend on the GPU: llama-server (main model). qdrant sits beside them on the same Docker network. It isn't a model, so it isn't behind the gateway.

GPU access

llama-server carries these GPU settings in compose:

  • group_add uses numeric GIDs, not group names. Docker resolves a named group_add entry against the container's own /etc/group, not the host's. That fails unpredictably when the image doesn't define the group, and it gets worse when several GPU services start at once. scripts/update.sh resolves the host's real GIDs into HOST_VIDEO_GID / HOST_RENDER_GID (see operations → Host-resolved values).
  • GPU_MAX_HW_QUEUES works around ROCm/ROCm#5706: the card sits pinned at full load and boost clock whenever two HIP contexts touch it. Keep it set on every GPU service.

Background: research/rocm-gpu-pin-and-render-group.md. Its service names are older than the current ones, but the findings still apply.

llama-server

The main model's backend. It is always running. Lazytainer used to idle-stop it, but was removed because its idle detection could never fire behind the gateway (research/lazytainer-omniroute-idle-stop.md).

  • Model file (LLAMA_MODEL_FILE): the quant choice and its VRAM budget are in research/qwen3.8-27b-quant.md.
  • LLAMA_GPU_LAYERS: the default puts every layer on the GPU. The model is dense, not Mixture-of-Experts, so this is the only RAM-offload knob. --n-cpu-moe, --cpu-moe and --override-tensor "exps" do nothing here. There is no SSD tier to enable either. llama.cpp mmaps the model file, so if GPU and RAM ever can't hold it, the OS pages the rest in from disk as a slow, implicit last resort. Explicit tiered offload is an open llama.cpp request.
  • LLAMA_CTX_SIZE: set to the model's own maximum (max_position_embeddings). It only fits beside the weights because the KV cache is quantized (LLAMA_KV_CACHE_TYPE, used for both --cache-type-k and --cache-type-v). An fp16 KV cache at that size would not fit the card. The numbers are in the quant research doc.
  • LLAMA_PARALLEL is the backend's number of slots. It's kept low on purpose: more slots meant more contended prefill, which pushed requests past the gateway's timeouts (gateway → Timeouts). Requests beyond the slot count wait in llama.cpp's own queue, which has no timeout. So the timeout that matters is the gateway's per-connection one.
  • Per-slot context = LLAMA_CTX_SIZE / LLAMA_PARALLEL. llama.cpp divides the context across slots, and each request gets one slot's share. Every coding CLI's context-window setting must use this number, not the raw context size. Real sessions already fill a large share of it, so check that the per-slot figure stays comfortably above observed usage before lowering either value.
  • No --cache-reuse: this model's context doesn't support it. llama-server logs "cache_reuse is not supported by this context, it will be disabled" at startup, so the flag was dropped. Re-prefill avoidance comes from --cache-ram (below) and OmniRoute's prompt-cache affinity (a dashboard setting, on by default), which keeps a conversation on the same slot. The earlier fix for the gateway's non-ping SSE timeout (research/omniroute-non-ping-sse-stream-timeout.md) assumed --cache-reuse worked, so long re-prefills can still hit that timeout.
  • Testing for token corruption (typos, broken tool-call keys): flip one key at a time in the server's .env, then run docker compose up -d llama-server. Blank LLAMA_SPEC_ARGS rules speculative decoding in or out, LLAMA_KV_CACHE_TYPE=f16 does the same for the quantized KV cache (lower LLAMA_CTX_SIZE so it fits), and LLAMA_CACHE_RAM=0 turns off the RAM prompt cache. A drafter's quant can't cause wrong tokens: the target model verifies every draft token, so a worse drafter only lowers acceptance.
  • LLAMA_CACHE_RAM (--cache-ram, MiB) is the host-RAM budget for saved prompt-cache entries. When a slot is reused, llama.cpp parks the old prompt's state in RAM so it can be restored instead of re-prefilled. The llama.cpp default is too small for ~40k-token agent prompts (entries are ~2 GiB each), so entries got evicted and requests re-prefilled in full. Raising it is the only place spare DDR RAM helps; it does not speed up decode. Keep it well below the host's free RAM. Evidence and the A/B method: research/qwen3.8-27b-throughput-tuning.md.
  • --jinja enables the chat template that tool calling depends on. Without it, tool-use requests fail outright.
  • Tool-calling reliability of this model family on llama.cpp has open upstream parser bugs: research/qwen3.8-27b-tool-calling.md.
  • Speculative decoding (DFlash2): all its flags live in one key, LLAMA_SPEC_ARGS: --spec-type draft-dflash, the drafter GGUF (-md, fetched by the downloader from models.yml) the draft length (--spec-draft-n-max), and --spec-draft-p-min, which ends a draft early once DFlash2's own confidence in the next token drops below the threshold. Long thinking output at the recommended sampling temperature gets low acceptance, so short confident drafts waste less verify work than long ones (common/speculative.cpp, DFlash2 lattice walk). One key means a single .env edit swaps the drafter or turns speculation off (leave it blank) for an A/B test. A blank server value differs from .env.example, so update.sh will ask about it on every run until it's restored (operations → Env sync). It replaced MTP (draft heads inside the target GGUF). Upstream: llama.cpp#27342. Draft acceptance can collapse at high slot counts (llama.cpp#27117), another reason LLAMA_PARALLEL stays low.
  • No classifier backend: the separate qwen-classifier was removed to free VRAM for the drafter. Anything that needs a small "fast" model must use the main model.
  • No host port: see network access → Backends.

omniroute

The gateway. Its runtime configuration (provider connections, API keys) lives in its own database and is managed through the dashboard, not in this repo: see gateway.md.

  • OMNIROUTE_MEMORY_MB + mem_limit: the default heap is sized for dashboard-only use. OmniRoute's Docker guide recommends more for coding-agent traffic, and mem_limit is the matching container ceiling.
  • stop_grace_period: SQLite needs time on shutdown to checkpoint its WAL back into the main database file. If this is shortened, recent writes (keys, provider connections) can be lost.
  • Healthcheck is a TCP connect, not an HTTP probe. The image has only node (no curl, wget or python), and OmniRoute's Docker guide accepts a TCP probe as a liveness check.

qdrant

A vector store. It has no authentication, so its port is bound to the server's loopback interface only: see network access → qdrant.