Skip to content

2. Toolchain, build, and project wiring

2.1 Toolchain (AGENTS.md, README.md)

Fucina is pinned to Zig 0.16.0zig version must print 0.16.0; other versions do not build. build.zig.zon names the package .fucina and its minimum_zig_version = "0.16.0" turns an older toolchain into a proper error (a newer toolchain passes that check but is equally unsupported — the pin is exact). Every module, executable, and option is wired in build.zig; the manifest has no dependencies of its own. There is also no C/C++ build system — the only non-Zig translation units are a few vendored shims (src/backend/metal/shim.m, the miniaudio/MIDI shims under examples/) compiled by build.zig itself when the relevant option or example requires them. System dependencies appear only when options select them: a CBLAS provider for -Dblas=..., Apple frameworks for -Dgpu=metal/-Dblas=accelerate and the audio examples, libc for -Dgpu=cuda (the CUDA driver and cuBLAS are dlopened at runtime — no CUDA SDK at build time), and a Rust toolchain for -Dllguidance=true (the vendored llguidance staticlib is Rust, built via cargo from build.zig; §2.2).

zig version        # 0.16.0
zig build test     # all test roots; no model assets needed
zig build --help   # lists every step and project option below

2.2 Build options (build.zig)

All project options are consumed at comptime through the generated build_options module (§2.4) — backend dispatch is compiled away, and unused kernel arms are not in the binary.

Option Values Default Effect Constraints
-Dbackend native | scalar | cpu native Kernel implementation set. native = Zig SIMD vector kernels + optional BLAS; scalar = the reference backend (correctness oracle — native and scalar must agree). cpu is a deprecated alias for scalar.
-Dblas none | accelerate | openblas | mkl | blis | nvpl | blas accelerate on macOS targets, none elsewhere CBLAS provider backing the native backend's large-GEMM arms; none keeps the pure Zig vector kernels (including the blocked packed f32 GEMM). accelerate on a non-macOS target panics the build.
-Daccelerate bool unset Compatibility alias, consulted only when -Dblas is absent: true-Dblas=accelerate, false-Dblas=none. An explicit -Dblas always wins.
-Dblas-threads u32 0 Pins the vendor BLAS thread count for explicit providers (OpenBLAS/MKL/BLIS/NVPL); 0 keeps the provider default. No effect with -Dblas=none.
-Dmax-threads usize 8 Comptime worker-team ceiling and runtime default thread count (src/parallel.zig). Sized for M1 Max P-cores; many-core servers must raise it at build time (FUCINA_MAX_THREADS only lowers it at runtime). Outside 1–64 panics the build.
-Dgpu none | metal | cuda none GPU GEMM offload provider (§9). metal: big f32/f16/bf16 GEMMs, dense quantized prefill linears, and the MoE expert FFN on macOS. cuda: the same surface plus fused prefill attention and opt-in decode GEMV on Linux/NVIDIA, no SDK at build time. Decode below the work gates and training stay on CPU. metal on a non-macOS target panics; cuda on a non-Linux target panics (cross-compiling from macOS with -Dtarget=x86_64-linux-gnu is the supported path).
-Dparakeet-mic bool false Links the vendored miniaudio capture stack into the parakeet example so --mic (live microphone) works; default off keeps the parakeet build fast. Only affects the parakeet executable/tests.
-Dllguidance bool false Builds the vendored llguidance constrained-decoding engine (cargo build in vendor/llguidance) and links its staticlib into the qwen3/gemma4/lmserve examples and the llm, lmserve, and snippet-check test roots, enabling llm.llguidance grammar/JSON-schema token masking (§13.6). Off (the default) the build stays pure Zig and llm.llguidance.Constraint.init returns error.LlguidanceNotEnabled; the LogitProcessor seam itself is always available. Requires a Rust toolchain >= 1.87 on PATH when enabled.
-Dvector-scan bool false Vectorizes the scan kernels (cumsum/cumprod and cumsum's reverse VJP pass). Off = the documented serial-per-row scans. On: non-last-axis scans vectorize across independent columns (bitwise identical to serial); last-axis scans use an in-register prefix scan — still bitwise deterministic for any thread count, but the accumulation order differs from the serial default (the sum-SIMD-lanes rounding class; exact for integer-valued data). Measured M1 ReleaseFast 256×8192: cumsum 3.3×, cumprod 5.2× (last axis), 4.3× (non-last, bit-identical).
-Doptimize Debug | ReleaseSafe | ReleaseFast | ReleaseSmall Debug Standard Zig optimize mode. Build with ReleaseFast whenever speed matters (Debug is 10–50× slower); validate in Debug/ReleaseSafe, bench in ReleaseFast. x86dot-check is always built ReleaseSafe regardless.
-Dtarget, -Dcpu standard queries host, native CPU Cross-compilation target and CPU model. See below — a bare -Dtarget silently loses the fast kernels.

Constraint violations are build-time panics (@panic/std.debug.panic inside build()), not recoverable configuration errors: the panic checks run against the target OS (target.result.os.tag), not the host, so -Dgpu=cuda -Dtarget=x86_64-linux-gnu from macOS builds fine while -Dgpu=cuda alone on macOS panics.

CPU targeting is native by default. With no -Dtarget, Zig targets the compiling machine's exact CPU (full detected feature set, like -march=native), and the kernels' comptime feature gates (src/backend/quant/common.zig) compile in the matching arms — NEON/sdot on Apple Silicon, AVX2/AVX-VNNI on modern x86, smmla on I8MM-class ARM servers, portable vectors elsewhere. Unused arms are compiled out entirely; there is no runtime dispatch. Cross-compiling with -Dtarget=... drops to that architecture's baseline unless -Dcpu=... names a model (x86_64_v3, alderlake, znver4, neoverse_v1, …). Two rules follow: build on the machine that will run the binary, or pin -Dcpu to match it.

The resolved configuration is visible on the fucina module root as comptime constants (active_backend_kind, native_blas_kind, native_uses_blas, native_uses_accelerate, native_blas_threads, parallel.vector_max_threads):

const std = @import("std");
const fucina = @import("fucina");

test "build options are comptime facts on the module root" {
    // Baked in by build.zig's `build_options`; all comptime-known.
    const kind: fucina.BackendKind = fucina.active_backend_kind; // -Dbackend
    try std.testing.expect(kind == .native or kind == .scalar);
    if (fucina.native_uses_blas) // -Dblas != none
        try std.testing.expect(fucina.native_blas_kind != .none);
    try std.testing.expect(fucina.parallel.vector_max_threads >= 1); // -Dmax-threads
}

compiled & run in CI ✓

Note fucina.BackendKind has two members (scalar, native): build.zig bakes the raw three-member -Dbackend value (including the deprecated cpu) into build_options.backend_kind, and the cpu → .scalar mapping happens at file scope of src/backend.zig. At runtime the effective worker count never exceeds the comptime ceiling — fucina.parallel.setMaxThreads(n) is the programmatic counterpart of FUCINA_MAX_THREADS (mirrors llama.cpp's -t; call once at startup, before the first parallel op — the first cpuThreadCount call latches the value). The two are not identical: the env var only lowers the detected CPU count, while setMaxThreads replaces it and can raise the team size above the detected count, up to the ceiling (§6.6):

test "runtime worker count never exceeds the comptime ceiling" {
    fucina.parallel.setMaxThreads(4); // programmatic twin of FUCINA_MAX_THREADS
    const n = fucina.parallel.cpuThreadCount(fucina.parallel.vector_max_threads);
    try std.testing.expect(n >= 1 and n <= 4);
    try std.testing.expect(n <= fucina.parallel.vector_max_threads);
}

compiled & run in CI ✓

2.3 Build steps (build.zig)

zig build with no step runs the default install step: it compiles all 27 installed executables into zig-out/bin/ (named fucina-<name>). Bench and check executables are not installed; they build on demand when their step runs. Every example-runner step depends only on its own executable's install-artifact step, so zig build qwen3 builds just that executable; among the bench* steps only bench-gate depends on the full install step. Arguments after -- are forwarded to the launched program.

Tests and gates:

Step What it does
test Runs the unit tests of all nine test roots (§2.7). No model assets needed.
test-fucina Runs the fucina-root unit tests only (the routine -Dbackend=scalar leg); the full test matrix stays the pre-merge gate.
bench-check Compiles every bench executable without running it — the cheap gate that keeps the bench suite building (bench mains are otherwise reachable only through their run steps). The addBench helper registers every bench into the gate, so a new bench cannot land outside it.
arch-check Builds and runs tools/check_import_graph.zig: the production (non-test) src/**/*.zig import graph must have zero strongly-connected components. AST-based and test-aware — imports reachable only from test decls or test-only private helpers are not counted. Also enforces test-file forwarding: every src/**/*_tests.zig/*_test.zig must be @imported by some non-test src file, so a forgotten forwarding stanza (§2.7) cannot silently drop a test file from zig build test.
doc-check Builds and runs tools/check_doc_links.zig: every backtick-quoted *.md in AGENTS.md's "## Doc index" section (root docs, docs/<name>.md, and per-example examples/<name>/README.md) must exist on disk; docs/RUNNING-MODELS.md is additionally scanned for examples/<name>/README.md references.
snippet-check Builds and runs tools/gen_snippet_tests.zig: every runnable ``zig snippet in this document (a fenced block with a column-0 namedtest "...") is extracted into a generated test root and run against the realfucina/fucina_llm` modules with the build's option set — a snippet that stops compiling or asserting fails the gate (conventions in §2.7).
x86dot-check Runs the cross-ISA int8/Q4_K/Q8_0/TQ2_0 dot-kernel parity checker (src/x86dot_check.zig, always ReleaseSafe, deterministic output diffable across environments). The run leg follows -Dtarget (so -Dtarget=x86_64-macos -Dcpu=baseline under Rosetta drives the emulated x86 legs); four additional compile-only legs (x86_64_v3, alderlake, znver4, neoverse_v1) catch bit-rot of the AVX2/AVX-VNNI/AVX512-VNNI/smmla inline-asm arms that the local machine cannot execute.
cuda-check Compile-only -Dgpu=cuda legs: semantically analyzes the fucina/fucina_llm roots and NVRTC PTX generator for x86_64-linux-gnu with gpu_kind=.cuda (never run), so the CUDA provider/tooling cannot bit-rot on GPU-less/macOS machines.
bench-gate Runs python3 tools/bench_gate.py (a system command, not a Zig artifact): the paired Fucina-vs-llama.cpp benchmark gate; protocol in BENCHMARK.md. Requires tools/fetch_refs.sh --build first.

Example and tool runners (each zig build <step> -- <args>; CLI details in the per-example examples/<name>/README.md and §14):

Step Program
smoke examples/smoke/main.zig — the smoke example (run is kept as an alias).
qwen3 Qwen3 dense/MoE GGUF inference: chat/REPL, --spec/--spec-ref lossless speculative decode, --tokenize tokenizer-parity oracle.
gemma4 Gemma 4 GGUF inference / logit-parity harness; chat/REPL/--spec.
qwen35 Qwen3.5 (hybrid Gated-DeltaNet) GGUF loader/parity harness.
diffusion-gemma DiffusionGemma block text-diffusion (parity harness + EB chat). Links libc.
deepseek2 DeepSeek-V2 family (MLA + MoE) GGUF inference.
glm4moe GLM-4.5 family GGUF inference; --mtp native multi-token-prediction speculative decode.
deepseek4 DeepSeek V4 Flash GGUF inference (CSA/HCA + streamed experts).
inkling Inkling (hybrid rel-bias attention + MoE) GGUF inference / parity harness.
nanochat nanochat port (karpathy/nanochat): tok-train / base-train / sft / eval-bpb / chat.
lmserve OpenAI- and Anthropic-compatible HTTP server (chat completions, responses, and /v1/messages; SSE streaming; hermes function calling on qwen3-family models in all three dialects; opt-in --spec speculative and --batch lockstep decode; JSON-schema constrained output with -Dllguidance=true) over qwen3/qwen35/gemma4/diffusion-gemma/inkling GGUFs + nanochat checkpoints. Links libc.
parakeet Parakeet ASR: WAV → text, --stream/--manifest/--mic (needs -Dparakeet-mic), --compare parity harness.
omnivoice OmniVoice MaskGIT TTS: voice cloning/design, codec encode/decode.
locate-anything LocateAnything-3B open-vocabulary detection: detect/info, exit-code parity gates, bench.
facedetect buffalo_l face pipeline (SCRFD/ArcFace/genderage/anti-spoof/landmarks): detect/embed/verify/analyze.
spirals Two-spirals training demo: SGD/AdamW/Muon/APOLLO, checkpoint, resume, infer.
nam Neural Amp Modeler: .nam profiles, training, live amp sim (vendored miniaudio + CoreMIDI shims always linked).
finetune LoRA fine-tune of a Qwen3 GGUF on a built-in SFT dataset.
cartridge Train/serve a corpus as a trained-KV-prefix cartridge on a Qwen3 GGUF (§13.10).
cartridge-fleet Per-document cartridge fleets: mixed-visibility training, RAM/disk budget manager, cosine cartridge-RAG (§13.10).
engram Graft conditional n-gram memory onto a frozen Qwen3 GGUF and train it (§13.11).
es-finetune Evolution-strategies fine-tune of a Qwen3 GGUF (--mode lora\|full, --reward rule\|nll\|acc).
es-spirals Two-spirals MLP trained from scratch by ES (self-verifying).
es-ternary-spirals Ternary-native ES on packed TQ2_0 layers (training state = the int8 inference model; see TERNARY.md).
ptqtp-spirals Self-verifying PTQTP acceptance demo: float-trains an MLP, decorates it post-training with dual trit-planes, asserts accuracy holds on the deployed int8 path (§10.9, PTQTP.md).
ptqtp-qwen3 Decorate a Qwen3 GGUF's linears in place (any source dtype; --planes 1\|2\|3, --down-planes/--o-planes, --skip-first/--skip-last, --head-planes, --tie-scales) with teacher-forced NLL before/after and greedy completion + decode timing; --save FILE persists the decorated model as a GGUF that reloads bitwise through the ordinary loaders (§13.2.1, PTQTP.md).
export-gguf tools/export_gguf.zig: GGUF re-emit/transcode (--dtype f16/bf16/f32/q8_0/q4_k/q5_k/q6_k/tq2_0/verbatim, --experts-dtype override), merge of Fucina LoRA adapters into dense weights (--adapters), or shard-streaming PTQTP quantization (--ptqtp[=K], --ptqtp-tie, one tensor at a time — models bigger than RAM; docs/PTQTP.md); see §12.

Microbenchmarks (all in bench/; run under -Doptimize=ReleaseFast; protocol and thermal discipline in BENCHMARK.md):

Step Measures
bench MLP-shaped inference and backward (bench/mlp.zig).
bench-optim Optimizer step kernels (SGD/AdamW/Muon/APOLLO) at LLM shapes.
bench-ce Softmax / cross-entropy row kernels at LLM shapes.
bench-conv conv2d forward/backward-input/backward-weight at CNN shapes.
bench-scatter Scatter-add (embedding-gradient) kernel at vocab × dim shapes.
bench-backward-diamond Serial vs manual-parallel independent GEMM VJPs.
bench-attention-backward Grouped causal attention backward.
bench-backend Scalar vs native backends on representative ops.
bench-f16gemm f16 TransB GEMM parallel efficiency (Qwen3 shapes).
bench-gemm Large-shape f32 GEMM: row kernels vs blocked packed kernel vs BLAS.
bench-train-step End-to-end GPT autograd training step (embed, blocks, cross-entropy, backward, AdamW) on a fixed synthetic sequence (bench/train_step.zig); --inference times the eval-mode forward alone; --dump <dir> writes weights/tokens/rope so tools/torch_train_step.py runs the identical model in PyTorch.
bench-packed-gemm Pack-once dense GEMM at skinny-m inference shapes (bench/packed_gemm.zig).
bench-gpu-dispatch CPU CBLAS vs blocking/async eager GPU GEMM/GEMV: host-visible latency, submit latency, queued throughput, and parity.
bench-gpu-formats Fucina f16/load-time-packed quant CPU kernels vs eager GPU f16/Q4_K/Q5_K/Q6_K/Q8_0 LLM linears: host-visible latency, submit latency, queued throughput, and parity (Q5_K is CUDA-only).
bench-q5kmoe Q5_K MoE-expert matmul variants.
bench-q8gemv q8_0 skinny-m decode GEMV: per-row vs x4 interleaved vs lane-packed LHS (bench/q8gemv.zig).
bench-ternary TQ2_0 ternary matmul: hot sdot/vpdpbusd tiles vs x4 interleaved pack (A/B pair) vs cold table path, f32 path, Q4_K, dense f32; prints weight-stream GB/s and %ceil of the measured single-thread DRAM ceiling.
bench-membw Measured DRAM read-bandwidth ceiling: single-thread + all-core roofline probe (bench/membw.zig); bench-ternary reports each kernel's %ceil against it.
bench-facade Raw tensor ops vs the public no-grad Tensor facade.
bench-einsum einsum vs hand-written dot/permute contraction pipelines (parity + advantage cases).

Common invocations:

zig build test                        # correctness, native backend
zig build test -Dbackend=scalar       # reference backend must agree
zig build test -Dblas=none            # native backend on pure Zig kernels
zig build arch-check doc-check snippet-check  # structure + doc gates
zig build -Doptimize=ReleaseFast      # install everything into zig-out/bin

zig build qwen3 -Doptimize=ReleaseFast -- models/Qwen3-0.6B-Q8_0.gguf \
  --chat "What is the capital of France?" --no-think

zig build -Dmax-threads=32 -Doptimize=ReleaseFast          # many-core server
zig build qwen3 -Dgpu=metal -Doptimize=ReleaseFast -- ...  # Metal offload (macOS)
zig build -Dgpu=cuda -Dtarget=x86_64-linux-gnu -Dcpu=znver4 \
  -Doptimize=ReleaseFast                                   # CUDA cross-build from macOS

FUCINA_MAX_THREADS=6 zig-out/bin/fucina-qwen3 models/... --chat "..."

2.4 Module graph and options wiring (build.zig)

build.zig registers two library modules and two internal microbench roots with b.addModule; executables get private root modules via b.createModule and pull the libraries in with addImport.

  • fucina — root src/fucina.zig. The public facade: tensors, autograd, ExecContext, optimizers, ES, LoRA, GGUF/safetensors I/O (§3§12). It is the only one of the two library modules that receives the option set: module.addOptions("build_options", options) (the microbench roots below and the test-root module instances receive the same options object).
  • fucina_llm — root src/llm.zig. The LLM/ASR stack (§13). It does not get build_options; every module built from src/llm.zig instead receives a single-key llm_build_options module (llguidance: bool, read by src/llm/llguidance.zig). It reaches the configured core exclusively through llm_module.addImport("fucina", module) and the fucina.internal seam, so there is exactly one copy of the backend/exec types.
  • bench_raw — root src/bench_raw.zig, same options. Internal raw tensor surface (RawTensor, ExecContext, optim) for bench/{mlp,optim,ce,conv,scatter,backward_diamond,attention_backward,train_step,facade,einsum}.zig. Not part of the public facade — the root export guard in src/fucina.zig makes fucina.RawTensor a compile error.
  • raw_backend — root src/backend.zig, same options. Direct kernel access for bench/{backend,f16gemm,gemm,packed_gemm,gpu_dispatch,gpu_formats,q5kmoe,q8gemv,ternary}.zig (bench/membw.zig imports neither module — the bandwidth probe is standalone). The bench-backend executable additionally receives a second options module named bench_options (native_blas_kind: BlasKind, native_uses_blas: bool, native_blas_threads: u32) so it can label its output with the native backend's BLAS configuration.

The build_options module is built with b.addOptions() and exactly these keys (options.addOption(T, name, value)):

Key Type Value
backend_kind enum { scalar, native, cpu } -Dbackend
blas_kind enum { none, accelerate, openblas, mkl, blis, nvpl, blas } resolved -Dblas
use_blas bool blas_kind != .none
blas_threads u32 -Dblas-threads
max_threads usize -Dmax-threads
use_gpu bool gpu_kind != .none
gpu_kind enum { none, metal, cuda } -Dgpu
vector_scan bool -Dvector-scan

Only eight files outside tests import it, all inside the fucina module: src/parallel.zig, src/backend.zig, src/backend/native.zig, src/backend/gpu.zig, src/backend/metal.zig, src/backend/cuda.zig, src/exec/reduce.zig, src/exec/matmul.zig (a src/ag/tensor_tests.zig test also branches on vector_scan, and a src/exec_tests.zig test skips on use_gpu). The parakeet executable and its test root get their own single-key build_options (parakeet_mic: bool) — the name collides deliberately; the example reads its key, the library module keeps its full set.

Example and bench targets are declared through two spec-driven helpers — addExample(b, ctx, spec) (exe + module imports + BLAS/GPU config + install + a run step forwarding -- args) and addBench(b, ctx, bench_check_step, spec) (no install; registers into bench-check) — with per-target special wiring (extra imports, libc, llguidance, option modules) attached to the returned artifacts at the call site. Linking itself is centralized in six helpers applied per executable:

  • configureBlas(step, blas_kind) — per provider: link libc plus Accelerate (framework), openblas, mkl_rt, blis, nvpl_blas, or generic blas, with Homebrew/oneAPI/HPC-SDK library search paths and rpaths added (/opt/homebrew/opt/{openblas,blis}, /usr/local/opt/{openblas,blis}, /opt/intel/oneapi/mkl/latest, /opt/nvidia/hpc_sdk).
  • configureGpu(b, step, gpu_kind)metal: link libc + Metal + Foundation and compile src/backend/metal/shim.m (-fobjc-arc); cuda: link libc only (the provider dlopens libcuda.so.1/cuBLAS via std.DynLib at runtime).
  • configureLlguidance(step, dep) — no-op unless -Dllguidance; then links the cargo-built staticlib plus libc, and on non-macOS targets Zig's bundled LLVM libunwind via link_libcpp (the Rust FFI converts panics to error strings with catch_unwind, and glibc does not export _Unwind_*; macOS's libSystem ships an unwinder).
  • configureNamAudio / configureOmnivoiceAudio / configureParakeetAudio — the vendored miniaudio C shims (examples/nam/audio_shim.c, plus midi_shim.c for NAM and examples/omnivoice/play_shim.c for playback), with CoreAudio/CoreMIDI frameworks on macOS; elsewhere miniaudio dlopens its backend through libc.

2.5 Consuming Fucina from another project

Fucina is an ordinary Zig package: build.zig.zon names it .fucina, the repository is tagged (v0.1.0), and both library modules are exported by build.zig (b.addModule), so the standard path is the package manager. From the consumer project:

zig fetch --save git+https://github.com/matteo-grella/fucina#v0.1.0
// build.zig (consumer) — verified against Zig 0.16.0
const fucina_dep = b.dependency("fucina", .{
    .target = target,
    .optimize = optimize,
    // Any §2.2 build option passes through by name, e.g.:
    //   .blas = .none, .backend = .native, .@"max-threads" = @as(usize, 4),
});
exe.root_module.addImport("fucina", fucina_dep.module("fucina"));
exe.root_module.addImport("fucina_llm", fucina_dep.module("fucina_llm")); // optional

@import("fucina") / @import("fucina_llm") then work exactly as in every snippet of this reference; omit the fucina_llm import for tensor/training-only consumers. In dependency builds the exported modules carry their own BLAS/GPU link inputs (link inputs propagate through module imports), so the default macOS configuration links Accelerate with no extra consumer steps and -Dgpu=metal brings its shim along — no configureBlas/configureGpu replication. Option defaults match the in-tree build (§2.2); pass .blas = .none for a zero-system-dependency build. Two limits: .llguidance = true is not supported through the package manager (the vendored cargo build is designed for an in-tree checkout — vendor the repo for constrained decoding), and the API is pre-1.0 (§1.5) — pin the tag or a commit (#<sha>) and expect churn between tags.

Vendoring fallback. A consumer can instead vendor the repository (git submodule, subtree, or plain copy) and wire the modules in its own build.zig with the same std.Build calls the in-tree build uses. The option enums must be re-declared, but only the field names matter — the fucina sources switch on them by name — and all eight keys are required (compilation of src/parallel.zig/src/backend.zig/src/backend/gpu.zig/src/exec/reduce.zig fails on a missing key). Keep the two derived booleans consistent with their enums.

git submodule add https://github.com/matteo-grella/fucina vendor/fucina
// build.zig (consumer) — verified against Zig 0.16.0
const std = @import("std");

const BackendKind = enum { scalar, native, cpu };
const BlasKind = enum { none, accelerate, openblas, mkl, blis, nvpl, blas };
const GpuKind = enum { none, metal, cuda };

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // The comptime configuration the fucina sources read as
    // `@import("build_options")`. All eight keys are required.
    const options = b.addOptions();
    options.addOption(BackendKind, "backend_kind", .native);
    options.addOption(BlasKind, "blas_kind", .none);
    options.addOption(bool, "use_blas", false); // keep == (blas_kind != .none)
    options.addOption(u32, "blas_threads", 0);
    options.addOption(usize, "max_threads", 8);
    options.addOption(bool, "use_gpu", false); // keep == (gpu_kind != .none)
    options.addOption(GpuKind, "gpu_kind", .none);
    options.addOption(bool, "vector_scan", false);

    const fucina = b.addModule("fucina", .{
        .root_source_file = b.path("vendor/fucina/src/fucina.zig"),
        .target = target,
        .optimize = optimize,
    });
    fucina.addOptions("build_options", options);

    const fucina_llm = b.addModule("fucina_llm", .{
        .root_source_file = b.path("vendor/fucina/src/llm.zig"),
        .target = target,
        .optimize = optimize,
    });
    fucina_llm.addImport("fucina", fucina);
    // fucina_llm's own comptime configuration, read as
    // `@import("llm_build_options")` — required by every module built from
    // `src/llm.zig` (src/llm/llguidance.zig reads the boolean `llguidance`
    // key; false keeps the engine stubbed). `true` additionally needs the
    // cargo staticlib build + link from fucina's build.zig (§2.2
    // `-Dllguidance`).
    const llm_options = b.addOptions();
    llm_options.addOption(bool, "llguidance", false);
    fucina_llm.addOptions("llm_build_options", llm_options);

    const exe = b.addExecutable(.{
        .name = "myapp",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });
    exe.root_module.addImport("fucina", fucina);
    exe.root_module.addImport("fucina_llm", fucina_llm);
    // Non-default -Dblas / -Dgpu configurations also need the link steps
    // from fucina's build.zig (configureBlas / configureGpu): frameworks,
    // system libraries, and the Metal shim C source.
    b.installArtifact(exe);
}

The application code then imports the modules by the names given to addImport: const fucina = @import("fucina"); and const llm = @import("fucina_llm");. fucina_llm is optional — omit it (and its addImport) for tensor/training-only consumers. For a BLAS or GPU configuration, replicate the corresponding configureBlas/configureGpu body from the in-tree build.zig on the consumer executable (the Metal shim path becomes vendor/fucina/src/backend/metal/shim.m). The public API is not yet stable (README.md says so explicitly); pin the vendored commit.

2.6 Runtime environment variables

Every knob in the core-runtime and GPU tables is read once and cached (atomically or under a mutex) at first use; changing the process environment afterwards has no effect. The example/test gates in the last table are plain getenv calls re-read at every use. Numeric knobs that fail to parse fall back to their defaults; FUCINA_MAX_THREADS-style positive-integer knobs ignore unset/invalid/zero values. On Linux without libc the lookup scans /proc/self/environ (src/parallel.zig), so FUCINA_MAX_THREADS also works in static builds.

Core runtime (src/parallel.zig, src/exec/conv.zig, src/exec/attention.zig, src/exec/matmul.zig):

Variable Effect Default
FUCINA_MAX_THREADS Lowers the worker count below the -Dmax-threads ceiling (mirrors llama.cpp -t). Never raises it. Consulted on the first cpuThreadCount call; a prior setMaxThreads wins. unset (detected CPU count — clamped to physical cores on SMT hosts and to performance cores on Apple Silicon — capped by the ceiling)
FUCINA_SPIN_BUDGET Overrides the worker-team spin-then-park window (src/thread.zig BarrierPool; 0 = park immediately is a valid override, values above u32 are ignored). Read once per pool init. Workload-coupled; the default is deliberate. unset (32768 spins; 0 when the team exceeds the physical-core count — spinning while oversubscribed starves the descheduled participants)
FUCINA_POOL_PROFILE=1 Emits one [pool-trace] line per BarrierPool dispatch with span, claim chunk, and each participant's first-claim/completion offsets and task count. Diagnostic only; read once when the team is created. off
FUCINA_WINOGRAD=1 / FUCINA_NO_WINOGRAD=1 Force the Winograd conv2d route on/off (A/B + emergency revert switches). on for no-BLAS builds, off when a platform BLAS backs the matmul
FUCINA_NO_WINOGRAD_F4=1 Pins Winograd-routed large maps to the F(2×2,3×3) tier. F4 tier enabled
FUCINA_WINOGRAD_F4_MIN Minimum output spatial size for the F4 tier. 14
FUCINA_WINOGRAD_F4_MAXCIN Maximum input channels for the F4 tier (deep-channel maps run faster on F2). 56
FUCINA_NO_CONV_BWD_GEMM=1 Pins the groups == 1 conv2d backward entries to the direct gather kernels instead of the GEMM (matmul + im2col/col2im) decomposition (A/B + emergency revert switch). GEMM route on
FUCINA_ATTN_BWD_STATS=1 / FUCINA_NO_ATTN_BWD_STATS=1 Force the forward-saved-stats route of the attention-backward softmax reconstruction (src/exec/attention.zig) on/off (A/B + emergency revert switches) — the two routes agree to f32 roundoff, not bitwise; only consulted when the autograd record saved forward stats (the stats-less exec path always recomputes). on
FUCINA_NO_ATTN_BWD_BLAS=1 Reverts the attention-backward contraction tiles from the BLAS-strip route (the per-tile contractions issued as strided sgemm strips) to the register-tiled route (src/exec/attention.zig; A/B + escape hatch for parity work) — the two routes agree to f32 roundoff, not bitwise. Only consulted on BLAS-backed native builds; elsewhere the register-tiled route always runs. BLAS-strip route on (BLAS builds)
FUCINA_CPU_F32_SHADOW=1 Opt-in (src/exec/matmul.zig): attaches a widen-once f32 shadow to a 16-bit weight's storage and routes m ≥ 32 GEMMs through the BLAS f32 path (decode stays on the streaming kernels). +4 bytes/weight resident; leave off when training 16-bit weights in place. CPU builds only. off
FUCINA_CPU_F32_SHADOW_MIN_M Overrides the shadow route's m ≥ 32 crossover. 32

GPU offload (read by both providers unless noted; src/backend/metal.zig, src/backend/cuda.zig; see §9):

Variable Effect Default
FUCINA_GPU Kill switch: a value starting with 0 disables the GPU provider entirely. enabled on -Dgpu builds
FUCINA_GPU_MIN_WORK Base f32 GEMM offload gate, in m·n·k work units. Metal 2^32 (cold single-op crossover); CUDA 2^30 (the transient floor below still dominates ordinary host RHS)
FUCINA_GPU_MIN_WORK_F16 f16 GEMM gate. 2^27 (lower — the CPU f16 competitor has no AMX-class arm)
FUCINA_GPU_MIN_WORK_F16_RESIDENT (cuda) f16 GEMM/GEMV gate when the RHS already has a device address; permits small-m decode without admitting a streamed weight. 2^20
FUCINA_GPU_MIN_WORK_16BIT_RESIDENT (metal) f16/bf16 GEMM gate when the RHS is already Metal-mapped; admits batched decode at m ≥ 16 and the lm-head row while narrow decode stays on the CPU streaming kernels. 2^27
FUCINA_GPU_MIN_WORK_GEMV Resident dense-f32 GEMV/small-m GEMM gate (m <= 8; nonresident CUDA RHS is refused). 2^24
FUCINA_GPU_MIN_WORK_RESIDENT (cuda) Dense-f32 GEMM/batched-GEMM gate when the RHS already has a device address. 2^27 (512³; 256³ loses to OpenBLAS-32 on the reference host)
FUCINA_GPU_MIN_WORK_QMOE Grouped quantized MoE GEMM gate; setting it also re-seeds the dense-Q6 gate. 2^30
FUCINA_GPU_MIN_WORK_DENSE_Q4 Dense Q4_K model-weight gate against the load-time-packed CPU fallback. Metal 2^30; CUDA 2^27
FUCINA_GPU_MIN_WORK_DENSE_Q5 (cuda) Dense Q5_K model-weight gate against the load-time-packed CPU fallback. 2^24
FUCINA_GPU_MIN_WORK_DENSE_Q6 Dense Q6_K gate; overrides both the compact/raw and packed-CPU tiers. compact/raw 2^22; packed Metal 2^31, CUDA 2^24
FUCINA_GPU_MIN_WORK_DENSE_Q8 Dense Q8_0 model-weight gate against the load-time-packed CPU fallback. Metal 2^29; CUDA 2^24
FUCINA_GPU_MIN_WORK_DENSE_TQ2 (metal) Dense/PTQTP ternary TQ2_0 gate against the x4 interleaved CPU kernels. 2^25
FUCINA_GPU_QMOE_MIN_FILL Tile-occupancy gate (percent) for grouped MoE: small expert batches whose 32-row tiles would run mostly empty stay on CPU; 0 disables the gate, >100 never passes it. 50
FUCINA_GPU_TRACE Non-0 first character enables dispatch tracing; dump via fucina.internal.gpu.traceDump() (no-op when off). off
FUCINA_GPU_TF32 (cuda) Non-0 opts f32 GEMMs into TF32 tensor cores (default is strict FP32). off
FUCINA_GPU_MIN_WORK_TRANSIENT (cuda) Work floor for non-resident operands (each crossing PCIe per call); an m ≥ 128 row floor applies alongside it. 2^33
FUCINA_GPU_MIN_WORK_ATTN (cuda) Fused prefill-attention gate, in q·kv·heads·d work units. 2^28
FUCINA_GPU_DECODE (cuda) Non-0 enables opt-in quantized decode for m ≤ 8 and resident weights only (GEMV generally; Q5_K uses tiled MMA at m=4..8). off
FUCINA_GPU_MIN_WORK_DECODE_Q5 (cuda) Q5_K-only decode work gate after FUCINA_GPU_DECODE=1; rejects the compact CPU kernel's measured 1×4096² win. 3·2^23
FUCINA_GPU_QUANT_MMA (cuda) A value starting with 0 disables the tensor-core Q4_K/Q5_K/Q6_K/Q8_0 kernels and selects the scalar-FFMA fallback (diagnostic A/B switch). enabled on compute capability ≥ 7
FUCINA_GPU_QUANT_SPLIT_K (cuda) A value starting with 0 disables the on-stream split-K/reduction used to fill idle SMs for underfilled dense quantized prefill (diagnostic A/B switch). enabled when the N64 output grid fills less than roughly 7/8 of the SMs
FUCINA_GPU_VRAM_BUDGET (cuda) Weight-residency budget in bytes; 0 disables the bound. 80% of free VRAM at init
FUCINA_GPU_KERNELS=src (cuda) NVRTC-recompiles the vendored kernels from kernels.cu instead of loading the committed PTX (dev loop; tools/gen_cuda_ptx.sh regenerates the PTX). committed PTX

LLM stack (src/llm/weights.zig §13.2, src/llm/qwen3/train.zig, src/llm/inkling/mmproj.zig; read once and cached like the tables above):

Variable Effect Default
FUCINA_NORM_QUANT_FUSED=1 / FUCINA_NO_NORM_QUANT_FUSED=1 Force the fused normalize+quantize+packed-GEMM route of linearSeqNormed on/off (prefill shapes on the packed CPU arms only; the fused route matches the unfused rmsNormMul + linear pair to f32 roundoff, not bitwise). on
FUCINA_Q4K_DECODE_COMPACT=1 / FUCINA_NO_Q4K_DECODE_COMPACT=1 Route decode-shape (m < 4) no-grad Q4_K matmuls through the GGUF-native compact blocks instead of the byte-expanded packed layout — bitwise-equal, ~1.92× fewer weight bytes streamed. on
FUCINA_Q5K_DECODE_COMPACT=1 / FUCINA_NO_Q5K_DECODE_COMPACT=1 The same switch for Q5_K (~1.57× byte ratio). on
FUCINA_Q6K_DECODE_COMPACT=1 / FUCINA_NO_Q6K_DECODE_COMPACT=1 The same switch for Q6_K (1.30× byte ratio). on
FUCINA_NO_FUSED_DISTILL=1 Forces the composed logits + cartridge.distillLoss tail instead of the fused distill route in cartridge training (src/llm/qwen3/train.zig; A/B + emergency revert — the fused route matches it to f32 roundoff, not bitwise). fused route on
FUCINA_MM_PROFILE=1 Per-stage timing profile of the Inkling multimodal-projector encode (src/llm/inkling/mmproj.zig; read once at load). off

Examples and test gates (examples/):

Variable Effect Default
FUCINA_NAM_PROFILES Profile directory for the nam CLI (--profiles-dir overrides it). nam-profiles
OMNIVOICE_PARITY=1 Enables the OmniVoice parity suites under zig build test (need model files under models/omnivoice/ and locally captured reference goldens); unset, they error.SkipZigTest. skipped
OMNIVOICE_AUDIO_DEVICE_TESTS=1 Enables the speaker-playback device tests. skipped
OMNIVOICE_TOKENIZER_GGUF=<path> Points the real-codec-GGUF load test at a tokenizer GGUF. skipped
NANOCHAT_PARITY=1 Enables the nanochat parity suites under zig build test (need locally captured reference goldens); unset, they error.SkipZigTest. skipped
FUCINA_TEST_VERBOSE Any value re-enables the facedetect/nanochat per-case test-progress prints on stderr (examples/{facedetect,nanochat}/testlog.zig); failure-path prints stay on regardless. silent

2.7 Test organization (src/, examples/)

Tests live in sibling *_tests.zig files next to the production file they cover (156 of them across src/ and examples/): exec.zigexec_tests.zig, src/llm/tokenizer.zigsrc/llm/tokenizer_tests.zig, and so on. The production file pulls its sibling in with a forwarding stanza, so analyzing the production file analyzes its tests:

test {
    _ = @import("exec_tests.zig");
}

Module roots forward everything: src/fucina.zig ends in a test block referencing every submodule (_ = dtype; _ = exec; …), and src/llm.zig does the same for every family and helper, so one addTest per root reaches the whole tree.

zig build test runs nine test roots, each compiled as its own test binary with the same option set as the corresponding executable:

  1. src/fucina.zig — the core (with build_options);
  2. src/llm.zig — the LLM/ASR stack (imports fucina);
  3. examples/lmserve/main.zig (imports fucina, fucina_llm, and the shared nanochat module; links libc);
  4. examples/nam/main.zig (with the audio/MIDI shims linked);
  5. examples/parakeet/main.zig (with its parakeet_mic options);
  6. examples/omnivoice/main.zig (with the playback shim);
  7. examples/locate_anything/main.zig;
  8. examples/facedetect/main.zig;
  9. examples/nanochat/main.zig (imports fucina and fucina_llm — the raw-byte BPE pretokenizer reuses the generated Unicode tables via llm.unicode_categories).

All nine pass with no model assets present. Suites that need external material skip themselves cleanly rather than fail: the OmniVoice parity suites gate on OMNIVOICE_PARITY (§2.6); asset-dependent tests (facedetect goldens, the GGUF re-emit byte-identity test, tokenizer-parity fixtures, NAM training goldens) translate error.FileNotFound into error.SkipZigTest; GPU-dependent tests (src/llm/gemma/moe_tests.zig) skip unless the build has a GPU provider and a device is actually present. Tests for opt-in build features follow the same discipline through the feature's comptime flag: every src/llm/llguidance_tests.zig case is guarded on the flag — the enabled-path cases open with if (!llm.llguidance.enabled) return error.SkipZigTest;, and one disabled-build case inverts the guard to assert error.LlguidanceNotEnabled — so the same test root compiles and passes under any flag combination and gains coverage — never failures — when the flag is on. Per CONTRIBUTING.md, numeric changes must additionally be green under -Dbackend=scalar and -Dblas=none — the scalar backend is the reference, and native must agree with it.

Doc snippets are tests too. zig build snippet-check extracts every runnable ``zig block from this document — any fenced block containing a column-0 namedtest "..."declaration — into a generated test root and runs it against the realfucina/fucina_llmmodules with the build's option set (tools/gen_snippet_tests.zig). Authoring contract: snippets assume an implicit prelude (std,fucina,llm = @import("fucina_llm"),optim = fucina.optim; entries a snippet declares itself are not re-emitted); acomment on the line before a non-test fence marks a definition block (an Op/Spec/fn the prose introduces) prepended to every later snippet in the same## chapter; acomment excludes a test-shaped block that cannot run hermetically. Illustrative fragments (signature blocks, baretest {stanzas, asset-dependentfnexamples) are ignored automatically. A snippet for an opt-in build feature stays RUNNABLE, not skip-marked: it opens with the feature's comptime-flag guard (e.g.if (!llm.llguidance.enabled) return error.SkipZigTest;), sosnippet-checkcompiles it under every flag combination and executes it exactly when the enabling-D` flag is passed.

2.8 Continuous integration (.github/workflows/ci.yml)

CI runs on pushes to main and on every pull request, on a two-OS matrix (fail-fast: false): ubuntu-latest (x86-64) and macos-15 (arm64 — pinned rather than -latest, bumped deliberately). Zig 0.16.0 is installed via mlugg/setup-zig@v2. Steps, in order:

  1. zig build test — native backend (Accelerate on macOS, no BLAS on Linux, per the -Dblas default);
  2. zig build — all executables compile;
  3. zig build bench-check — the bench-check set compiles (bench mains are reachable only through their run steps, so nothing else in the build graph exercises them);
  4. zig build arch-check — import-graph gate;
  5. zig build doc-check — doc-index link gate;
  6. zig build snippet-check — REFERENCE.md runnable-snippet gate (§2.7);
  7. zig build x86dot-check — dot-kernel parity on the host ISA (x86 on ubuntu, NEON/sdot on macOS) plus the compile-only bit-rot legs;
  8. zig build test -Dbackend=scalar — ubuntu only (the reference backend);
  9. zig build test -Dblas=none — macOS only (pure-Zig native kernels, complementing the Accelerate run in step 1);
  10. zig build test -Dllguidance=true + snippet-check -Dllguidance=true — ubuntu only (the runner image ships cargo): un-skips the flag-gated llguidance tests and snippets (§2.7), keeping the extern ABI, the cargo build, and the Rust-staticlib link from bit-rotting behind a green default build — and continuously proving the Linux link of that staticlib.

Between the matrix and the conditional legs, every backend combination that can run on stock CI hardware is covered: native+BLAS, native without BLAS, and scalar, on both ISAs' unit-test surface, plus the opt-in llguidance feature on Linux. The CUDA GPU provider is covered by the compile-only cuda-check leg locally (not in CI); CPU dot ISA arms that CI cannot execute (AVX-VNNI, AVX512-VNNI, smmla) are covered by the compile-only legs and attestation records in src/x86dot_check.zig.