14. Model families and example applications¶
The fucina_llm module root (src/llm.zig) exposes each model family as a
namespace — llm.qwen3.{model,train}, llm.kimi3.model,
llm.qwen35.{model,chat},
llm.gemma.{gemma4,gemma4_train,moe,moe_route,moe_route_tensor},
llm.diffusion_gemma.model, llm.deepseek2.model, llm.glm4moe.model,
llm.deepseek4.model, llm.inkling.{model,mmproj,chat}, llm.parakeet.*,
llm.speculative.* — while the
generic helpers (llm.weights, llm.kv_cache, llm.kv_persist, llm.tokenizer,
llm.spm_tokenizer, llm.sampler, llm.logit_processor, llm.llguidance,
llm.chat, llm.data, llm.gguf_meta, llm.ptqtp_gguf, llm.cartridge,
llm.cartridge_fleet, llm.engram,
llm.unicode_categories) stay flat and are covered in §13. This section
documents the per-family model
APIs, their runner CLIs, and the example applications under examples/.
Weight containers (LinearWeight and its quant arms), KvCache, tokenizers,
sampling, chat orchestration, and speculative decoding are §13 material;
GGUF parsing is §12; LoRA/optimizer/ES mechanics are §11.
14.1 Conventions shared by every family¶
(src/llm/*/model.zig, src/llm/gguf_meta.zig)
Config from GGUF metadata. Each family's Config.fromGguf(file)
(deepseek4/inkling additionally take an allocator; kimi3 is the one
non-GGUF family — it loads a reference checkpoint directory, §14.7) reads
hyperparameters from the standard GGUF key convention: the value of
general.architecture (e.g. "qwen3", "qwen3moe", "qwen35", "gemma4",
"diffusion-gemma", "parakeet") prefixes every key —
<arch>.block_count, <arch>.embedding_length,
<arch>.attention.head_count, <arch>.rope.freq_base, and so on — so one
loader covers every size of a family without hardcoding. The vocab size comes
from the token_embd.weight tensor shape, not a key. The llm.gguf_meta
helpers (metaInt, metaIntOpt, metaFloat, metaFloatOpt) implement the
prefixing plus a per-family zero policy: qwen3/qwen35 reject present-but-zero
required ints (.reject_zero — every config int is structurally positive),
gemma accepts zeros (.accept_zero — keys like
attention.shared_kv_layers are legitimately 0). Missing or malformed keys
surface as Error.InvalidConfig (parakeet: Error.MissingMetadata;
parakeet also departs from the tensor-shape vocab convention — its
Config.fromGguf reads parakeet.vocab_size from metadata).
Loader entry points. The qwen3, qwen35, gemma4 and diffusion_gemma
families expose the same pair (the deepseek2, glm4moe, deepseek4, and inkling
loaders read their Config from the file internally; glm4moe additionally
takes max_positions, and deepseek2/glm4moe/deepseek4 take a
LoadOptions):
pub fn loadGguf(ctx: *ExecContext, io: std.Io, path: []const u8, config: Config) !Model
pub fn loadGgufFromFile(ctx: *ExecContext, file: *gguf.File, config: Config) !Model
// qwen35 takes `file: *const gguf.File` (it never calls takeMapping);
// a mutable pointer coerces, so caller code is unaffected
loadGguf opens the file itself (mmap via gguf.File.loadMmap for qwen3,
gemma4 and diffusion_gemma; qwen35's convenience arm uses the eager
gguf.File.load). loadGgufFromFile takes an already-parsed gguf.File so
the caller can build a tokenizer from the same file's metadata without a
second read — the pattern every runner uses:
var file = try fucina.gguf.File.loadMmap(alloc, io, path);
defer file.deinit();
const config = try llm.qwen3.model.Config.fromGguf(&file);
var model = try llm.qwen3.model.Model.loadGgufFromFile(&ctx, &file, config);
defer model.deinit();
var tok = try llm.tokenizer.Tokenizer.initFromGguf(alloc, &file, .{});
defer tok.deinit();
Weights are materialized through llm.weights.LinearWeight.load (§13), which
keeps the GGUF dtype resident — f32/f16/bf16 and the quant forms (Q8_0,
Q4_K/Q5_K/Q6_K and the other ggml types) run their own packed kernels; no
global dequantization happens at load. Sibling projections that share a dtype
and layout are fused at load (weights.fuseLinear): q/k/v into one QKV
matrix, gate/up into one gate_up matrix — one wider GEMM per block instead of
two or three. Layer loading is parallelized across the work pool
(gguf_meta.parallelLoadLayers). Ownership: Model.deinit releases every
weight; when expert blocks borrow from the mmap (qwen3 MoE when a
single-file GGUF is mmap'd — split GGUFs' experts are copied, and opt-in
expert disk streaming (LoadOptions.moe_stream) leaves the mapping with the
caller; gemma4/diffusion_gemma under borrow_experts) the model takes the
mapping via file.takeMapping() and unmaps it last in deinit. On GPU
builds nothing changes at this API level — offload decisions are per-GEMM
work-gates inside the shared kernels (§9); the two model-level GPU knobs are
gemma-MoE's raw expert representation (14.4) and diffusion_gemma's
convertDenseWeightsToF16 (14.5).
Forward/decode surface. The autoregressive families share one contract:
forwardLastLogits(ctx, token_ids)— cacheless whole-sequence forward; returns the last position's[1, vocab]logits (caller deinits). Empty input isError.InvalidSequenceLength.initKvCache(ctx, capacity)/ qwen35'sinitCache— build the streaming cache sized forcapacitypositions. This is the duck-typed construction seam the genericllm.chat.Conversationembedder uses (§13).forwardStep(ctx, kv, token_ids, pos0)— processtoken_idsat absolute positionspos0..pos0+len, append their K/V, advance the cache bylen, return the last row's logits. Contract:kv.len == pos0orError.InvalidSequenceLength;kv.len + len <= kv.capacityorkv_cache.Error.KvCacheOverflow. Prefill is one call on a fresh cache withpos0 == 0(last-token logits equalforwardLastLogits); decode is a one-token call atpos0 == kv.len.forwardStepAllLogits(qwen3, gemma4) — same KV semantics, but returns[len, vocab]logits for every appended position: the speculative-decoding verify entry (§13 — one batched pass scores all draft positions for ~one step's weight traffic).forwardStepBatch(qwen3, gemma4) — lockstep multi-stream decode, 14.2.generate(ctx, kv, prompt_tokens, out_tokens, options)(qwen3, gemma4; not qwen35) — greedy loop (argmax;GenerateOptions{ .max_new_tokens, .stop_token = null }); resetskvfirst, returns the count written. diffusion_gemma's block-diffusiongeneratehas its own options and returns aGenerateResult(14.5). Sampled decoding is composed by the callers fromllm.sampler(§13).forward*Profiledvariants takeio: std.Ioand a family-specificForwardProfileaccumulator (per-block wall-clock buckets; the--profilerunner flag).
All forward entries take *const Model and mutate only the ExecContext,
the cache, and (profiled) the profile struct; a loaded model is read-only.
ExecContext is single-threaded (§6), so concurrent streams over one model
need one context and one cache per thread. Returned logits are caller-owned
constants (deinit them); no exec scope is required for inference.
14.2 Qwen3 — dense and MoE (src/llm/qwen3/model.zig)¶
The reference transformer family and the most complete runner: standard GQA
attention with per-head q/k RMSNorm and full RoPE, SwiGLU FFN — dense, or a
top-k routed mixture (qwen3moe, e.g. 30B-A3B) selected purely by GGUF
metadata.
pub const Config = struct {
vocab_size, hidden_size, intermediate_size, num_layers,
num_attention_heads, num_key_value_heads, head_dim: usize,
rms_norm_eps, rope_theta: f32,
num_experts: usize = 0, // 0 = dense
num_experts_used: usize = 0,
moe_intermediate_size: usize = 0,
norm_topk_prob: bool = true,
moe_expert_top_p: f32 = 1.0, // adaptive expert top-p; 1.0 = full top-k (runtime knob, not GGUF)
pub fn isMoe(self: Config) bool
pub fn qwen3_0_6b() Config // hardcoded 0.6B reference config
pub fn fromGguf(file: *const gguf.File) !Config
};
fromGguf reads <arch>.{embedding_length, feed_forward_length, block_count,
attention.head_count, attention.head_count_kv, attention.key_length,
attention.layer_norm_rms_epsilon, rope.freq_base} plus the MoE trio
{expert_count, expert_used_count, expert_feed_forward_length}; a model with
no expert_count key stays dense. Validation (at load) rejects zero heads,
non-divisible GQA grouping, odd head_dim, and inconsistent MoE fields with
Error.InvalidConfig.
test "qwen3 reference config" {
const cfg = llm.qwen3.model.Config.qwen3_0_6b();
try std.testing.expect(!cfg.isMoe());
try std.testing.expectEqual(@as(usize, 28), cfg.num_layers);
try std.testing.expectEqual(@as(usize, 8), cfg.num_key_value_heads);
}
compiled & run in CI ✓
pub const Error = weights.Error || error{ InvalidConfig,
InvalidSequenceLength, MismatchedKvCaches }. Public surface on Model:
loadGguf, loadGgufOptions, loadGgufFromFile, loadGgufFromFileOptions
(opt-in MoE expert disk streaming, LoadOptions.moe_stream), deinit,
forwardLastLogits, forwardLastLogitsProfiled, initKvCache,
forwardStep, forwardStepProfiled, forwardStepAllLogits,
forwardStepBatch, forwardStepBatchSpans, generate, decoratePtqtp, savePtqtpGguf (§10.9);
plus GenerateOptions, ForwardProfile, MoeStreamOptions, LoadOptions,
and applyExpertTopP at module level.
Load specifics: when the GGUF is mmap'd, MoE expert stacks
(ffn_{gate,up,down}_exps.weight) are borrowed zero-copy from the
mapping (weights.loadMoeRhs with borrow = file.is_mmap and
!file.isSplit() — split GGUFs cannot hand over their multiple mappings, so
their experts are copied) instead of copying multi-GB tensors; the model
owns the mapping and unmaps it last.
A missing output.weight means tied embeddings (token_embedding.cloneView).
The MoE FFN routes on the host (routerTopK with
normalize_selected = norm_topk_prob) and runs the router-weighted SwiGLU
mixture through weights.moeSwiGluFfnSeq: decode (seq 1) uses a fused
expert-parallel GEMV, prefill groups tokens by expert so each expert's
weights are read once per batch.
initKvCache builds a uniform-geometry f16 KvCache
(KvCache.init(ctx, num_layers, num_key_value_heads, head_dim, capacity)).
Qwen3 is the only family whose attention also accepts a q8_0 cache
(construct it with kv_cache.KvCache.initWithDtype(..., .q8_0), §13; the
runner flag is --cache-type q8_0 — half the KV memory, and since the
integer q8xq8 score path also the long-context speed option: decode meets
f16 by ~8k context and the halved footprint doubles how much context a
RAM budget holds).
var file = try fucina.gguf.File.loadMmap(alloc, io, "models/Qwen3-0.6B-Q8_0.gguf");
defer file.deinit();
const config = try llm.qwen3.model.Config.fromGguf(&file);
var model = try llm.qwen3.model.Model.loadGgufFromFile(&ctx, &file, config);
defer model.deinit();
var kv = try model.initKvCache(&ctx, 512);
defer kv.deinit();
var prefill = try model.forwardStep(&ctx, &kv, &.{ 151644, 872, 198 }, 0); // [1, vocab]
defer prefill.deinit();
var step = try model.forwardStep(&ctx, &kv, &.{9707}, kv.len); // one decode step
defer step.deinit();
// requires model assets to run
Lockstep batch decode. forwardStepBatch(ctx, caches, token_ids) decodes
one new token per stream, each stream backed by its own sibling cache from
this model's initKvCache (same dtype, distinct pointers, layer count
matching the model — violations return Error.MismatchedKvCaches; a full
cache returns KvCacheOverflow). Row s of the returned
[n_streams, vocab] logits is stream s's next-token distribution and every
cache advances by one. The dense trunk (QKV/O-proj, FFN or MoE mixture,
lm_head) runs as ONE m=n pass — weights are read once for all streams, the
batch-decode bandwidth win — while RoPE positions, KV appends and attention
stay per-stream (ragged, each row against its own cache at its own position).
Per-row numerics match per-stream forwardStep bit-for-bit below the
m-dependent kernel thresholds (quantized x4-packed kernels engage at n >= 4,
fused FFN at seq >= 12, tiled attention at seq >= 48); beyond them rows can
differ by ~1e-6 reassociation drift. The same thresholds bound
forwardStepAllLogits against per-token steps.
var kv_a = try model.initKvCache(ctx, 256);
defer kv_a.deinit();
var kv_b = try model.initKvCache(ctx, 256);
defer kv_b.deinit();
var a = try model.forwardStep(ctx, &kv_a, &.{ 151644, 872 }, 0);
a.deinit();
var b = try model.forwardStep(ctx, &kv_b, &.{ 151644, 8948 }, 0);
b.deinit();
// One m=2 weight pass decodes both streams; row s = stream s's logits.
var logits = try model.forwardStepBatch(ctx, &.{ &kv_a, &kv_b }, &.{ 9707, 3838 });
defer logits.deinit();
// requires model assets to run
Speculative decoding is available on this family: the runner's --spec
drives the draft-model-free SAM + Token-Recycling cascade from
llm.speculative (§13) with forwardStepAllLogits as the verify pass;
--spec-ref doc.txt injects a reference document the drafter can copy spans
from. Output is lossless (greedy streams verified identical with and without
--spec).
Runner (examples/qwen3/main.zig, the full CLI surface is documented in the
README):
# chat / REPL, sampling flags, GPU offload
zig build qwen3 -Doptimize=ReleaseFast -- models/Qwen3-0.6B-Q8_0.gguf \
--chat "What is the capital of France?" --no-think \
--temp 0.7 --top-k 40 --top-p 0.9 --seed 42
zig build qwen3 -Dgpu=metal -Doptimize=ReleaseFast -- models/Qwen3-0.6B-f16.gguf --repl
# raw completion, speculative decode, lockstep multi-stream bench
zig build qwen3 -Doptimize=ReleaseFast -- models/Qwen3-0.6B-Q4_K_S.gguf \
--prompt "The capital of France is" --gen 64 --spec
zig build qwen3 -Doptimize=ReleaseFast -- models/Qwen3-0.6B-Q4_K_S.gguf \
151644,872,198,9707 --gen 64 --bench 3 --streams 4
# q8_0 KV cache; tokenizer / logit parity oracles
zig build qwen3 -Doptimize=ReleaseFast -- models/Qwen3-0.6B-Q8_0.gguf \
--prompt "..." --gen 256 --cache-type q8_0
zig build qwen3 -Doptimize=ReleaseFast -- models/Qwen3-0.6B-Q8_0.gguf --tokenize input.txt
# constrained decoding (§13.6; needs -Dllguidance=true): the reply must
# satisfy the JSON schema / regex / Lark grammar; composes with --spec
zig build qwen3 -Dllguidance=true -Doptimize=ReleaseFast -- models/Qwen3-0.6B-Q8_0.gguf \
--chat "Give me facts about Paris." --no-think \
--json-schema '{"type":"object","properties":{"city":{"type":"string"},"population":{"type":"integer","maximum":99999999}},"required":["city","population"],"additionalProperties":false}'
zig build qwen3 -Dllguidance=true -Doptimize=ReleaseFast -- models/Qwen3-0.6B-Q8_0.gguf \
--prompt "The answer is" --gen 32 --regex ' (yes|no)\.'
# --json-schema @schema.json / --lark @grammar.lark read the grammar from a file
14.2.1 LoRA fine-tuning (src/llm/qwen3/train.zig)¶
llm.qwen3.train trains LoRA adapters over a frozen, possibly quantized
qwen3.Model (dense only — MoE configs return Error.MoeUnsupported). The
trainer mirrors the inference forward op-for-op but routes every frozen
projection through the differentiable frozen-RHS dot (gradients flow to f32
activations only; weight memory stays quantized/f16) and adds trainable A/B
deltas on the projections selected at comptime. Mechanics — adapters,
optimizers, checkpoints, exec scopes — live in §11; this is the entry-point
map.
pub const Targets = struct { q: bool = true, k: bool = false, v: bool = true,
o, gate, up, down: bool = false };
pub const ignore_index: usize = std.math.maxInt(usize);
pub fn Trainer(comptime targets: Targets) type
Module-level symbols: Error (MoeUnsupported, ExecScopeRequired,
InvalidSequenceLength, LabelLengthMismatch, InvalidLayerRange,
InvalidInjection, InvalidCartridge, InvalidCapture, InvalidPacking,
InvalidEngram, CartridgeCheckpointUnsupported), Targets,
ignore_index, ModelLayer (test seam:
the model's per-block layer type), Hidden
(fucina.Tensor(.{ .seq, .embed })), Injection ({ pos, row } — a
differentiable single-row embedding override), ForwardOptions
({ start_layer = 0, layer_count = null, inject = null } plus the
cartridge/engram fields — cartridge, cartridges, capture,
packed_segments, engram — and the module-level
KvCapture/EngramOptions, all §13.10-§13.11 material).
Trainer(targets) members: init(ctx, model, lora.Config, seed) /
deinit; registerAllParams(opt) (registers every A/B under
layers.<i>.<target>.lora_{a,b} on anything with addParamNamed; the
trainer must outlive the optimizer — params and names are borrowed);
saveAdapters(writer) / loadAdapters(reader) /
loadAdaptersWithOptions(reader, optim.LoadOptions) (clean safetensors state
dict, strict one-to-one on load); loss(ctx, tokens, labels) /
lossExt(..., LossOptions) (mean CE against pre-shifted labels,
ignore_index masks; must run inside an open exec scope —
Error.ExecScopeRequired otherwise — and returns a scope-owned borrow;
LossOptions{ .reduction = .mean|.sum, .loss_scale = 1 } is the gradient-
accumulation seam, TRAINING.md §4); lossInjected(...);
evalLastLogits / evalLogits / evalLastLogitsExt (dropout off, no step
advance, run under their own scope, return caller-owned constants);
forwardHidden(ctx, tokens, step, opts) (raw residual stream, scope
required); the cartridge/engram seams (captureKv, initCartridge,
distillLoss/distillLossExt, lossForwardExt,
evalLogitsExt/evalLogitsRows, embedLastHidden, freeTransientRope)
are §13.10-§13.11 material; the checkpoint_layers field enables
recompute-in-backward per
layer; n_enabled and LayerAdapters are the comptime target plumbing.
Dropout is deterministic per (step, layer, projection) from the base seed;
RoPE tables are cached per sequence length and freed only in deinit.
const Trainer = llm.qwen3.train.Trainer(.{ .q = true, .v = true });
var trainer = try Trainer.init(ctx, model, .{ .rank = 8, .alpha = 16 }, 42);
defer trainer.deinit();
var opt = fucina.optim.AdamW.init(ctx.allocator, .{ .lr = 1e-3 });
defer opt.deinit();
try trainer.registerAllParams(&opt);
const scope = ctx.openExecScope();
defer ctx.closeExecScope(scope);
const tokens: []const usize = &.{ 1, 2, 3, 4 };
const labels: []const usize = &.{ 2, 3, 4, llm.qwen3.train.ignore_index };
var loss = try trainer.loss(ctx, tokens, labels);
try loss.backward(ctx);
try opt.step(ctx);
opt.zeroGrad();
// requires model assets to run
The end-to-end loop — fine-tune (zig build finetune), merge adapters into
dense weights (zig build export-gguf -- --adapters ... --alpha ...),
re-quantize, serve — is scripted in
examples/finetune/README.md; the
gradient-free twin is zig build es-finetune (§11, TRAINING.md §13).
14.3 Qwen3.5 — Gated-DeltaNet hybrid (src/llm/qwen35/model.zig)¶
The qwen35 GGUF arch is a hybrid linear-attention transformer (sibling
of qwen3next, not a Qwen3 variant): every full_attention_interval-th block
is full GQA attention (fused Q+gate projection, per-head q/k RMSNorm,
multi-section/partial RoPE, sigmoid output gate); the rest are DeltaNet
linear blocks — a causal depthwise conv1d feeding a gated delta-rule
recurrent scan over per-v-head state matrices. Both feed a SiLU dense FFN.
Config adds, on top of the usual attention keys: rope.dimension_count
(rope_n_rot — partial RoPE when < head_dim), rope.dimension_sections
(rope_sections: [4]i32), full_attention_interval (default 4), and the SSM
dims ssm.{conv_kernel, inner_size, state_size, time_step_rank, group_count}
(ssm_d_conv/d_inner/d_state/dt_rank/n_group), plus nextn_predict_layers
and expert_count. Config.isRecurrent(il) implements the block schedule;
isMoe() mirrors qwen3. Validation rejects qwen35moe and MTP/NextN
variants with Error.UnsupportedVariant (dense text path only).
DeltaNet heads may be non-uniform: ssm_dt_rank v-heads over
ssm_n_group q/k heads (numVHeads % numKHeads == 0 required), with the
q/k heads broadcast onto the v-heads tiled — v-head h reads q/k head
h % numKHeads, matching ggml_gated_delta_net's iv1 % ne1 semantics in
both the recurrent and the batched chunked scan. Qwen3.5 dense is uniform
(1:1); Ternary-Bonsai-27B (a ternarized Qwen3.6-27B, general.
architecture = "qwen35", weights in the Q2_0 g128 container — §10.7) runs
48 v-heads over 16 k-heads across 64 blocks (16 full-attention + 48 linear).
Its tokenizer declares tokenizer.ggml.pre = "qwen35" (§13.5.1): the qwen2
rules with \p{M} combining marks folded into the word class. Loading it is
the ordinary flow — every projection (embeddings and LM head included) is a
.q2_0 LinearWeight, logit-parity-validated against the PrismML llama.cpp
fork (argmax match at pp1..pp512, cosine ≥ 0.9998 — ≥ 0.99999 on the BLAS
prefill arm — token-ID-exact tokenizer).
test "qwen35 hybrid layer pattern" {
const cfg = llm.qwen35.model.Config{
.vocab_size = 151_936, .hidden_size = 1024, .intermediate_size = 4096,
.num_layers = 24, .num_attention_heads = 16, .num_key_value_heads = 2,
.head_dim = 256, .rms_norm_eps = 1e-6, .rope_theta = 1_000_000,
.rope_n_rot = 64, .rope_sections = .{ 11, 11, 10, 0 },
.full_attention_interval = 4,
.ssm_d_conv = 4, .ssm_d_inner = 4096, .ssm_d_state = 128,
.ssm_dt_rank = 32, .ssm_n_group = 16,
};
// Every 4th block is full attention; the rest run the DeltaNet scan.
try std.testing.expect(cfg.isRecurrent(0));
try std.testing.expect(!cfg.isRecurrent(3));
try std.testing.expect(cfg.isRecurrent(4));
}
compiled & run in CI ✓
pub const Error = weights.Error || error{ InvalidConfig,
InvalidSequenceLength, UnsupportedVariant, UnsupportedKvCacheDtype }.
Model surface: loadGguf, loadGgufFromFile, deinit, blockCounts
(.{ attn, linear } counts for --info), forwardLastLogits (cacheless,
chunked DeltaNet scan; logit-parity-validated against llama.cpp on
Qwen3.5-0.8B), initCache, forwardStep, forwardStepWithScanMode,
forwardStepProfiled, forwardStepProfiledWithScanMode; module-level
LinearScanMode and ForwardProfile.
The streaming state is Cache, not a bare KvCache: an f16 attention KV
cache (q8_0 caches are rejected with Error.UnsupportedKvCacheDtype) plus,
per linear layer, a conv window ((d_conv-1)*conv_dim floats) and the
recurrent state matrix (H*Sd*Sd floats) — O(1) state per linear layer
regardless of context. Cache.deinit, Cache.reset (zero all carried
state), Cache.len() (current position). LinearScanMode selects the
DeltaNet prefill path: .chunked (default — exact batched chunked-GEMM) or
.recurrent (exact token-by-token scan, forced even for prefill); both are
exact, the choice is performance/validation.
var file = try fucina.gguf.File.loadMmap(alloc, io, "models/Qwen3.5-0.8B-Q8_0.gguf");
defer file.deinit();
const config = try llm.qwen35.model.Config.fromGguf(&file);
var model = try llm.qwen35.model.Model.loadGgufFromFile(&ctx, &file, config);
defer model.deinit();
var cache = try model.initCache(&ctx, 256); // KV + conv/SSM state
defer cache.deinit();
var prefill = try model.forwardStep(&ctx, &cache, &.{ 9707, 11, 1879 }, 0);
defer prefill.deinit();
var step = try model.forwardStepWithScanMode(&ctx, &cache, &.{0}, cache.len(), .recurrent);
defer step.deinit();
// requires model assets to run
No training entry, no forwardStepAllLogits/forwardStepBatch, no
speculative decoding on this family. Chat lives in llm.qwen35.chat
(src/llm/qwen35/chat.zig): renderPrompt renders the shared ChatML
template with the Qwen3.6 generation-prompt think prefill (<think>\n
opener when thinking is on; the ChatML empty think block when off), and
Engine(TokMod).generate runs one sampled reply per call on a fresh
Cache (the recurrent state cannot be truncated to a token prefix, so
there is no cross-request KV reuse). lmserve serves the family through
it (backend_qwen35.zig — reasoning channel, JSON-schema/regex/Lark
constrained output; LMSERVER.md); Ternary-Bonsai-27B
(README) is the flagship checkpoint. The
CLI is a loader/parity harness:
zig build qwen35 -Doptimize=ReleaseFast -- models/Qwen3.5-0.8B-Q8_0.gguf
zig build qwen35 -Doptimize=ReleaseFast -- models/Qwen3.5-0.8B-Q8_0.gguf --info
zig build qwen35 -Doptimize=ReleaseFast -- models/Qwen3.5-0.8B-Q8_0.gguf --linear-scan chunked
14.4 Gemma 4 — text + MoE (src/llm/gemma/)¶
gemma4 (26B-A4B class) is the geometry-heavy family: 16 query heads over
per-layer KV geometry — interleaved local sliding-window (SWA) and global
layers with different head dims, KV-head counts and RoPE bases, trailing
layers that share an earlier layer's K/V (shared_kv_layers), optional
per-layer embeddings (PLE), per-layer output scales, GeGLU FFNs (shared dense
MLP + a 128-expert top-8 MoE), and final logit softcapping.
Config keys beyond the common set: attention.key_length_swa,
attention.sliding_window, attention.shared_kv_layers,
rope.freq_base_swa, expert_count/expert_used_count/
expert_feed_forward_length, embedding_length_per_layer_input (PLE width,
0 = disabled), final_logit_softcapping, plus the per-layer arrays
gemma4.attention.sliding_window_pattern and
gemma4.attention.head_count_kv (read by gguf_meta.readU32OrBoolArray,
which broadcasts a scalar across layers like llama.cpp's
get_key_or_arr).
Config.fromGguf wraps Config.fromGgufArch(file, "gemma4"); the arch
argument exists because diffusion-gemma shares the identical hparam key set
under its own prefix. Config.borrow_experts is a load-time policy field,
not a GGUF hparam: true (the --experts=borrow flag) borrows MoE experts
zero-copy from the mmap on CPU builds — load in seconds at ~half the RSS
instead of x4-packing ~20 GB — at some decode-throughput cost; the default
packed path favors peak CPU throughput. Numerically identical either way.
deriveGeometry(allocator, n_layer, swa_pattern, kv_heads_in,
shared_kv_layers, head_dim_global, head_dim_swa) !LayerGeometry computes the
per-layer view (is_swa, head_dim, kv_heads, has_kv, kv_ref;
LayerGeometry.deinit(allocator) frees it): the trailing shared_kv_layers
layers store no K/V and instead reference the last same-type writer (offset
2 for SWA, 1 for global).
test "gemma4 shared-KV geometry" {
const alloc = std.testing.allocator;
var geom = try llm.gemma.gemma4.deriveGeometry(
alloc,
4, // n_layer
&.{ true, true, false, true }, // SWA pattern (false = global)
&.{ 4, 4, 8, 4 }, // per-layer KV heads
1, // shared_kv_layers: the last layer stores no K/V
256, // head_dim_global
128, // head_dim_swa
);
defer geom.deinit(alloc);
try std.testing.expect(!geom.has_kv[3]);
try std.testing.expectEqual(@as(usize, 1), geom.kv_ref[3]); // reuses layer 1
try std.testing.expectEqual(@as(usize, 128), geom.head_dim[3]);
}
compiled & run in CI ✓
pub const Error = weights.Error || error{ InvalidConfig,
InvalidSequenceLength, MismatchedKvCaches, MissingMetadata, PleUnsupported,
UnsupportedExpertType,
UnsupportedKvCacheDtype }. Model surface: loadGguf, loadGgufFromFile,
deinit, initKvCache (per-layer geometry:
KvCache.initPerLayer(ctx, geom.kv_heads, geom.head_dim, capacity)),
forwardLastLogits, forwardLastLogitsProfiled, forwardStep,
forwardStepProfiled, forwardStepAllLogits (speculative verify entry —
softcapping applies to every row), forwardStepBatch/forwardStepBatchSpans
(lockstep multi-stream decode, 14.2 — PLE models rejected with
Error.PleUnsupported), generate + GenerateOptions,
ForwardProfile. Only f16 caches are accepted (requireF16KvCache returns
Error.UnsupportedKvCacheDtype for q8_0). Final logits are softcapped when
final_logit_softcapping != 0 (a fused softcap30 kernel serves the
model's actual 30.0 value). The remaining public symbols are loader/forward
plumbing reused by diffusion_gemma and the trainer: max_heads (64),
metaInt/metaIntOpt/metaFloat/metaFloatOpt, LayerGeometry,
deriveGeometry, MoeFfn, PerLayerInject,
SeparateAttentionProjection, FusedAttentionProjectionKind,
FusedAttentionProjection, AttentionProjectionResult,
AttentionProjection (with toResidentF16 and project), Layer,
loadLayers (the pub wrapper over a file-private LayerLoader),
requireF16KvCache,
attnBlock, ffnBlock.
var file = try fucina.gguf.File.loadMmap(alloc, io, "models/gemma-4-26B-A4B-it-UD-Q6_K.gguf");
defer file.deinit();
var config = try llm.gemma.gemma4.Config.fromGguf(&file);
config.borrow_experts = true; // zero-copy experts from the mmap (--experts=borrow)
var model = try llm.gemma.gemma4.Model.loadGgufFromFile(&ctx, &file, config);
defer model.deinit();
var kv = try model.initKvCache(&ctx, 512); // per-layer geometry
defer kv.deinit();
var prefill = try model.forwardStep(&ctx, &kv, &.{ 2, 651, 235 }, 0);
defer prefill.deinit();
var step = try model.forwardStep(&ctx, &kv, &.{651}, kv.len);
defer step.deinit();
// requires model assets to run
MoE expert kernels (moe.zig, moe_route.zig, moe_route_tensor.zig,
survey depth). The expert FFN has two weight representations: per-expert
packed RHS (MoeFfn.gate/up/down, the tested Q6_K/Q8_0 packed kernels —
peak CPU throughput) and raw GGUF-layout blocks
(RawExpertWeights{ gu: .q6_k|.q4_k, dn_blocks, device_owned, borrowed },
plus guBlockCount), used on -Dgpu=metal builds (grouped dequant-in-kernel
Metal GEMMs read them; the loader then keeps ONE representation — tens of
seconds and ~24 GB saved at load), on Q4_K-transcoded experts, under
--experts=borrow, and by the trainer. Four entry pairs cover the
(decode | batch) x (packed | raw) matrix: decodePackedTensor /
batchPackedTensor / decodeRawTensor / batchRawTensor (tagged-tensor
wrappers) over decodePacked / batchPacked / decodeRaw / batchRaw.
Batch entries consume the shared counting-sort route plan re-exported by
moe_route (Plan, BuildResult, build) with the gemma-specific
expert-major scatter (moe_route.scatterInto, deliberately serial to keep
each token's summation order fixed against parity oracles);
moe_route_tensor.scatterGrouped / recordBatch are the tensor-level
scatter and profile hooks.
LoRA fine-tuning (gemma4_train.zig, pointer depth — §11).
llm.gemma.gemma4_train.Trainer(targets) mirrors the qwen3 trainer over the
gemma4 forward: identical Targets struct and defaults (q, v), identical
ignore_index, and the same member set — init(ctx, model, lora.Config,
seed), deinit, registerAllParams, saveAdapters, loadAdapters,
loadAdaptersWithOptions, loss, lossExt + LossOptions,
evalLastLogits, n_enabled, LayerAdapters (per-layer geometry sizes the
k/v adapters); its cartridge seams (captureKv, initCartridge,
distillLoss/distillLossExt, evalLogitsExt/evalLogitsRows,
embedLastHidden, freeTransientRope, ForwardOptions) are §13.10
material. Its Error set encodes the intentional exclusions checked in
init: PleUnsupported (PLE models rejected), SharedKvUnsupported
(any layer with has_kv == false), RawMoeWeightsRequired (MoE layers must
retain raw expert blocks — load with --experts=borrow or a raw-expert
build; the packed inference-only RHS cannot take gradients), plus
CartridgeGeometry, ExecScopeRequired, InvalidSequenceLength,
LabelLengthMismatch.
Runner (examples/gemma4/main.zig,
README — chat/REPL over the SPM tokenizer
(llm.spm_tokenizer) and the generic llm.chat.Conversation; sampling
defaults come from the GGUF):
zig build gemma4 -Doptimize=ReleaseFast -- models/gemma-4-26B-A4B-it-UD-Q6_K.gguf \
--chat "Why is the sky blue?" --experts=borrow
zig build gemma4 -Doptimize=ReleaseFast -- models/gemma-4-26B-A4B-it-UD-Q6_K.gguf \
--repl --system "Answer tersely." --think
zig build gemma4 -Doptimize=ReleaseFast -- models/gemma-4-26B-A4B-it-UD-Q6_K.gguf \
--chat "Why is the sky blue?" --spec # lossless speculative decoding
zig build gemma4 -Dllguidance=true -Doptimize=ReleaseFast -- models/gemma-4-26B-A4B-it-UD-Q6_K.gguf \
--chat "List three facts about the sky." \
--json-schema '{"type":"array","items":{"type":"string"},"minItems":3,"maxItems":3}' # constrained reply (§13.6)
zig build gemma4 -Dgpu=metal -Doptimize=ReleaseFast -- models/gemma-4-26B-A4B-it-UD-Q6_K.gguf \
--chat "..." # MoE expert FFN on the GPU
zig build gemma4 -Doptimize=ReleaseFast -- models/gemma-4-26B-A4B-it-UD-Q6_K.gguf \
2,651,235 --bench 3 --profile # prefill/decode benchmark
14.5 DiffusionGemma — block text-diffusion (src/llm/diffusion_gemma/model.zig)¶
The diffusion-gemma arch is not autoregressive: the transformer is
exactly gemma4 (this module reuses gemma4's layer loader and attn/ffn
blocks), but generation denoises fixed-length token canvases and commits them
block-autoregressively. Two forward modes share one weight set:
encodeStep(ctx, kv, token_ids, pos0) !void— causal prefix pass over the prompt or a finalized canvas; exists only for its K/V side effect (the lm head is skipped), appends and advances the cache, applies the per-layerenc_layer_output_scale. Samepos0/capacity contract asforwardStep.canvasForward(ctx, kv, canvas_ids, sc) ![seq, vocab]— one bidirectional denoiser pass over the canvas at absolute positions[kv.len, kv.len + C). Canvas K/V are written into the cache's scratch region pastkv.lenWITHOUT advancing it (the next step overwrites), so the cache is read-only from the caller's perspective; logits are returned for every row (softcapped).scis the previous step's self-conditioning signal (null on the first step); passing one on a GGUF without theself_cond_*MLP isError.SelfConditioningUnavailable.
Config = { base: gemma4.Config, canvas_length, eb: EbParams };
Config.fromGguf reads the gemma4 keys under the diffusion-gemma. prefix
plus diffusion.canvas_length (required — Error.MissingCanvasLength) and
the optional diffusion.eb_* overrides of EbParams (defaults are the
reference generation_config: max_steps = 48, t_min = 0.4, t_max = 0.8,
entropy_bound = 0.1, stability_threshold = 1,
confidence_threshold = 0.005). Loading additionally requires both per-layer
scales (layer_output_scale via the gemma4 layer loader, the diffusion-only
enc_layer_output_scale into Model.enc_scale) — Error.MissingLayerScale
otherwise — and rejects PLE configs. pub const Error = gemma4.Error ||
error{ MissingCanvasLength, MissingLayerScale, CanvasLengthMismatch,
KvCapacityTooSmall, SelfConditioningUnavailable }.
Model surface: loadGguf, loadGgufFromFile, deinit, initKvCache
(per-layer geometry; capacity must cover prefix + one canvas),
convertDenseWeightsToF16 (dequantize attention q/k/v/o, the shared dense
FFN, the self-conditioning MLP and the lm head to resident f16 so the
m = 256 canvas GEMMs take the f16 GPU path — the --gpu-f16 flag; ~4.6 GB
extra resident on 26B-A4B, pointless without -Dgpu=metal), encodeStep,
canvasForward.
The entropy-bound sampler is exposed as free functions over the canvas
logits: SamplerOptions / SamplerPass (owns results + an optional
ScSignal; deinit(allocator)) / samplerPass(ctx, logits, temp, u,
options) (per-position argmax, entropy of softmax(z/t) and one multinomial
draw, parallelized over positions with caller-pre-drawn uniforms so results
are thread-count independent; also collects the sparse self-conditioning
candidate lists), ScSignal (sparse per-row id/prob lists; deinit),
entropyBoundAccept(results, entropy_bound, order, accepted) (accept
positions by ascending entropy while the cumulative entropy of the
strictly-lower set stays within the bound). The loop drivers:
denoiseCanvas(model, ctx, kv, canvas, DenoiseOptions) !DenoiseResult
(denoise one canvas in place — uniform-random init, temperature schedule
t_max→t_min, per-step acceptance + renoise, stable-and-confident adaptive
stop; DenoiseOptions{ .eb, .seed = 0, .self_conditioning = true, .sampler,
.on_step, .on_step_user } with StepInfo snapshots feeding the runner's
live inline visualization) and
generate(model, ctx, kv, prompt_tokens, out_tokens, GenerateOptions)
!GenerateResult (encode the prompt once, then per block: denoise, trim at
the first EOG token — default ids {1, 106, 50} — or a repetition-loop
onset, append the kept tokens, encoder-pass the canvas back into the cache;
.on_block callback; returns { produced, steps, blocks }).
const dg = llm.diffusion_gemma.model;
var file = try fucina.gguf.File.loadMmap(alloc, io, "models/diffusiongemma-26B-A4B-it-Q6_K.gguf");
defer file.deinit();
const config = try dg.Config.fromGguf(&file); // gemma4 hparams + canvas_length + EB sampler
var model = try dg.Model.loadGgufFromFile(&ctx, &file, config);
defer model.deinit();
const prompt: []const usize = &.{ 2, 651, 235 };
var kv = try model.initKvCache(&ctx, prompt.len + 2 * config.canvas_length);
defer kv.deinit();
var out: [512]usize = undefined;
const result = try dg.generate(&model, &ctx, &kv, prompt, &out, .{
.denoise = .{ .eb = config.eb, .seed = 42 },
.max_new_tokens = 256,
});
_ = out[0..result.produced];
// requires model assets to run
No training entry and no speculative decoding (there is no autoregressive
draft/verify seam). Runner (examples/diffusion_gemma/main.zig,
README; on a TTY the
reply denoises live inline — --no-visual disables):
zig build diffusion-gemma -Doptimize=ReleaseFast -- models/diffusiongemma-26B-A4B-it-Q6_K.gguf \
--chat "Why is the sky blue? Answer in two sentences." --max 256 --seed 42 --experts=borrow
zig build diffusion-gemma -Doptimize=ReleaseFast -- models/diffusiongemma-26B-A4B-it-Q6_K.gguf \
--repl --system "Answer tersely."
zig build diffusion-gemma -Doptimize=ReleaseFast -- models/diffusiongemma-26B-A4B-it-Q6_K.gguf \
--chat "..." --steps 32 --entropy-bound 0.2 --t-max 0.9 --t-min 0.4 # sampler knobs
zig build diffusion-gemma -Dgpu=metal -Doptimize=ReleaseFast -- models/diffusiongemma-26B-A4B-it-Q6_K.gguf \
--chat "..." --gpu-f16
14.6 Parakeet ASR (src/llm/parakeet/)¶
NVIDIA NeMo FastConformer speech recognition (110M hybrid TDT+CTC through
0.6B multilingual TDT), ported stage-for-stage against parakeet.cpp/NeMo.
The pipeline is a chain of free functions over one gguf.File — there is no
monolithic Model struct; ParakeetWeights is a lazy name-keyed cache:
| stage | module | role |
|---|---|---|
| front end | frontend.zig |
WAV → 16 kHz mono f32 → preemphasis → STFT power → log-mel (+ per-feature normalization) |
| subsampling | subsampling.zig |
stride-2 conv2d stack (subsampling_factor, 8x on the shipped models), mel → [T/8, d_model] |
| encoder | encoder.zig |
Conformer layers: rel-pos MHA + conv module + macaron half-step FFNs |
| decoder | decoder.zig |
CTC argmax-collapse, or LSTM predictor + joint (RNNT/TDT greedy) |
| text | tokenizer.zig, transcription.zig |
SentencePiece detokenize; word grouping, timestamps, JSON |
| streaming | streaming.zig |
cache-aware chunked encoder + carried-state RNN-T session |
Config and loading (loader.zig). Config.fromGguf requires
general.architecture == "parakeet" and reads flat parakeet.* keys:
arch (a DecoderArch: ctc, rnnt, tdt, hybrid_tdt_ctc,
hybrid_rnnt_ctc, with hasCtc/hasTransducer/isTdt predicates —
describes which weights exist, not which decoder runs), the encoder set
encoder.{d_model, n_layers, n_heads, ff_dim, feat_in, conv_kernel,
conv_norm_type, subsampling_factor, subsampling_conv_channels,
pos_emb_max_len, xscaling}, the mel front end
preprocessor.{sample_rate, n_mels, n_fft, win_length, hop_length, preemph,
mag_power, log_zero_guard, normalize}, vocab_size/blank_id, the
predictor decoder.{pred_hidden, pred_rnn_layers} +
decoding.max_symbols, the joint joint.{joint_hidden, activation}, and the
TDT duration table parakeet.tdt.durations (required iff the arch is TDT;
max_durations = 16). Derived accessors: vPlus / checkedVPlus (joint
output width = vocab + blank + durations), subsampledFreq /
checkedSubsampledFreq, durationsSlice. Supporting enums: ConvNorm,
Normalize, JointActivation. Streaming-variant models additionally carry
StreamingConfig.fromGguf (null for offline models): att_context_left/
right/style (AttContextStyle.regular|chunked_limited), the
[step0, step>=1] schedules chunk_size/shift_size/
pre_encode_cache_size (+ stepIdx), cache_drop_size,
last_channel_cache_size, valid_out_len, drop_extra_pre_encoded.
Multilingual prompt-conditioned models carry PromptConfig.fromGguf (null
otherwise) with resolveLang mapping a locale to its one-hot index.
expectTensor/TensorClass and validateTensors gate the tensor inventory
at load (f32_required vs quantizable — the GGUFs ship f16/q8_0/q6_k/
q5_k/q4_k variants of the big matmuls; norms, biases and the featurizer stay
f32). loadFeaturizer returns the Featurizer (mel filterbank fb +
window, borrowed zero-copy from the mapping — valid only while the
gguf.File lives); loadPieces decodes the SentencePiece table (outer
slice caller-freed, pieces borrow the mapping). Error covers
NotParakeet, UnsupportedArch, UnsupportedConvNorm, InvalidConfig,
MissingMetadata, TensorNotFound, TensorShapeMismatch,
TensorDtypeMismatch.
Weights (weights.zig). ParakeetWeights.init(ctx, file) /
deinit — a lazy cache mapping tensor names to LinearWeights, built on
first use; enableF32Blas pre-converts f32 GEMM operands for the BLAS path
(the --f32-cache flag); accessors getLinear, getLinearF32, linear,
linearD, linearQkvD (fused QKV), linearPosAllD (all layers'
linear_pos in one GEMM); free borrowF32 (alignment-checked zero-copy f32
view of mapped bytes). Sessions borrow the weights struct; it must outlive
them.
Front end (frontend.zig): Audio (+ deinit), loadWav16kMono /
loadWav16kMonoFile (PCM16/24/32/f32, stereo downmix, linear resample to
16 kHz via resampleLinear), preemphasis, StftParams, Spectrogram,
DftBasis (precomputed direct-DFT basis for the melSpectrogramFast*
variants), stftPower, MelParams, MelSpectrogram (feat-major
feats[m * n_frames + t]), melSpectrogram, melSpectrogramFast,
melSpectrogramFastWithBasis. NeMo-exact: constant-pad STFT, f64
accumulation, per-feature z-score over the valid frames.
Subsampling (subsampling.zig): subsample / subsampleWithWeights
(the offline stride-2 conv stack + linear proj), streamingSubsample (the
causal variant), conv2dPublic (the shared conv2d entry, also exercised by
tests). Encoder (encoder.zig): encode / encodeWithWeights (mel
[n_mels, T] → [T/8, d_model], caller-owned Tensor(2)), built from
conformerLayer = relposAttention (Transformer-XL relative-position
attention) + convModule + feedForwardT, with helpers relPosEncoding,
layerNorm, layerNormByName, layerNormByNameT, linearWT, f32Data,
attnName, convName.
Decoders (decoder.zig): ctcDecode (frame argmax → ctcCollapse /
ctcCollapseWithMeta); tdtDecode / tdtDecodeWithWeights (greedy TDT:
LSTM Predictor (init/deinit/step) + Joint
(init/deinit/encProjAll/step), duration head skips frames);
rnntDecodeFrames + RnntDecodeState (init/deinit/reset) — the
carried-state per-chunk variant streaming uses. The batch decoders return
caller-freed []i32 token ids; rnntDecodeFrames returns !void and
appends into a caller-provided *std.ArrayList(i32).
TokenInfo/TokenMeta optionally collect per-token frame
indices and confidences. Text: tokenizer.detokenize (SentencePiece
piece join, ▁ → space); transcription.Word, groupWords, freeWords,
toJson (per-word timestamps from token frames x frame_sec).
Offline transcription end-to-end:
const pk = llm.parakeet;
var file = try fucina.gguf.File.loadMmap(alloc, io, "models/parakeet/tdt_ctc-110m-f16.gguf");
defer file.deinit();
const cfg = try pk.loader.Config.fromGguf(&file);
const feat = try pk.loader.loadFeaturizer(&file, cfg); // borrows the mmap
var audio = try pk.frontend.loadWav16kMonoFile(alloc, io, "clip.wav");
defer audio.deinit(alloc);
var mel = try pk.frontend.melSpectrogram(alloc, audio.samples, .{
.stft = .{ .n_fft = cfg.n_fft, .hop = cfg.hop_length, .win_length = cfg.win_length,
.mag_power = cfg.mag_power, .preemph = cfg.preemph },
.n_mels = cfg.n_mels,
.log_guard = cfg.log_zero_guard,
.normalize_per_feature = cfg.normalize == .per_feature,
}, feat.fb, feat.window);
defer mel.deinit(alloc);
var w = pk.weights.ParakeetWeights.init(&ctx, &file);
defer w.deinit();
var enc = try pk.encoder.encodeWithWeights(&ctx, &file, cfg, mel.feats, cfg.n_mels, mel.n_frames, &w);
defer enc.deinit();
const ids = try pk.decoder.tdtDecodeWithWeights(&ctx, cfg, &enc, alloc, &w, null);
defer alloc.free(ids);
const pieces = try pk.loader.loadPieces(&file, alloc);
defer alloc.free(pieces);
const text = try pk.tokenizer.detokenize(alloc, pieces, ids);
defer alloc.free(text);
// requires model assets to run
Streaming API (streaming.zig). Two layers:
StreamingEncoder(init(allocator, cfg, StreamingConfig)/deinit/reset/step/layerStack) runs the full cache-aware encoder on one mel chunk: causal subsampling → dropdrop_extra_pre_encodedleading frames (steps >= 1) → the layer stack with carried caches → slice tovalid_out_len(all frames on the last chunk). Per-layer state is aConvCache(depthwise conv tail,init/deinit/reset) and aChannelCache(attention K/V left-context window,init/deinit/reset/advance); the windowed attention itself isstreamingAttnMask+streamingRelposAttention+streamingConformerLayer, withstreamingDepthwiseConvfor the conv module andapplyPromptKernelfor the multilingual one-hot projection.StreamingSession(init(allocator, file, cfg, sc, weights, lang)/deinit) owns the encoder caches, the LSTM predictor + joint, the carriedRnntDecodeStateand the accumulated output. Feed granularities:feedMel(ctx, file, w, mel, n_mels, t)windows a whole clip through the chunk schedule;feedMelChunkprocesses one pre-windowed mel chunk;encodeChunkPromptedreturns a chunk's encoder frames (+ prompt kernel);feedEncoderFramesgreedy-decodes frames you encoded yourself. Non-special tokens accumulate insession.tokens(setcollect_meta = trueto aligntoken_metafor timestamps);<EOU>/<EOB>events are counted ineou_eventsand reset the decoder state for the next utterance (decoder-only, matching the reference).initreturnserror.UnknownLangif a prompt-conditioned model cannot resolvelang.
No training entry, no speculative decoding (not autoregressive text).
Runner (examples/parakeet/main.zig, README):
zig build parakeet -Doptimize=ReleaseFast -- --model models/parakeet/tdt_ctc-110m-f16.gguf \
--audio clip.wav --transcribe # offline; --json --timestamps for word timing
zig build parakeet -Doptimize=ReleaseFast -- --model models/parakeet/tdt_ctc-110m-f16.gguf \
--audio clip.wav --stream # cache-aware chunked pipeline
zig build parakeet -Dparakeet-mic -Doptimize=ReleaseFast -- \
--model models/parakeet/tdt_ctc-110m-f16.gguf --mic # live microphone
zig build parakeet -Doptimize=ReleaseFast -- --model ... --manifest files.txt --decoder ctc
--decoder tdt|ctc picks the head on hybrid models; --lang XX selects the
prompt locale on multilingual models; --threads N caps the worker team.
14.7 Kimi-K3 — KDA/MLA hybrid, architecture parity (src/llm/kimi3/model.zig)¶
The kimi3 family (the Kimi-Linear lineage) is a KDA linear-attention /
Gated-MLA-NoPE hybrid with a latent sigmoid-routed MoE, cross-layer
attention residuals (layers at index % attn_res_block_size == 0 snapshot
their entry into a residual bank that later layers depth-mix against), and
the SiTU gated activation (§4.5). It is the one family outside the GGUF
conventions of §14.1: an architecture-parity model over the tiny f32
reference checkpoint, loaded from a checkpoint directory.
Config.fromJsonFile(allocator, io, path) parses the reference
config.json (text_config, including the 1-based KDA layer list from
linear_attn_config — Config.isKdaLayer(layer_idx) implements the
schedule, Config.deinit(allocator) frees the list) and
Model.load(allocator, io, ctx, dir) reads <dir>/config.json plus
<dir>/model.safetensors (safetensors.File.loadMmap).
Heavy lifting goes through the shared exec ops — matmulTransB,
causalDepthwiseConv1dAxisRank, kdaRecurrent (§4.13),
gatedRank(.situ), rmsNormMulAxisRank — while the depth mixture and the
small MLA core are model-local routines. Model surface: load, deinit,
forward(ctx, tokens) (full-sequence forward: token ids, []const u32,
to [seq, vocab] logits) and forwardProbed(ctx, tokens, probe) with
Probe ({ context, callback }) — a stage observer that receives the
same per-layer intermediates the reference dumps. No KV/state cache, no
runner CLI, no GGUF path; the golden tests pin the forward against
reference activations.
14.8 Example applications¶
Beyond the family runners, six applications exercise the library end to end.
nanochat (examples/nanochat/,
README) is a from-scratch CPU port of
karpathy/nanochat: BPE tokenizer training (rustbpe-equivalent), GPT base
pretraining (grad-accum loop, Muon+AdamW, checkpoint/resume), supervised
fine-tuning on the task mixture, bits-per-byte evaluation, and an
interactive chat CLI with a calculator tool. The port is example-local —
everything composes from the public facade — and every stage is validated
against the fp32 Python reference under a tiered parity ladder.
zig build nanochat -- tok-train|base-train|sft|eval-bpb|chat ....
lmserve (examples/lmserve/, README;
LMSERVER.md is the full design doc) is an OpenAI- and
Anthropic-compatible HTTP server over the in-tree language models: Chat
Completions (POST /v1/chat/completions), the stateless Responses API
(POST /v1/responses), and the Anthropic Messages API
(POST /v1/messages — a translation layer onto the same engine), with SSE
streaming in all three dialects, hermes-style function calling on the
qwen3-family backends (tool declarations render into the prompt, the
client executes; tool_choice is grammar-forced on -Dllguidance=true
builds), JSON-schema/regex/Lark constrained output (-Dllguidance=true
builds), an optional Host-header allowlist (--allow-host), and a bounded
request queue in front of one inference worker — a per-request stream pipe
decouples generation from client sockets, so a stalled client never blocks
the worker. Serving levers: --kv-slots keeps interleaved conversations
warm (guarded against RAM overcommit; --kv-cache-dir adds a disk tier,
and a request sharing a prefix with another resident slot copies those KV
rows instead of re-prefilling them, §13.4), --batch N lockstep-decodes
queued requests together with per-stream failure isolation (§13.8),
--spec adds lossless self-draft speculative decoding (composes with
reuse and stop sequences, §13.9), and --cartridge/--fleet mount
trained KV-prefix cartridges (§13.10). The GGUF's general.architecture
picks the backend (qwen3 / qwen3moe / qwen35 / gemma4 / diffusion-gemma /
inkling); --nanochat <dir> serves a nanochat checkpoint.
zig build lmserve -- <model.gguf> [--host H] [--port N] [flags].
facedetect (examples/facedetect/,
README) runs the insightface
buffalo_l pack — SCRFD det_10g detection (boxes + 5-point landmarks),
ArcFace IResNet-50 recognition (512-d embeddings, 1:1 verification),
GenderAge MobileNet-0.25 attributes, a MiniFASNet x2 anti-spoof ensemble, and
2d106det/1k3d68 dense landmarks — from self-contained GGUFs, as a pure-Zig
port of face-detect.cpp. It is the CNN workout for the core op set: the
hand-mapped nets (recognizer.zig, scrfd.zig, genderage.zig) drive the
public tagged-Tensor facade with channel-last [h, w, c] conv2d,
pool2d, prelu, channelAffine and upsample (§4), with GGUF dequant,
layout repack and BatchNorm folding at load; the anti-spoof and landmark nets
replay a GGUF-embedded node list through an app-level graph dispatcher over
ExecContext ops. Decision-critical control paths (cv2-exact letterbox,
umeyama alignment, NMS) are verbatim scalar ports; detect/analyze JSON is
byte-identical to the reference, embeddings agree at cosine >= 0.999999.
zig build facedetect -- detect|embed|verify|analyze|landmarks|bench ....
locate_anything (examples/locate_anything/,
README) runs NVIDIA
LocateAnything-3B open-vocabulary detection — MoonViT vision tower +
MLP projector + Qwen2.5-3B, detection in token space via coordinate tokens —
from one GGUF, ported from locate-anything.cpp and validated stage by stage
(token streams id-exact in all three decode modes, detections JSON
byte-identical at f32). Everything numeric runs on stock tensor ops: the
interleaved 2D vision RoPE is a hand-filled RopeTable, ViT attention is the
bidirectional grouped-attention arm, the MTP block-diffusion mask rides the
additive-bias attention arm, and every linear goes through LinearWeight
(so f16/quant arms and BLAS/Metal/CUDA GEMM dispatch apply unchanged).
Decode modes hybrid (parallel box decoding with AR fallback), slow
(pure autoregressive), fast (MTP-only). ~1.2-2.4x faster than the
reference CLI depending on ISA and dtype.
zig build locate-anything -- detect --model ... --input scene.png --prompt '...'.
nam (examples/nam/, README) is a complete
Neural Amp Modeler ecosystem port: load any upstream-format 0.5.0-0.7.x
.nam guitar-amp capture (WaveNet incl. gated/grouped/FiLM variants, LSTM,
ConvNet, Linear, SlimmableContainer), render offline or play live through
real audio devices (vendored miniaudio, allocation-free lock-free callback,
MIDI control), append cabinet IRs and multi-stage chains, train new profiles
from a reamp pair (classic/A2/packed WaveNet recipes, ESR validation) and
exchange them losslessly with upstream tooling (GGUF interchange recovers a
byte-identical .nam). It exercises the streaming-convolution regime: tiny
L1-resident models where per-block latency dominates — standard WaveNet at
~49 us per 64-frame block on one x86 P-core (~27x realtime), numerically
within 2.3e-6 of upstream tools/render with a strict-contract SIMD tanh.
zig build nam -- live|render|train|profile|bench|devices ....
omnivoice (examples/omnivoice/,
README) is multilingual zero-shot TTS
(646 languages): a MaskGIT non-autoregressive decoder on a Qwen3-0.6B
backbone drives the Higgs Audio v2 codec — HuBERT semantic encoder + DAC
acoustic codec + 8-codebook RVQ at 25 fps / 24 kHz — for auto voice, voice
design (attribute prompts) and voice cloning (reference WAV + transcript).
RVQ codes and MaskGIT token streams are byte-exact vs omnivoice.cpp at F32
with a fixed seed; decoded audio cosine >= 0.99999; 2.3-4.6x faster than the
reference's CPU backend on M1 Max. The example doubles as a library
(pipeline.synthesize/synthesizeStream, ring-buffered play.Player) and
ships a WAV↔RVQ codec tool. zig build omnivoice -- tts --model ... --codec
... --lang English -o out.wav (see 14.9 for the flag shape).
The didactic set (single-main.zig examples under examples/):
smoke/main.zig(zig build smoke) — the minimal facade round trip: two variables,dot,sumAll,backward, gradients printed.spirals/main.zig(zig build spirals) — two-spirals MLP trained with every optimizer (SGD/AdamW/Muon/APOLLO/APOLLO-Mini) + param groups, lr schedule, clipping; proves bit-exact checkpoint/resume (§11).finetune/main.zig(zig build finetune, README) — the qwen3 LoRA loop of 14.2.1 on a built-in pirate SFT set;--data PATH.jsonl,--accum-steps,--verify-gradsgradient-evidence audit.es_finetune/main.zig(zig build es-finetune, README) — the gradient-free twin:fucina.esover the same trainer forward;--mode lora|full,--reward rule|acc|nll, anchored weight decay.es_spirals/main.zig(zig build es-spirals) — from-scratch ES on two spirals; member-parallel evaluation (one ExecContext + model replica per worker); self-verifying (fails below--targetaccuracy).es_ternary_spirals/main.zig(zig build es-ternary-spirals) — the ternary-native ES flagship: packed TQ2_0 genomes are the inference model (§10, §11), trained by trit flips on the real int8 kernels; self-verifying.cartridge/main.zig(zig build cartridge, README) — KV-prefix corpus compression on a frozen qwen3 GGUF (§13.10); corpus-init, distillation training,--equivacceptance gate.cartridge_fleet/main.zig(zig build cartridge-fleet, README) — per-document cartridges trained jointly, cosine-retrieval selection (§13.10).engram/main.zig(zig build engram, README) — conditional n-gram memory graft trained on a frozen qwen3 GGUF (§13.11,ENGRAM.md).
14.9 Example → features → run command¶
Weights are never bundled; RUNNING-MODELS.md lists the
download source for every model row; each example's README
(examples/<name>/README.md) documents its full flag set. The table omits
-Doptimize=ReleaseFast for width — add it to every real run.
| example | demonstrates | run |
|---|---|---|
smoke |
facade: tensors, dot, autograd (§3-§5) |
zig build smoke |
spirals |
optimizers, schedules, checkpoint/resume (§11) | zig build spirals |
es_spirals |
from-scratch ES, member-parallel eval (§11) | zig build es-spirals |
es_ternary_spirals |
ternary-native ES on TQ2_0 kernels (§10, §11) | zig build es-ternary-spirals |
finetune |
qwen3 LoRA SFT, accumulation, data loader (§11, 14.2.1) | zig build finetune -- --model models/Qwen3-0.6B-Q4_K_S.gguf --steps 30 --rank 8 --alpha 16 --save /tmp/qwen3-lora |
es_finetune |
gradient-free LLM fine-tuning (§11) | zig build es-finetune -- --model models/Qwen3-0.6B-Q4_K_S.gguf --reward acc --iterations 100 --population 8 |
qwen3 |
dense+MoE decode, KV cache (f16/q8_0), batch decode, speculative (§13, 14.2) | zig build qwen3 -- models/Qwen3-0.6B-Q8_0.gguf --chat "..." --no-think |
qwen35 |
Gated-DeltaNet hybrid, recurrent cache (14.3) | zig build qwen35 -- models/Qwen3.5-0.8B-Q8_0.gguf --info |
gemma4 |
per-layer KV geometry, MoE experts, SPM chat, speculative (14.4) | zig build gemma4 -- models/gemma-4-26B-A4B-it-UD-Q6_K.gguf --chat "..." --experts=borrow |
diffusion_gemma |
block text-diffusion, EB sampler, live denoise UI (14.5) | zig build diffusion-gemma -- models/diffusiongemma-26B-A4B-it-Q6_K.gguf --chat "..." --seed 42 |
parakeet |
ASR pipeline, streaming encoder, mic capture (14.6) | zig build parakeet -- --model models/parakeet/tdt_ctc-110m-f16.gguf --audio clip.wav --transcribe |
omnivoice |
MaskGIT TTS, HuBERT/DAC/RVQ codec, streaming WAV | zig build omnivoice -- tts --model models/omnivoice/omnivoice-base-Q8_0.gguf --codec models/omnivoice/omnivoice-tokenizer-F32.gguf --lang English -o out.wav |
nam |
streaming conv nets, live audio/MIDI, .nam/GGUF interchange |
zig build nam -- live profile.nam --ir cab.wav |
facedetect |
channel-last conv2d/pool2d/prelu/upsample CNNs (§4) | zig build facedetect -- detect --model models/buffalo_l.gguf --input face.png |
locate_anything |
VLM: ViT + projector + LM, token-space detection | zig build locate-anything -- detect --model models/locate-anything-f32.gguf --input scene.png --prompt '...' |
nanochat |
end-to-end GPT: BPE tokenizer training, pretraining, SFT, bpb eval, chat (14.8) | zig build nanochat -- chat -i <ckpt dir> --tokenizer <tokenizer.bin> -p "..." |
lmserve |
OpenAI- and Anthropic-compatible HTTP server: chat completions + responses + messages, function calling, SSE streaming, constrained output, --spec/--batch/--kv-slots (14.8) |
zig build lmserve -- models/Qwen3-0.6B-Q8_0.gguf --port 8080 |
deepseek2 |
DeepSeek V2/V3: MLA compressed KV cache, MoE decode (README) | zig build deepseek2 -- models/DeepSeek-V2-Lite-Chat.Q8_0.gguf --prompt "..." --gen 64 |
glm4moe |
GLM-4.5 family: native MTP speculative decode, streamed experts (README) | zig build glm4moe -- models/glm45-air/GLM-4.5-Air-Q6_K-00001-of-00002.gguf --prompt "..." --gen 64 --mtp |
deepseek4 |
DeepSeek V4 Flash: CSA/HCA trunk, streamed experts, MTP sidecar (README) | zig build deepseek4 -- <model.gguf> --chat --prompt "..." --moe-stream |
inkling |
Inkling: hybrid rel-bias attention, shortconv sites, sink-shared MoE; parity harness (README) | zig build inkling -- <model.gguf> --prompt "..." --gen 64 |
cartridge |
KV-prefix corpus compression on a frozen qwen3 GGUF (§13.10) | zig build cartridge -- --model models/Qwen3-0.6B-f16.gguf --corpus README.md --p 256 --suffix-max 128 --equiv |
cartridge_fleet |
per-document cartridge fleets: joint training, cosine retrieval (§13.10) | zig build cartridge-fleet -- --model models/Qwen3-0.6B-f16.gguf --docs README.md --equiv --p 256 --suffix-max 128 |
engram |
n-gram memory graft training on a frozen qwen3 GGUF (§13.11) | zig build engram -- --model models/Qwen3-0.6B-f16.gguf --equiv |
ptqtp_spirals |
float-trained MLP decorated post-training with trit-planes, self-verifying (§10.9) | zig build ptqtp-spirals |
ptqtp_qwen3 |
PTQTP-decorate a Qwen3 GGUF in place, NLL before/after, --save GGUF (§10.9, §13.2.1) |
zig build ptqtp-qwen3 -- models/Qwen3-0.6B-Q4_K_S.gguf --planes 2 |
(tool) export-gguf |
transcode/re-emit GGUF, merge LoRA adapters (§11, §12) | zig build export-gguf -- --from-gguf in.gguf --out out.gguf --dtype q8_0 |