Models and Inference Engines¶
Tryll is a model host first and a workflow engine second. This page is about the host side: what counts as a "model", how the server talks to the hardware, what the KV cache is doing under your agents, and how to think about context size and quantisation when you pick one.
Four kinds of models¶
Tryll treats four kinds of models as first-class citizens:
- Language models — generate text. These are what
GenerateandToolCallnodes use. - Embedding models — turn text into fixed-length float vectors.
Retrieveuses one to embed the current user message before searching an embedded string storage. - STT models (speech-to-text) — transcribe audio to text for voice
input. Exposed in every client via the voice-input API (C++
CreateVoiceInput, UnityCreateVoiceInputAsync/TryllVoiceInput, UnrealUTryllVoiceInputComponent). - TTS models (text-to-speech) — synthesise speech from text. Exposed via
the
SpeakandGenerateAndSpeaknodes, with ready-made playback components (TryllSpeakerin Unity,UTryllSpeakerComponentin Unreal). See Add Voice Output to an Agent.
Every entry in the catalog (models.json) declares which kind it is.
Asking for a model of the wrong kind fails fast with a clear error. See
Model Management for the full
catalog schema.
You do not pick an engine per kind. Each catalog entry names the engine
that runs it, so downloading or registering a model is all a session
needs. CreateSession's model_config may carry an optional engines
list of (role, engine) pairs, but only as a warm-up hint: the server
starts those engines at session creation instead of at first use. It
never gates a session or a load, and leaving it empty is normal. (Before protocol v13, CreateSession had four engine fields
that defaulted to Mock; they are gone.)
Inference engines: how the model actually runs¶
An inference engine is the runtime that executes the model against your CPU or GPU. Tryll currently ships these engines:
| Engine | Backend | Status |
|---|---|---|
Mock |
In-process stub | Used for tests; no real inference. |
LlamaCpp |
llama.cpp | Default for language + embedding. Also hosts SNAC TTS (Orpheus, Maya1) via a shared ONNX decoder. GGUF files; CPU + Vulkan / CUDA / ROCm GPU. |
SherpaOnnx |
sherpa-onnx | STT, TTS, and VAD. TTS families: Supertonic and Pocket TTS — see TTS Models. |
OnnxRuntime |
ONNX Runtime | TTS for the Chatterbox family — internal only, not in the public catalog. |
OnnxGenAI, WindowsML, OpenVino, TensorRtLlm |
Reserved | Enum slots exist; implementations land in future releases. |
The engine comes from the model's catalog entry, not from the session (see the C++ API or wire protocol for the optional warm-up hint).
A catalog entry lists one or more variants. Variants describe different files, not different engines: every variant of one model must name the same engine, and the catalog is rejected otherwise.
{
"name": "Llama-3.2-3B-Instruct",
"variants": [
{ "engine": "llama-cpp",
"huggingface_repo": "bartowski/Llama-3.2-3B-Instruct-GGUF",
"files": ["Llama-3.2-3B-Instruct-Q4_K_M.gguf"],
"kv_cache_type": "q8_0",
"tool_call_support": "supported" }
]
}
(The JSON keys are engine, huggingface_repo, and files — an array. Use
local_path + files for a model already on disk. Every language-model
variant must declare tool_call_support as "supported" or "unsupported".)
The server loads a model on the engine its catalog entry names. If this
server build does not include that engine, the load fails with
6008 EngineNotAvailable.
The KV cache: why generation is fast after the first token¶
A language model's prompt can be thousands of tokens long. Naively re-running the full prompt through the model on every new token would be unworkable. Instead, transformer models keep an intermediate per-token state — the key/value cache, or KV cache — that encodes "everything the model has seen so far". Adding one more token costs one more forward pass, not N.
Tryll keeps one KV cache per node, per agent. Each turn, the node compares the new prompt against the cached token sequence:
- Unchanged prefix is kept.
- Stale tail (tokens that differ) is evicted.
- New suffix is processed in batches.
So the cost of "remembering the conversation" is the cost of appending the new user turn's tokens — not the whole history — as long as you are not rewriting the past.
KV cache dtype¶
Each llama.cpp variant can declare a kv_cache_type:
| Value | Bytes/element | Notes |
|---|---|---|
f16 |
2 | Highest quality. |
q8_0 |
~1 | Default when kv_cache_type is absent, on every build. Indistinguishable quality for most chat use. |
q4_0 |
~0.5 | Halves memory vs. q8_0; small quality hit. Useful on memory-constrained GPUs. |
The cache size dominates VRAM / RAM for long conversations. Moving
from f16 to q8_0 roughly halves it; from q8_0 to q4_0 halves
it again. Catalog kv_cache_type is the model-load default. A node's
structural context table can override it per context (Inherit keeps
the catalog value). At a right-sized window (Classify / ToolCall 2048)
a q4_0 override is usually not a measurable VRAM win; the dtype
delta shows up at large n_ctx (around 8192).
Context size and why 4k is not 4k¶
Every language model has a maximum context size — the total
number of tokens the model can attend to at once (prompt + generation
buffer). Tryll reserves a slice at the end for the generation itself
(generation_reserve), and uses the rest as the high water mark
for projection. Tokens beyond the high water mark get trimmed by
token-budget projection before the
model ever sees them.
So a model nominally advertised as "4k context" will, in practice, give you something like 3.5k of usable history — more than enough for most dialog, but worth knowing about when you design a RAG prompt.
Embedding models¶
Embedding models are much smaller and simpler: a stateless encoder that turns text into fixed-dimension vectors, with mean pooling to produce one vector per input. The server calls the model once per query and once per chunk at index-build time; that is the whole story.
An embedding model is required whenever you create an
embedded string storage —
either sent on the wire (Path B) or declared as embedding_model in the
Path A config JSON (not in the *.kb.json records file). The two sides
must match.
The loaded-model pool¶
Language, embedding, STT, and TTS models are loaded once per process into separate caches and reference-counted with the same retention rules:
- Pinned retention — set by
LoadModelRequestfor any of the four kinds. The model stays in memory until you explicitly unload it and no active contexts / sessions reference it. - OnDemand retention (default) — set by the lazy load path when an
agent, voice input, TTS, or embedding use first needs the model. The
server frees unused OnDemand models after
DestroyAgentRequestand again immediately before the next validated load into any cache (cross-cache sweep). Pinned models and models still held by agents are left alone.
VAD models appear in the catalog for download/config, but they are not part of this pin/unload pool — offline STT loads VAD as a dependency of the STT model.
This pair lets you either nail a model in place ("this is the chat
model, always keep it warm") or let the pool breathe when sessions
cycle through models. See
Lifetime and Ownership → Models
for the full reference-counting story, including what happens when
UnloadModelRequest arrives while agents are still using the model.
Downloading and the "Downloaded" status¶
Models you did not bundle with the server can be pulled from Hugging
Face via DownloadModelRequest. The server streams
DownloadProgress frames and finishes with a
DownloadComplete(success=true). After that the model has status
Downloaded and can be loaded. Local is reserved for a model whose
catalog local_path resolved to a file on disk. See
Model Management for the
full lifecycle and the distinction between on-disk statuses and Loaded
(memory).
Edges and pitfalls¶
- No model swap mid-turn. The model a context points to is resolved at agent-creation time. Pinning or unloading any model while an agent is running is safe: unloading the model a running agent depends on only drops the pin, and the model stays resident until that agent's contexts are destroyed.
- KV cache is per node, per agent. Two
Generatenodes using the same model in the same graph still each pay their own KV cost. Budget memory accordingly. - Embedding and language models are not interchangeable. Trying
to point a
Retrievenode at a language model (or aGeneratenode at an embedding model) fails validation with a clear error. - Context-window claims are rough. Official context sizes assume no generation buffer; the high-water mark is what actually governs your prompt budget.
- One base model, several behaviours. An LLM node can run its model with LoRA adapters: small GGUF files trained for that base model. The base weights stay loaded once; only the nodes that name an adapter use it.