A high-performance Apple Silicon LLM inference runtime written in Zig.
lfm.zig explores how far CPU-only inference can be pushed on modern Macs. It
runs GGUF weights directly from a read-only mmap, keeps quantized weights packed
throughout inference, and drives the compute-heavy paths with ARM64 sdot and
Apple's Accelerate framework. On the reference M5 Max system, the current
runtime reaches 220.6 prefill tokens/s and 95.67 decode tokens/s with the
5.2 GB Q4_K_M model described in Reference performance.
The current release supports Liquid AI's dense LFM2.5-2.6B and sparse MoE LFM2.5-8B-A1B. Both use the reusable runtime pieces: a bounds-checked GGUF loader, tokenizer, packed quantized kernels, persistent CPU worker pool, attention and convolution caches, sampling, and an embeddable Agent API.
The runtime includes a command-line interface for generation and chat, plus a Zig library for streaming tokens, cloning agent state, and running tool-calling loops.
| Supported today | Not supported today |
|---|---|
| Apple Silicon macOS | Intel Macs, Linux, and Windows |
| LFM2.5-2.6B and LFM2.5-8B-A1B GGUF files with the expected tensor layouts | Other model architectures |
| Q4_K and Q6_K quantized weights | Arbitrary GGUF quantization formats |
| CPU inference through ARM64 and Accelerate | Metal, MLX, MPS, CUDA, or Vulkan |
| Pinned Zig stable (0.16.0), with no third-party Zig packages | Other Zig versions |
Model support is strict by design: the loader detects the dense or MoE architecture and validates its dimensions, layer schedule, and all 266 or 256 required tensor names and shapes before inference. Adding another model family requires an explicit model configuration and forward path while reusing the platform, storage, kernel, execution, and session layers already in place.
The project is independent and is not affiliated with or endorsed by Liquid AI. See ARCHITECTURE.md for a detailed implementation tour.
./scripts/fetch-zig.shThe zigw wrapper prefers a matching .zig-toolchain/zig, falls back to a
matching system installation, and requires the version declared in .zigversion.
The official LFM2.5-2.6B Q4_K_M model is approximately 1.67 GB:
mkdir -p "$HOME/.cache/models"
curl -fL \
"https://huggingface.co/LiquidAI/LFM2.5-2.6B-GGUF/resolve/main/LFM2.5-2.6B-Q4_K_M.gguf" \
-o "$HOME/.cache/models/LFM2.5-2.6B-Q4_K_M.gguf"
export LFM_WEIGHTS_FILE="$HOME/.cache/models/LFM2.5-2.6B-Q4_K_M.gguf"For the larger sparse model, download
LFM2.5-8B-A1B-Q4_K_M.gguf
and point LFM_WEIGHTS_FILE at it. The compatible
Heretic Q4_K_M variant
has also passed the model-backed smoke test. The official weights are
distributed under the LFM Open License 1.0.
./zigw build -Doptimize=ReleaseFast
./zig-out/bin/lfm ask "What is 21 + 21?"The release binary is written to zig-out/bin/lfm.
lfm has three modes:
| Command | Purpose |
|---|---|
lfm chat |
Interactive, cached, multi-turn chat |
lfm ask <question>... |
One-shot ChatML question and answer |
lfm generate <prompt>... |
Raw text continuation |
# Interactive chat
./zig-out/bin/lfm chat
# One-shot question
./zig-out/bin/lfm ask "Explain rotary embeddings briefly."
# Deterministic raw completion
./zig-out/bin/lfm generate --greedy --max-gen 64 \
"The capital of Denmark is"All three commands accept the same options:
| Option | Effect |
|---|---|
--greedy |
Use deterministic argmax decoding |
--max-gen N |
Generate at most N tokens per turn; default: 2048 |
--max-think N |
Force </think> after N reasoning tokens |
--no-think |
Disable the reasoning block |
--hide-think |
Generate reasoning without printing it |
--num-threads N |
Cap the number of active CPU worker partitions |
Interactive chat additionally supports:
| Input | Effect |
|---|---|
/clear |
Reset the conversation |
/exit |
End the session |
@path |
Insert the contents of a file into the prompt |
Commands can also run directly through the build system:
./zigw build -Doptimize=ReleaseFast run -- ask "What is 21 + 21?"build.zig exports the lfm module and installs a static library. The main
types are model.Model, which owns the mapped model, and agent.Agent, which
owns conversation, sampling, cache, and convolution state.
Until a stable package release is published, pin the repository as an SSH submodule:
git submodule add git@github.com:CerebralCoding/lfm_zig.git vendor/lfm_zigAdd the path dependency to your build.zig.zon:
.dependencies = .{
.lfm_zig = .{ .path = "vendor/lfm_zig" },
},Then import its module from your build.zig:
const lfm_dep = b.dependency("lfm_zig", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("lfm", lfm_dep.module("lfm"));The API follows semantic versioning, but releases before 1.0.0 may make
breaking API changes between minor versions. Pin an exact commit or release
when using the library from another project.
const std = @import("std");
const lfm = @import("lfm");
pub fn answer(allocator: std.mem.Allocator) !void {
var model = try lfm.model.Model.load(
allocator,
"LFM2.5-8B-A1B-Q4_K_M.gguf",
);
defer model.deinit();
var agent = try lfm.agent.Agent.init(allocator, &model);
defer agent.deinit();
try agent.appendUser("What is the capital of Denmark?");
var turn = try agent.assistantTurn(.{});
defer turn.deinit();
std.debug.print("{s}\n", .{turn.text});
}Generation accepts a context plus function pointer and invokes it for every decoded token:
const Printer = struct {
fn token(_: ?*anyopaque, _: u32, text: []const u8) void {
std.debug.print("{s}", .{text});
}
};
var turn = try agent.generate(.{ .call = Printer.token });
defer turn.deinit();Agent configuration is explicit and mutable:
agent.useGreedy();
agent.setTemperature(0.2);
agent.setTopK(80);
agent.setRepeatPenalty(1.05);
agent.setMaxGen(2048);
agent.setMaxContext(32_768);
agent.setMaxThink(512);The prompt and state APIs cover the common embedding workflows:
| API | Purpose |
|---|---|
append |
Append raw text, adding BOS when the transcript is empty |
appendUser |
Append a complete ChatML user turn |
appendSystem |
Append a system turn and registered tool declarations |
appendTokens |
Append pre-tokenized IDs |
prefill |
Warm the newly appended prefix without generating |
clone |
Fork the transcript, sampler, KV cache, and convolution state |
Each returned Turn owns its generated token IDs, decoded text, timing and
token statistics, and stop reason (eos, max_new, or tool_call). Release it
with Turn.deinit.
Tools are ordinary Zig callbacks described by a small, programmatically built
JSON schema. Register tools before calling appendSystem, then use
assistantTurnWithTools to let the agent generate calls, dispatch callbacks,
append results, and continue until completion.
var schema = lfm.tool.Schema.init(allocator);
defer schema.deinit();
_ = try schema.req("a", .int, "First addend");
_ = try schema.req("b", .int, "Second addend");
const Add = struct {
fn call(
_: ?*anyopaque,
alloc: std.mem.Allocator,
request: *const lfm.tool.ToolCall,
) ![]u8 {
const a = request.parseArg(i64, "a") orelse 0;
const b = request.parseArg(i64, "b") orelse 0;
return std.fmt.allocPrint(alloc, "{d}", .{a + b});
}
};
const add = try lfm.tool.Tool.init(
allocator,
"add",
"Add two integers.",
&schema,
.{ .context = null, .call = Add.call },
);
try agent.addTool(add); // ownership moves to the agentSee examples/tool_call_demo.zig for the minimal complete flow and
examples/tool_suite_demo.zig for multiple tools, callback context, observer
hooks, and error handling.
| Example | What it demonstrates |
|---|---|
examples/chat.sh |
Interactive chat and one-shot prompts |
examples/stream.sh |
A streaming-style prompt loop |
examples/throughput.sh |
Repeated runs with aggregate throughput |
examples/agent_library.zig |
Library integration and agent settings |
examples/tool_call_demo.zig |
Minimal tool schema and call loop |
examples/tool_suite_demo.zig |
Multiple tools, context, and error handling |
examples/tool_suite.sh |
Shell wrapper for the multi-tool example |
./examples/chat.sh --chat
./examples/chat.sh "Summarize Apple's Apple Silicon launch strategy"
./examples/stream.sh "How does vectorization help CPU inference?"
./examples/throughput.sh --threads 4 --runs 5 "$LFM_WEIGHTS_FILE"
./zigw build -Doptimize=ReleaseFast run-tool-suite-example
./examples/tool_suite.sh --model "$LFM_WEIGHTS_FILE"The current LFM2.5 inference paths are specialized for these models and platform:
- GGUF weights remain in a read-only private mmap and are demand-loaded by macOS.
- Q4_K and Q6_K weights are fused with per-256-element Q8 activation blocks.
- ARM64
sdotperforms quantized integer inner products. - Four-row quantized tiles reuse unpacked weights across prompt tokens.
- Prompt projections are batched and mixture-of-experts prompt work is grouped by expert.
- A persistent worker pool dynamically assigns contiguous row chunks.
- Accelerate BLAS handles dense F32 matrix operations; vDSP and vForce handle suitable vector operations.
- Cached grouped-query attention carries KV state across tokens.
- RMSNorm and softmax use F64 reductions for numerical stability.
There is no GPU fallback or alternative backend.
Fast tests do not require model weights:
./zigw build test
./zigw build test -Doptimize=ReleaseFastThe end-to-end smoke test loads LFM_WEIGHTS_FILE and checks a deterministic
continuation:
./zigw build e2e -Doptimize=ReleaseFastUse benchmark.sh for a combined throughput and correctness run:
MAX_NEW=128 ./benchmark.sh "$LFM_WEIGHTS_FILE"
MAX_NEW=128 ./benchmark.sh "$LFM_WEIGHTS_FILE" 4Benchmark output reports prefill throughput, decode throughput, and the
generated continuation used for the correctness check. To collect a macOS
sample(1) profile instead:
./profile_macos.sh "$LFM_WEIGHTS_FILE"The thermally controlled comparison recorded on 2026-07-26 used this machine:
| Component | Specification |
|---|---|
| Machine identifier | Mac17,6 |
| SoC | Apple M5 Max |
| CPU topology | 18 physical/logical cores: 6 Super + 12 Performance |
| Memory | 64 GiB |
| Operating system | macOS 26.5.2, build 25F84 |
| Power | AC power; Low Power Mode disabled |
| Zig | 0.16.0 |
| Build | Native target, ReleaseFast, all 18 CPU workers |
| Temperature monitor | macmon 0.7.2 |
Both binaries used the official LFM2.5-8B-A1B-Q4_K_M.gguf, exactly
5,155,564,768 bytes with SHA-256
4923ec14f06b968b74d663e5949867d2d9c3bf13a20b8be1a9f9af39989b2bb0.
The baseline was main at cf61ab092339. The optimized binary was commit
5943349, which is now part of main.
The controlled comparison ran benchmark.sh with MAX_NEW=256, using its
exact 233-token long Copenhagen prompt. macmon measured CPU temperature
immediately before each run:
| Build | Starting CPU temperature | Prefill | Decode |
|---|---|---|---|
Baseline (cf61ab0) |
41.92 °C | 215.4 tok/s | 85.04 tok/s |
Current (5943349) |
42.14 °C | 220.6 tok/s | 95.67 tok/s |
The current implementation improves prefill throughput by 2.4% and decode throughput by 12.5% in the matched run. Both continuations passed the benchmark's Copenhagen sanity check.
Absolute throughput varies with temperature, background load, prompt length, and growing attention context.
On 2026-08-11, three MAX_NEW=256 runs of the same 233-token benchmark prompt
with the official LFM2.5-2.6B-Q4_K_M.gguf measured the worker-dispatch change:
| Build | Average prefill | Average decode |
|---|---|---|
| Before generation-based worker dispatch | 201.0 tok/s | 68.33 tok/s |
| Current default scheduling | 199.8 tok/s | 90.33 tok/s |
This is a 32.2% decode-throughput improvement with effectively unchanged
prefill throughput. Prompt batches still use all 18 logical CPUs on the test
machine; memory-bound decode automatically uses 12 worker partitions. Passing
--num-threads continues to override the automatic choice.
The mapped Q4_K_M weights occupy approximately 1.67 GB for LFM2.5-2.6B or 5.2 GB for LFM2.5-8B-A1B. Runtime memory also includes the KV cache, convolution state, tokenizer tables, a small set of pre-decoded F32 tensors, and transient forward-pass arenas. Total use primarily depends on the selected model and context length.
lfm.zig is an independent Zig implementation inspired by
maximecb/bebelm. While it shares the same
high-level goal, it has been reimplemented and optimized specifically for Apple
Silicon, with a focus on CPU inference using Apple-native technologies such as
ARM64 SIMD and Accelerate.
The source code is available under the MIT License. Model weights remain separate artifacts governed by their respective licenses.
See CHANGELOG.md for release history and SECURITY.md for vulnerability reporting.