1. Introduction and mental model¶
Fucina is an eager, close-to-metal CPU tensor/autograd runtime plus an
LLM/ASR inference stack, written in Zig 0.16. This document is the detailed
reference for the whole library: the public API surface, its exact semantics
(ownership, errors, defaults, thread-safety), and the internal layers you
need to understand to extend it. The structural overview lives in
ARCHITECTURE.md; command cheat sheets live in AGENTS.md;
per-model getting-started recipes live in the per-example
examples/<name>/README.md, with the shared weights table and runtime knobs
in RUNNING-MODELS.md.
Every runnable Zig snippet in this document is machine-verified against the tree:
zig build snippet-check (§2.7, a CI step) extracts every runnable
snippet written as a named test block and runs it against the real
modules; snippets that need model assets are non-test fragments the
harness ignores and are marked // requires model assets to run.
1.1 The two modules¶
The build exposes two library modules (§2):
fucina(src/fucina.zig) — the tensor library: the publicTensorfacade, theExecContextruntime, autograd, quantized formats, training (optimizers, ES, LoRA), and persistence (GGUF, safetensors, checkpoints).fucina_llm(src/llm.zig) — the model stack built on top: GGUF weight binding, KV caches, tokenizers, samplers, chat sessions, speculative decoding, and the model families (Qwen3, Qwen3.5, Gemma 4, DiffusionGemma, DeepSeek V2/V3, GLM-4.5, DeepSeek V4 Flash, Kimi-K3, Inkling, Parakeet ASR).
Applications (examples/, tools/, bench/) sit above both.
1.2 Mental model¶
Five ideas carry the whole library:
Eager, explicit execution. There is no graph compiler, no lazy
evaluation, no fusion pass. Every tensor operation validates its inputs,
allocates its output through the ExecContext, and calls a backend kernel
immediately. What you write is what runs, in the order you wrote it.
One public tensor, typed at comptime. fucina.Tensor(spec) is the only
user-facing tensor type. Its axis tags (names) and rank are part of the
Zig type — checked at compile time — while dimension sizes stay runtime
values. The same facade carries no-grad inference and gradient-tracked
training: a tensor with gradient state records backward information, a
constant does not, and the operation call sites are identical. The raw,
untagged tensor underneath is deliberately not exported (§8).
Tags instead of axis numbers. Operations name the axes they act on
(x.dot(&ctx, &w, .in) contracts the .in axis) and broadcasting is
tag-driven: axes align by name, not by position. The tag algebra is
comptime-only data — it compiles down to stride manipulation on the raw
tensor with zero runtime tagging cost (§7).
Explicit ownership, deterministic cleanup. Tensors are value handles
over reference-counted buffers. Operations return owned results;
defer x.deinit() is the norm. Training loops use exec scopes to own the
flood of intermediates implicitly (§6). Loaded model weights borrow mmap'd
bytes (holder-managed lifetime) or device-resident bytes (freed through
storage release hooks) instead of copying (§8, §12).
Multi-dtype with a sealed policy. Tensors span bool/integer dtypes,
f16/bf16/f32/f64, and the GGML block-quantized formats. What each
dtype branch can do is enforced by the type system, not runtime checks:
.f32 is the differentiable branch; other scalar dtypes are constant typed
tensors (floats additionally get forward-only math); block-quantized tensors
are constant inference tensors that dequantize, gather rows, and serve as
matmul right-hand sides (§3, §10). Float compute/output dtypes follow a
fixed per-op-family policy (§8.3).
1.3 Layer stack¶
Top-down; a band depends only on bands at or below it. Acyclicity of the
production import graph is machine-enforced by zig build arch-check (§2);
the band direction is checked by a development-side dependency-structure
lint whose configuration is not part of this tree (see
ARCHITECTURE.md):
| Band | Contents | Reference |
|---|---|---|
| apps | examples/, tools/, bench/ |
§14 |
| llm | fucina_llm module |
§13, §14 |
| facade | src/fucina.zig public root |
§1–§5 |
| autograd + training | src/ag/, optim/es/lora/persistence |
§5, §11, §12 |
| tagged ops | src/tagged.zig |
§7 |
| exec runtime | ExecContext, src/exec/ |
§6 |
| backends | CPU SIMD, BLAS, Metal/CUDA | §9, §10 |
| tensor/storage/dtype | raw value types | §8 |
1.4 A first program¶
The canonical smoke test — build two variables, contract them, reduce, and differentiate:
const std = @import("std");
const fucina = @import("fucina");
test "first program" {
const alloc = std.testing.allocator;
var ctx: fucina.ExecContext = undefined;
ctx.init(alloc);
defer ctx.deinit();
// x: [batch=1, in=2], w: [in=2, out=1]
var x = try fucina.Tensor(.{ .batch, .in }).variable(&ctx, try ctx.fromSlice(&.{ 1, 2 }, &.{ 2, 3 }));
defer x.deinit();
var w = try fucina.Tensor(.{ .in, .out }).variable(&ctx, try ctx.fromSlice(&.{ 2, 1 }, &.{ 4, 5 }));
defer w.deinit();
var y = try x.dot(&ctx, &w, .in); // contract .in => [batch, out]
defer y.deinit();
var loss = try y.sumAll(&ctx);
defer loss.deinit();
try loss.backward(&ctx);
var gx = (try x.grad(&ctx)).?; // dloss/dx = w^T = [4, 5]
defer gx.deinit();
try std.testing.expectApproxEqAbs(@as(f32, 23.0), try loss.item(), 1e-6);
try std.testing.expectApproxEqAbs(@as(f32, 4.0), (try gx.dataConst())[0], 1e-6);
}
compiled & run in CI ✓
Everything in this snippet — the context lifecycle, tensor specs, construction, ownership, tagged contraction, backward — is unpacked in §3–§6.
1.5 Stability¶
Fucina is a production-oriented core, not a finished 1.0 product: the
package manifest and 0.x tags (v0.1.0) exist so consumers can pin a
version (§2.5), but a 0.x tag is a pin, not a semver stability contract —
the public API may change between tags (see
Current Production Gaps in ARCHITECTURE.md). This
reference describes the tree it ships with; sections marked internal
(§7 library level, §8, backend internals in §9) document machinery that is
explicitly not a stable API.