A Rust-native LLM inference engine. Load a GGUF model and run it locally, on your laptop's CPU, an Apple GPU, a cross-platform Vulkan/DX12 GPU, a phone, or in the browser, from a single dependency-free core.
[!NOTE] The 0.7.0 release adds the native Qualcomm Hexagon NPU backend, dynamic CPU topology discovery and worker threadpool resizing, and core lifecycle enhancements. See the API guide for usage and compatibility limits, and Releases for published builds.
.gguf file, a
LeapBundles manifest, or a
bundle id; it can auto-download and cache models from Hugging Face.AudioPipeline in pure Rust, linked through a single unified binary across mobile and desktop.Dispatch is on the GGUF general.architecture string. Selected supported
families are listed below; tensor layouts and quantization must also match the loader:
| Architecture | Models | Modalities |
|---|---|---|
lfm2 | Liquid LFM2 / LFM2.5 (the canonical LeapBundles family) | text, vision, audio |
lfm2moe | Liquid LFM2.5-8B-A1B (routed mixture-of-experts) | text |
llama | LLaMA 2 / 3, and classic Mistral 7B (ships as GGUF arch llama) | text |
qwen2, qwen3 | Qwen2 / Qwen2.5 / Qwen3 | text |
qwen35 | Qwen 3.5 / Ornith 1.0 (interleaved Gated Delta Net hybrid) | text |
granite | IBM Granite 3.x, and the dense Granite 4.1 line (3b / 8b / 30b) | text |
minicpm, minicpm5 | MiniCPM / MiniCPM5 (MiniCPM-1B / 2B) | text |
nanbeige | Nanbeige 4.2 (looped-layer dense transformer) | text |
phi3, phi | Phi-3 / Phi-3.5 / Phi-4-mini (fused QKV, packed SwiGLU FFN) | text |
mistral3 | Ministral 3 (Norm YaRN RoPE, attention temperature scaling, Tekken BPE) | text |
bailingmoe3, bailingmoe | Ling 3.0 Tiny (hybrid KDA linear, MLA latent attention, and MoE) | text |
CPU loading also supports families such as Gemma 2/4, Olmo 2/3, Granite Hybrid, Falcon H1 and Mamba-2. See the loading dispatch for architecture aliases and backend admission rules.
Metal and wgpu support lfm2, lfm2moe, llama, qwen2, qwen3, granite,
minicpm, minicpm5, nanbeige, phi3, phi and mistral3 / ministral3
(YaRN is folded into the rope kernel's frequency table; attention temperature
scaling is not implemented on GPU, so those models are limited to the context
below its floor, the original context length, and warn when that caps a longer
request). Qualcomm Hexagon NPU supports lfm2, lfm2moe, the dense families
(llama, qwen2, qwen3, granite, gemma2, olmo2/olmo3, mistral3,
phi3 and others listed in docs/HEXAGON_NPU.md) and qwen35
(HexagonLfmModel); those paths are host-tested and still need device validation.
The listed bailingmoe3 family uses CPU; requesting an unsupported GPU or NPU
backend explicitly returns an error. Auto selection can fall back to CPU.
The LFM2 and supported dense-transformer paths use batched-GEMM prefill,
including on CPU, with tiled flash attention for long prompts where supported.
lfm2moe's routed feed-forward block is the one exception to that
batched-GEMM sentence, and it is deliberate: each token picks its own 4-of-32
experts, so an expert's weights are re-read once per routed token rather than
once per prompt. (The GPU backends still issue one dispatch per projection for
the whole chunk, not one per token; what they do not do is share a weight row
between two tokens that chose the same expert.) Grouping tokens by expert is a
follow-up, kept separate from getting the routing arithmetic right. On the two GPU backends its expert kernels
are also Q4_0-only, and its routed-FFN LoRA targets (the router and the
per-expert projections) apply on CPU only: an adapter carrying them is refused
rather than half-applied.
CeraEngine auto-attaches the
vision encoder; Session::append_image runs image → ViT → projector → prefill.
The ViT encoder runs on the GPU (Metal or wgpu) with a CPU fallback. Verified
against LFM2.5-VL-450M.lfm2-audio-v1): feed PCM audio in, and
(with a vocoder) decode audio out.cera::hotword), Silero VAD v5 (cera::vad), and OpenAI Whisper speech-to-text (cera::model::whisper).Cera dispatches to the fastest available backend at runtime (--device auto),
or you can pin one:
| Backend | --device | Platforms | Notes |
|---|---|---|---|
| CPU | cpu | everywhere | Scalar reference + NEON (aarch64) / AVX2 (x86_64) kernels; optional Accelerate/OpenBLAS via the blas feature; dynamic cpuset awareness |
| Native Metal | metal | macOS, iOS | Hand-written MSL shaders, single-encoder dispatch, GPU argmax |
| Qualcomm Hexagon NPU | hexagon | Android, Linux (aarch64) | FastRPC Unsigned Process Domain, shared DMA memory, repacked 32x32 Q4_0/Q8_0 matrix kernels on Snapdragon 8 Gen 2 / 8 Gen 3 / 8 Elite |
| wgpu | gpu | macOS, Linux, Windows, browser | WGSL shaders over Metal / Vulkan / DX12 / WebGPU |
--device auto prefers native Metal on macOS and iOS, Qualcomm Hexagon NPU on supported Snapdragon devices, and wgpu where a GPU is available, falling back to CPU otherwise. A routed-expert model too large for the DSP's mappable memory (LFM2.5-8B-A1B) skips the NPU under auto, since paging it is slower than the CPU; set CERA_HEXAGON_PAGE_EXPERTS=1, or ask for --device hexagon, to page it.
Cera provides a native backend for Qualcomm Hexagon NPUs and Compute DSPs on Snapdragon 8 Gen 2 (v73), Snapdragon 8 Gen 3 (v75), Snapdragon 8 Elite (v79), and next-generation Snapdragon architectures (v81/v85). See the Hexagon NPU Guide for full architecture and Android packaging details.
/dev/fastrpc-cdsp), enabling third-party Play Store applications to execute on HTP/HVX hardware without root privileges or vendor signing keys.rpcmem (DMA-BUF / ION) mapped into both CPU and DSP address spaces, eliminating bus copying.libcera-htp-v{73,75,79,81,85}.so) into the host binary and extracts them automatically at startup. Skeletons incorporate extended operators (Conv1D, ConvTranspose1D, Snake, UnaryStep, Sum).CERA_HEXAGON_BATCH_TENSORS=<n> caps each batch at n tensors for bisection), ping-pong scratch memory isolation, FastRPC latency QoS (FASTRPC_CONTROL_LATENCY = 100 µs), FastRPC driver wakelock (FASTRPC_CONTROL_WAKELOCK), and Q8_0 quantized KV cache (~47% memory reduction over F16).HtpOpCode::Argmax, opcode 62), reading only 4 bytes of token ID from rpcmem rather than copying or invalidating 128 to 256 KB of float32 logits, eliminating host CPU allocations and memory traffic during greedy decode.forward_prefill_logits_all). When paired with prompt-lookup drafting (ngram=2, k=4), speculative decoding lifts 350M decode throughput from 164.0 tok/s to 259.5 tok/s (peak 260.2 tok/s) and 2.6B decode from 26.7 tok/s to 47.7 tok/s (peak 48.5 tok/s) on Snapdragon 8 Elite.HexagonDepthformer executes all 8 autoregressive passes (48 transformer layers per audio frame) entirely on the NPU using HTP GEMV, RMSNorm, RoPE, and FlashAttnExt, alongside on-DSP audio detokenization (LayerNorm, linear GEMM, SwiGLU, Conv1D).Weights run in Q4_0, Q4_1, Q8_0, Q4_K, Q5_K, and Q6_K, plus dense F32. Activations are dynamically quantized to Q8_0 for fast integer GEMV on CPU.
Those are GGML tensor types, and dispatch is per tensor, so what decides
whether a file runs is the mix inside it, not its Q4_K_M-style label. K-quant
downloads usually work, but the label is not a guarantee: llama.cpp substitutes
Q5_0 / Q5_1 / IQ4_NL for tensors whose rows are not a multiple of 256, and cera
has no kernel for those. cera inspect --model <file> lists the per-tensor types,
and an unsupported one fails at weight resolution naming the tensor rather than
failing anonymously, with the type name too, for the types cera knows about (an
IQ type reports its numeric id).
Backends are not uniform, so a file that runs on one may not run on another. On
native Metal, Q4_1 works as a projection weight but not as token_embd /
output; --device auto falls back to wgpu or CPU on its own, while
--device metal reports the gap. F16/BF16
tensors parse, and are dequantized on the LoRA, vision, and audio paths, but the
transformer weight and token-embedding paths have no kernel for them; an
F16-weight LLM is not a supported configuration.
One Rust core, consumed from many places:
| Target | Crate / package | Consumers |
|---|---|---|
| Rust (Engine) | cera | any Rust project (cargo add cera) |
| Rust (API client) | cera-client | any Rust project (cargo add cera-client); OpenAI and OpenRouter endpoints |
| CLI | cera-cli | the cera binary |
| Kotlin / Swift / Python | cera-ffi (UniFFI) | JVM, Apple platforms (LLMs, ChatSession, VAD, KWS, Whisper) |
| Android | cera-ffi-kotlin | Android apps (AAR; includes Qualcomm Hexagon NPU support) |
| iOS / macOS | Package.swift (SwiftPM XCFramework) | Apple apps (.package(url:)), Metal GPU (Auto: Metal → CPU) |
| Flutter | cera_ffi_flutter | cross-platform apps; ships the native library per platform (LLMs, ChatSession, VAD, KWS, Whisper, Hexagon NPU) |
| Dart (no Flutter) | cera_ffi | CLI / server; bring your own cera-ffi cdylib (ChatSession, explicit loading) |
| Browser / Node | cera-wasm (@hyeons-lab/cera-wasm) | WebAssembly + WebGPU |
A complete SwiftUI example app (streaming chat + embeddings + LoRA) that consumes
the published Swift Package lives in examples/CeraChat;
it doubles as a real-device Metal validation harness.
Force the model's output to match a grammar, useful for JSON, tool calls, or any schema. Cera ships a byte-level GBNF engine (mirroring llama.cpp's) that masks the sampler each step so only grammar-valid tokens can be produced.
# Constrain generation to JSON (a token limit can still truncate it)
cera run -m model.gguf -p "List 3 colors as JSON" --json
# Any custom GBNF (inline or @file)
cera run -m model.gguf -p "..." --grammar @schema.gbnf
Supports literals, character classes, alternation, grouping, and repetition
(* + ? and bounded {n,m}). Available from every binding, not just the
CLI: in Rust set GenerateOpts.grammar to a compiled Grammar (Grammar::parse(gbnf)?),
while the Kotlin/Swift FFI (GenerateOpts.grammar) and browser/Node WASM
(GenerateOpts.setGrammar(gbnf)) take the GBNF string directly and compile it
natively, so mobile and web apps use the same grammar rules. A token limit or
cancellation can leave an incomplete value; parse and validate the final output.
Cera compiles a subset of JSON Schema directly to GBNF without external tooling:
let grammar = Grammar::from_json_schema_str(&schema_json)?; or opts = opts.with_json_schema(&schema_json)?;let constrainedOpts = try opts.withJsonSchema(schema)val constrainedOpts = opts.withJsonSchema(schema)constrained_opts = opts.with_json_schema(schema)chat.streamJson(opts, schemaJson)opts.setJsonSchema(schema) or const constrainedOpts = GenerateOpts.withJsonSchema(opts, schema)Pass the returned constrained options to generation. The Swift/Kotlin/Python
helpers and WASM withJsonSchema return copies; WASM setJsonSchema updates
opts in place.
Supports nested properties and required keys, bounded arrays, scalar enums,
local $defs/definitions and chained $ref resolution. allOf accepts only
supported scalar wrappers and compatible object merges. Numeric ranges, string
patterns and other unimplemented keywords are not enforced. See the
supported subset before relying on a schema.
Give the model a set of tools (OpenAI "function" schemas) and get the calls it
makes back out as structured data. Cera renders the tools into the model's chat
template and parses tool calls from the reply, format-aware, since the wire
format isn't interchangeable: LFM2/LFM2.5 emit Pythonic calls
([get_weather(city="Paris")]) while Hermes-style Qwen2.5/Qwen3 emit JSON
(<tool_call>{"name": …, "arguments": {…}}</tool_call>). The format is detected
from the GGUF architecture.
# Let the model decide whether/how to call a tool (free text or a call)
cera run -m model.gguf -p "What's the weather in Paris?" \
--tools '[{"name":"get_weather","description":"Get weather for a city",
"parameters":{"type":"object",
"properties":{"city":{"type":"string"}},"required":["city"]}}]'
# Constrain tool-call syntax after the model emits a call marker
cera run -m model.gguf -p "..." --tools @tools.json --constrain-tools
With --constrain-tools a lazy grammar trigger keeps generation free until
the model starts a tool call, then constrains function/argument names and outer
value syntax. Its separate tool grammar does not enforce required arguments,
uniqueness or nested item/property schemas. Prose and truncated calls remain
possible; parse and validate arguments before executing a tool. In
--tools mode stdout is machine-readable: only the JSON array of calls
([] when the model answered in prose); the assistant reply and timing stream to
stderr, so … --tools tools.json | jq just works.
Available from every binding, not just the CLI: Rust (cera::tools),
Kotlin/Swift (applyChatTemplateWithTools, parseToolCalls, toolGrammar,
detectToolFormat), and browser/Node WASM (the same names); see each crate's
README for the API surface.
Cera promotes the high-level SessionChat coordinator (cera::session::chat / ChatSession in FFI) to the public API:
Idle to PromptReady; generation reaches TurnComplete only on a profile terminal. Token limits, cancellation and nonterminal stops can leave Interrupted; zero-token/no-progress calls can preserve PromptReady for retry. Recovery may restore, reset or mark execution unusable.TemplateFamily, ProfileBuilder); unsupported continuation profiles are rejected.chat.set_tools(tools); tool calls are parsed into TurnResult.tool_calls and tool execution outputs are ingested with chat.ingest_tool_response(...).use cera::{Message, SessionPhase};
let mut chat = session.into_chat().map_err(|(_, err)| err)?;
chat.ingest(&Message::user("Hello!"))?;
let result = chat.complete(&opts)?;
println!("Assistant: {}", result.text);
// Continue only after the profile's terminal marker ended the turn.
if chat.phase() == SessionPhase::TurnComplete {
chat.ingest(&Message::user("What did I just say?"))?;
let reply = chat.complete(&opts)?;
println!("Assistant: {}", reply.text);
} else {
// Start a fresh conversation; alternatively replace the full history.
chat.reset()?;
}
Cera provides native reactive streaming wrappers tailored for each language ecosystem:
AsyncThrowingStream<String, Error> via chat.stream(opts: opts) and chat.streamJson(opts: opts, schemaJson: schema).Flow<String> via chat.stream(opts) and chat.streamJson(opts, schema).Iterator[str] via chat.stream(opts) and chat.stream_json(opts, schema).Stream<String> via chat.stream(opts) and chat.streamJson(opts, schema).chat.stream_text(&opts, |chunk| { ... })? yielding real-time text fragments into a callback closure.Stream completion does not imply a completed chat turn. Inspect the phase before the next user message; reset or replace interrupted conversations. Abandoning an active stream requests cancellation. Let that operation finish before reusing the handle. See streaming and cancellation.
CPU Session and Chat support resumable snapshots. Native Metal/Hexagon/wgpu Session
checkpoint and restore operations reject backend-owned state; browser
WebGpuSession has a separate device snapshot API.
SessionCheckpoint / ChatCheckpoint formats with magic headers (CERASCHK / CERACHAT), structural fingerprints and versioned headers. Fingerprints are not cryptographic model identities.checkpoint.save_to_file(path) and SessionCheckpoint::load_from_file(path) with atomic writes and clean error recovery.Version 0.6.2 and newer validates row geometry, KV precision and compression identity, including TurboQuant seeds. Recreate older f16/TurboQuant snapshots whose fingerprints lack compression identity; plain f32 fingerprints are unchanged. See checkpoint compatibility.
Runtime LoRA. Load a LoRA adapter, a llama.cpp GGUF (from
convert_lora_to_gguf) or a PEFT .safetensors, and attach it to a session. The
delta is applied at inference time (y += scale·B·(A·x)), never merged into the
weights, so the base model stays quantized and adapters hot-swap / unload per
request at ~no cost. Runs on CPU, Metal, and wgpu (batched-GEMM prefill +
decode) and is dimension-checked against the model at attach. The one gap is
lfm2moe's routed-FFN targets, which apply on CPU only; on a GPU backend such an
adapter is refused with CeraError::LoraUnsupportedByBackend rather than
partially applied.
Hidden-states extraction. Pull the per-token last-layer hidden state
(post-final-RMSNorm, the llama.cpp --pooling none vector) straight out of the
engine, reflecting the active adapter. This unblocks classifier / extractor /
embedding heads, e.g. a section router: LFM2.5 + a route_section LoRA + a
small linear head over the mean-pooled hidden state.
Both are available from every binding:
// Swift / Kotlin (UniFFI): same shape on both
let adapters = try LoraAdapters.fromSafetensors(path: p, alpha: nil)
try session.attachLora(adapters: adapters) // hot-swap-able
let pooled = try session.hiddenStatesMeanPooled(tokens: toks) // [hidden_size]
// Browser / Node (WASM)
const adapters = LoraAdapters.fromSafetensorsBytes(bytes, undefined);
session.attachLora(adapters);
const pooled = session.hiddenStatesMeanPooled(tokens); // Float32Array
Cera includes the first implementation of TurboQuant (arXiv:2504.19874, Google Research 2025: TurboQuant: Extreme KV Cache Compression) for LFM2, compressing the KV cache to ~3 bits/key + ~2 bits/value (~12× vs f32) with near-lossless quality and no calibration. On a 1.6B LFM2 model at 4K tokens that's ~192 MB → ~16 MB of KV, with decode staying within ±5% of f32.
The default KV cache is f16 on the CPU backend (llama.cpp's default cache type; --kv-cache-keys auto),
which halves the bytes a decode streams at depth; --kv-cache-keys f32 keeps full precision. Backends that
keep their own cache (wgpu, Metal, Hexagon) are unaffected.
Enable TurboQuant on the CLI:
cera run -m lfm2.gguf -p "Hello" --kv-cache-keys tq3 --device cpu
cera run -m lfm2.gguf -p "Hello" --kv-cache-keys tq3 --device gpu # wgpu
cera run -m lfm2.gguf -p "Hello" --kv-cache-keys tq3 --device metal # native Metal
Supported on all three backends (CPU, wgpu, and native Metal) for both
decode and chunked prefill. On the GPU backends the compressed cache lives in GPU
buffers and the prefix-cache snapshots are byte-compatible with the CPU's, so all
three write the same TQK1/TQV1 blob format. Two GPU-specific restrictions:
head_dim must be a power of two ≤ 128 and a multiple of 32 (every supported
model is 64 or 128), and only the both-sides mode (tq3) is available; the
single-sided debug modes tq3-keys / tq3-values fall back to the backend's
uncompressed KV there (f32 on wgpu, f16 on Metal) with a warning. --n-keep
context shift is not supported with any TurboQuant mode on any backend.
See cera/src/turboquant.rs for the algorithm (PolarQuant + QJL).
cera::kv_cache::KvPrefixCache retains token-prefix snapshots in warm memory and,
when disk caching is enabled and configured, a cold disk tier. Its insert_anchor
and find_deepest_semantic_anchor APIs let callers tag and select matching
prefix snapshots using caller-supplied boundary metadata. The standard engine
cache path does not automatically select semantic anchors.
FlatBuffers v2 persistence preserves each snapshot's existing precision, including TurboQuant data when already compressed. Callers choose compression before capturing snapshots; moving a snapshot to disk does not recompress it.
Cera includes support for DSpark (arXiv:2407.08608, 2024: DSpark: High-Throughput Speculative Decoding with Lightweight Drafters), accelerating inference throughput via parallel multi-token draft verification:
cera::model::dspark) that share embedding and GGUF weight backings with the target model without redundant memory mapping.Metal, Hexagon, and wgpu permit one live Session per loaded model. A second session
returns Busy until the first is dropped; reset and cancellation keep ownership.
CPU models continue to support shared weights across concurrent sessions. Load
separate GPU or NPU models for simultaneous conversations. The
session ownership walkthrough shows the public API, cleanup behavior
and executable checks.
LFM2-Audio transcription during a live GPU conversation uses a cached secondary model built from retained weights. This preserves conversation KV and costs additional model memory and first-use setup; the walkthrough includes a native Dart consumer for transcription and seeded generation.
The standalone FfiWhisperModel loads a Whisper GGUF from a path or owned bytes
and transcribes 16 kHz mono float PCM through Swift and Kotlin. Use
transcribeAsync for a background decode; whisperDefaultTranscribeOpts supplies
language, translation, timestamps, token limit and temperature defaults.
The Swift/Kotlin recording examples
are compiled and executed against the generated bindings. Build matching wrappers
and native libraries from the same revision.
# Exercise loading, nonempty decoding, async calls and ownership without downloads.
cargo test -p cera-ffi --test whisper_ffi --locked --offline
Each call handles at most 30 seconds; segment longer recordings first. This is
separate from LFM2-Audio's CeraEngine.transcribe. Dropping its Rust future
cooperatively cancels decoding. Kotlin propagates coroutine cancellation; the
pinned Swift wrapper does not propagate Task.cancel().
Cera includes a native, pure-Rust implementation of Silero VAD v5 (cera::vad), enabling zero-dependency, real-time voice activity detection across desktop, mobile, and web:
VadIterator) processing 512-sample frames (32ms at 16kHz) with state persistence.cera::vad::SileroVad), CLI (cera vad), Swift/Kotlin via UniFFI, and Dart/Flutter via cera_ffi.# Detect speech timestamps from a WAV file
cera vad --model models/silero_vad.gguf --audio speech.wav --json
# Convert official Silero VAD v5 ONNX model to GGUF
python scripts/convert_silero_vad.py silero_vad.onnx models/silero_vad.gguf
Cera includes a native, pure-Rust Keyword Spotting (KWS) engine (cera::hotword) designed for always-on, low-power wake word detection on mobile and edge devices:
kws.*.LogMelFrontEnd and HotwordDetector retain scratch buffers for acoustic features and inference. Event strings, growing stream buffers and foreign argument/result marshaling can still allocate; process chunks serially on a background audio worker.HotwordIterator): Interleaves Silero VAD gating (512 samples at 16 kHz or 256 at 8 kHz) with configurable hop KWS checks. It skips acoustic evaluation when the buffered window has no recent speech; other model sample rates disable VAD gating.cera::hotword), Kotlin/Android, Swift/iOS, Python via cera-ffi, and Flutter via cera_ffi_flutter.# Export and fold PyTorch wake word weights into a self-describing GGUF container
python scripts/convert_kws.py --checkpoint model.pt --output models/hey_liquid.gguf
# Train a custom wake word model with automated phonetic near-miss negative synthesis
python tools/wake_word/train_kws.py --phrase "Hey Liquid" --epochs 25 --output models/hey_liquid.gguf
Cera provides a pure-Rust implementation of OpenAI Whisper ASR (cera::model::whisper), exposed across all foreign language bindings via UniFFI (cera-ffi):
libcera_ffi.so or CeraFFI.xcframework) for wake word detection, speech activity gating, and full speech-to-text transcription without external C++ or JNI dependencies.transcribe_async Rust future signals a running decoder to stop at its next cancellation check. Kotlin propagates coroutine cancellation; the pinned Swift wrapper does not propagate Task.cancel(). See the Whisper examples and limitations.cera::model::whisper), Swift, Kotlin, Python, and Dart/Flutter.Cera provides a unified stateful streaming audio pipeline (cera::audio_pipeline::AudioPipeline and cera-ffi FfiAudioPipeline) connecting Silero VAD v5, Keyword Spotting, and Whisper ASR:
ListeningForHotword, ListeningForSpeech, SpeechActive, and Transcribing states with independent audio buffer ownership.max_utterance_ms without resetting Silero VAD recurrent hidden states across continuation segments.AudioPipelineBuilder::with_diarizer, or FfiAudioPipeline.fromFilesWithDiarizer), or the Nemotron-3-Diarization add-on (with_diarizer_nemotron3 / fromFilesWithDiarizerNemotron3, 8 speakers at 10 ms frames), and every transcribed utterance is followed by an UtteranceLabeled event with its speaker slot (0 to 3, or 0 to 7), once the diarizer has covered it. The diarizer hears the same samples as the pipeline, so both share one clock, and either diarizer can run on the Hexagon NPU (prefer_npu, with a GGUF converted using --tail-outtype q8_0; see scripts/sortformer/README.md and scripts/nemotron3_diarization/README.md). Compare the two side by side with cera diarize --compare.AudioPipeline), Swift, Kotlin, Python, and Dart/Flutter (FfiAudioPipeline).Cera supports direct loading and streaming execution from Hugging Face model repositories:
--hf org/repo).--quant-strategy fast-mse, hqq, quarot).$HOME/.cache/cera.# Run directly from a Hugging Face GGUF repository
cera run --hf LiquidAI/LFM2.5-1.2B-Instruct-GGUF --quant Q4_0 --prompt "Explain quantum computing."
# Quantize and run on-the-fly from a remote SafeTensors repository
cera run --hf LiquidAI/LFM2.5-350M --quant Q4_0 --quant-strategy fast-mse --prompt "Hello"
# Install the CLI (CPU-only build)
cargo install cera-cli --locked
# ...or with a GPU backend
cargo install cera-cli --locked --features metal # or: --features gpu
# ...or build from source
just release # optimized LTO build → target/release/cera
# Generate from a local GGUF
cera run --model model.gguf --prompt "Explain quantization in one sentence."
# Auto-download a bundle from Hugging Face and generate
cera run --bundle-id LFM2.5-1.2B-Instruct --quant Q4_0 --prompt "Hello"
# Interactive multi-turn chat (keeps the prefix cache warm across turns)
cera chat --bundle-id LFM2.5-1.2B-Instruct --quant Q4_0
# Or with no model source at all: pick one from the published catalog
# interactively (needs a terminal, and not --no-tui)
cera chat
# Pick an accelerator backend
cera run -m model.gguf -p "Hi" --device metal # or: gpu, npu, hexagon, cpu, auto
Using the library directly:
use cera::{CeraEngine, EngineConfig, GenerateOpts, Message, SessionConfig};
let engine = CeraEngine::from_path("model.gguf", EngineConfig::default())?;
let mut session = engine.new_session(SessionConfig::default())?;
// Raw prompt completion:
session.append_text("Once upon a time")?;
let opts = GenerateOpts { max_tokens: 128, ..Default::default() };
let summary = session.generate(&opts, &mut sink)?; // sink: your ModalitySink
// Or conversational chat coordination (delta-only prefill, live KV retention):
// Start a new conversation after the raw completion above.
session.reset()?;
let mut chat = session.into_chat().map_err(|(_, err)| err)?;
chat.ingest(&Message::user("What is the capital of France?"))?;
let reply = chat.complete(&opts)?;
println!("Assistant: {}", reply.text);
See the cera crate README for the full library API.
This checkout also exposes explicit loading alongside the existing constructors:
use cera::{ModelLoader, ModelSource, SessionConfig};
let model = ModelLoader::new(ModelSource::path("model.gguf")).build_generative()?;
let mut session = model.create_session(SessionConfig::default())?;
drop(model); // The session retains its loaded resources and live KV state.
session.append_text("Once upon a time")?;
Run the complete text-completion example:
cargo run -p cera --example explicit_loading -- model.gguf "Once upon a time".
For multi-turn chat, run the conversational chat example:
cargo run -p cera --example chat -- model.gguf.
The API refactor also provides runnable multi-language examples,
including conversational chat (Rust, Swift,
Kotlin, Python, Dart),
session continuation, and vision/draft loading.
The audio walkthrough demonstrates PCM input and output with locally generated test weights.
The remote companion examples run HF discovery, downloads, cache repair and retained vision/audio/draft execution against a local HTTP fixture.
The HF revision examples load changed GGUF revisions into separate cache entries, verify defaults and keep a CPU session live across another revision load. They also exercise failed metadata resolution and explicit commit checks.
The SafeTensors conversion examples exercise real conversion, corruption repair and option-sensitive cached reloads. The examples verify checkpoint bytes, pin inputs to one upstream commit, and refresh changed revisions. Retained models execute after replacement, and sessions continue after their parent model is released.
The persistent cache examples demonstrate same-path weight replacement, unchanged-weight disk reuse and retained live sessions.
| Command | Purpose |
|---|---|
run | One-shot inference: text, optional grammar/JSON or tool calling (--tools), plus image/audio input for VL/Audio bundles |
chat | Interactive multi-turn REPL with a persistent KV prefix cache. Given no model source at all on a terminal (and without --no-tui), opens a picker over the published catalog |
embed | Extract last-layer hidden-state embeddings: mean-pooled, or --per-token for the full matrix |
logits | Dump next-token logits over the vocabulary (single prefill), handy for cross-backend parity checks |
convert | Convert a local SafeTensors model directory to a quantized GGUF file (including d1_omni decision models) |
decide | Answer structured questions over text, image, or audio state using a bidirectional d1 decision model |
inspect | Dump a GGUF's metadata, tensor shapes, and resolved backend tier |
cpu | Print the host's CPU backend tier + detected SIMD features (no model needed) |
thermal | Print current and forecast device thermal headroom on Android API 30+ (unavailable elsewhere) |
tokenize | Encode text to token IDs (e.g. to compare against Hugging Face) |
bench | Measure decode/prefill throughput with p10/p50/p90/mean/stddev |
gemv-bench | Microbenchmark wgpu quantized GEMV kernels (--features gpu) across m,k shapes and verify CPU parity |
gemm-bench | Microbenchmark wgpu quantized GEMM kernels (--features gpu) across m,n,k shapes and verify CPU parity |
list-bundles | List bundles available on LiquidAI/LeapBundles |
list-hf | Discover and list GGUF model files in a Hugging Face repository |
download-bundles | Prefetch bundle manifests + model files without loading |
vad | Voice activity detection on audio files using Silero VAD v5 |
diarize | Speaker diarization on audio files using Streaming Sortformer or Nemotron-3-Diarization (plus --compare) |
transcribe | Speech-to-text transcription on audio files using OpenAI Whisper or LFM2-Audio |
compare-quants | Audit metadata, tensor inventory, quantization fidelity, and logit parity between Cera-converted models and reference community GGUFs |
Defaults are chosen to be correct-or-harmless across a wide range of hosts, not optimal on any one. The knobs below override them.
| Variable | Effect |
|---|---|
CERA_DECODE_THREADS=<n> or CERA_DECODE_THREADS=auto | Worker count for decode (per-token GEMV). A fixed <n> overrides the automatic sizing below (clamped to the detected count); auto selects it. |
CERA_DECODE_SIZING=off | Disable model-aware decode sizing (0 / false / off, case-insensitive) and fall back to the flat cap (detected perf cores, capped at 12). Use this to A/B the sizing on a new machine. |
CERA_DECODE_NARROW=<n> / CERA_DECODE_WIDE=<n> | Override the two widths the sizing picks between. Defaults derive from the physical core count: physical/2 capped at 12, and physical + physical/4 capped at 24; the narrow arm never exceeds the wide one. Setting either also forces sizing on where it would otherwise be declined; on a host whose physical core count is undetectable, both must be pinned. |
CERA_DECODE_BPD_KB=<n> | Bytes-per-dispatch threshold (decimal KB) separating the narrow arm from the wide one. Default 2500. Unlike the two above, this does not force sizing on where it is declined; it moves the threshold, it does not pin a width. |
CERA_THREADS=<n> | Overrides the detected performance-core count, which the decode/prefill pools are sized from. Clamped to the number of pinnable cores on hosts that have any, with a warning: past that the surplus workers run unpinned and contend with pinned ones that are spin-waiting (measured 35x slower on a Tensor G4). Not clamped where nothing gets pinned anyway, i.e. hosts with no affinity or CERA_PIN=0. |
CERA_PREFILL_THREADS=<n> | Prefill pool width on its own, without moving decode. May reach past the performance cores up to every pinnable core, for sweeping a part where widening might pay; it does not on the parts measured so far. Subject to the same clamp rule as CERA_THREADS, and to CERA_THREADS itself, which lowers the ceiling it is clamped against. |
CERA_PIN=0 | Disables worker affinity pinning, for host apps that manage thread placement themselves. Also lifts the clamps above, since with nothing pinned there is no oversubscription cliff to guard against. |
CERA_POOL_STATS=1 | Annotates each cera bench run with pool fan-out health: how many dispatches wanted more than one worker, and how many of those silently ran serially because the pool was busy. The counts are exact; the work percentage mixes units across dispatch kinds, so read the counts. |
CERA_CPU_TIER=<tier> | Caps the SIMD tier (e.g. avx2, avx512). May only downgrade, useful for A/B-ing a kernel path. |
CERA_ATTN_PREFETCH=<n> | Positions ahead to prefetch KV rows in CPU f16 decode attention on aarch64 (default 16; 0, off, false disables; clamped to 1024). |
CERA_ATTN_PREFETCH_L2=<n> | Positions ahead for L2 prefetch in CPU f16 decode attention on aarch64 (default 64; 0, off, false disables; clamped to 1024). |
CERA_Q6K_PREFETCH=<n> | Bytes ahead to software-prefetch 4 cache lines per block in aarch64 Q6_K GEMV on tensors >= 32 MB (default 16384; 0, off, false falls back to the 1-line 512 B prefetch; clamped to 1048576). |
CERA_LM_HEAD_NO_GEMM=1 | Puts the LM-head projection in forward_prefill_logits_all back on the per-row loop the batched GEMM replaced, so both halves of a speculative-decoding A/B run from the same binary. Measurement lever only: both paths compute the same projection, to within f32 accumulation order. |
CERA_Q4_DEC4=off | Disables the 4-row interleaved Q4_0 decode repack (0 / false / off), which costs one extra copy of each repacked weight (about +155 MB resident on the 450M, measured on a Galaxy S25 Ultra and not seen on an Apple-silicon Mac) and decodes bit-identically to the repacked prefill GEMMs. A memory/A-B lever. |
CERA_CPU_SMMLA_TILED=off | Disables 4-token tile activation interleaving for aarch64 Q4_0 smmla GEMMs (0 / false / off), restoring the columnar kernels. An A/B performance lever. |
CERA_CPU_ATTN_TILED=off | Disables the register-tiled NEON prefill attention (0 / false / off), restoring the whole-head blocked kernel. An A/B performance lever. |
CERA_VIT_INT8=off | Runs the CPU vision tower's linears on the f32 path instead of the int8 GEMM (roughly 2 to 3x slower, measured on an Apple-silicon Mac; 0 / false / off). |
CERA_VIT_PROFILE=1 | Prints per-phase wall time of each vision-tower encode (CPU and GPU). Off by default and free when off. |
CERA_GPU_PREBUILD=off | wgpu decode: stops finishing the next token's command buffer while the current one executes (0 / false / off). A/B lever; decode is about 20% slower with it off on Adreno 830. |
CERA_GPU_ATTN_SPLIT=off | wgpu decode: uses the single-kernel attention instead of the split-K attention (head_dim 64 only; 0 / false / off). |
CERA_WGPU_STREAM_MIN_N=<n> | wgpu prefill: smallest batch size routed to the streaming Q4_0 GEMM (default 1, i.e. every batch). |
CERA_WGPU_GEMM_DIRECT_B=off | wgpu prefill: disables direct-B staging and restores transposed f16 activation pass (0 / false / off). |
CERA_WGPU_GEMM_R1=1 | wgpu prefill: forces the 1-row streaming Q4_0 GEMM kernel everywhere (skips 2-row kernel). |
CERA_WGPU_FUSE_GATE_UP=off | wgpu prefill: disables fused gate/up streaming Q4_0 GEMM with SiLU epilogue (0 / false / off). |
CERA_WGPU_ATTN_F32=1 | wgpu prefill: forces the f32 attention kernel everywhere (skips 64-query fp16 attention kernel). |
CERA_VIT_STREAM=off / CERA_VIT_WARMUP=off | wgpu vision tower: CERA_VIT_STREAM=off restores the generic kernels instead of the fp16 streaming GEMMs and attention; CERA_VIT_WARMUP=off skips the blank-image warm-up encode at load (about 190 ms once). |
RUST_LOG=<filter> | Log level. Defaults to warn, which surfaces things like prefill falling back to the slow per-token path. |
Decode width is sized from the loaded model, because there is no single best value, and, less obviously, model size does not predict it. Measured on a Ryzen AI MAX+ 395 (16 physical / 32 logical), decode tok/s change going from 12 to 20 workers:
| Model | Weights | Bytes/dispatch | 12 → 20 |
|---|---|---|---|
| TinyStories-20M Q8_0 | 21 MB | 0.84 MB | −47% |
| SmolLM-135M Q4_0 | 92 MB | 0.51 MB | −11% |
| LFM2.5-350M Q8_0 | 379 MB | 3.83 MB | +29% |
| SmolLM-360M Q8_0 | 386 MB | 1.50 MB | −7% |
| Llama-3.2-1B Q8_0 | 1321 MB | 10.24 MB | +38% |
Note the middle two: near-identical weight bytes, opposite answers. What separates them is how many pool dispatches a token is split across; LFM2 issues 99 per token against SmolLM-360M's 257, so each dispatch amortizes 2.6× more work against the same fixed barrier cost. Below ~2.5 MB per dispatch decode is barrier-bound and wants fewer workers; above it decode is bandwidth-bound and wants more.
cera computes that ratio from the GGUF at load and picks a narrow or wide width
accordingly. A/B against the old flat cap of 12 (same binary, interleaved,
CERA_DECODE_SIZING=off vs on, the five models above at two prompt depths)
gave +19% mean over those 10 cells with no cell slower (small models gain
too: they get fewer than 12 workers). Scored differently, against each model's
own best measured width across the full 12-model set, the rule reaches 98.1% of
peak versus 90.1% for the flat cap; one combo near the threshold
(LFM2.5-350M Q4_0) lands on the narrow arm when its measurement mildly preferred
the wide one, which is the cost that average already includes.
The pool itself is a process-wide singleton, built once on the first decode dispatch, so the width comes from the first model loaded into a process and stays there for any loaded after it; it does not re-size per load. (The thread count was already inherited that way before this sizing existed.) Embedders running several models in one process should pick the knobs below for whichever model's decode matters, or load that one first.
This is calibrated on one x86 host. The sizing steps aside, leaving the previous flat-cap behaviour, in two cases. On heterogeneous parts, because decode there measured best across all big cores (that evidence is from ARM big.LITTLE; x86 hybrid parts decline by the same argument, without the direct measurement). And on hosts whose physical core count cannot be detected (Windows, BSD, Intel macOS), where deriving a width from the logical count would overshoot badly. Pinning a width forces it on anyway, for sweeping a machine it has not been tuned against: one arm is enough on a heterogeneous host, both are required where the physical core count is undetectable.
Apple Silicon is sized, not declined. M-series parts are heterogeneous in hardware, but cera detects only their P-cores, so neither decline case applies: an M4 Max gets 12 workers for large models (its full P-core count; there is no SMT for the wide arm to spend) and 6 for small ones. That narrow arm is the part of this that has not been measured off x86.
If you run one model repeatedly and care about decode latency, it is still
worth measuring: cera bench --model <path> with CERA_DECODE_THREADS set
sweeps a fixed width, and CERA_DECODE_SIZING=off gives you the old default to
compare against. Interleave the arms rather than sweeping; laptop clocks drift
enough to invent a trend that isn't there.
run, chat,
and bench (--spec and --no-spec).ModalitySink as they decode; Session::cancel() interrupts long prompts
responsively via chunked prefill.n_keep prefix pinning, on CPU
and GPU, keeps generation going past the context window.Cera is a Cargo workspace. The core library does GGUF parsing, quantization, the compute backends, the models, and the tokenizer; everything else is a thin adapter over it.
cera: core librarycera-cli: CLI binary (clap)cera-ffi: UniFFI bindings (Kotlin / Swift / Python)cera-ffi-kotlin: Android packaging (AAR)cera_ffi · cera_ffi_flutter: Dart bindings, and the Flutter plugin that ships a native library for themcera-wasm: wasm-bindgen browser / Node bindingscera-parity: cross-binding parity harness (runs one prompt through every binding and reports drift)See the cera crate README for the module layout, the model
trait, and the inference loop.
Cera is competitive with (and on decode, often faster than) llama.cpp on the LFM2 family. On an M1 Max with Q4_0 weights, the native Metal backend decodes roughly 2× faster than llama.cpp across tested VL and Audio models; prefill leads at short prompts and trails at long ones.
On the cross-platform wgpu backend, decode is 1.78x faster than it was
on the same model and machine (LFM2-VL-450M Q4_0, M1 Max: 63.4 to 112.8 tok/s),
from four changes: removing register spills in the quantized GEMV kernels,
merging the LFM2 conv block into one compute pass, deleting two per-token GPU
round trips that carried almost no work, and running the LM head on the weight as
GGUF stores it instead of a dequantized f16 copy (which also gives back ~79 MB of
VRAM on a 230M model). A fifth change since then, word-loading the Q6_K GEMV
instead of reading it byte-at-a-time, measured +11.6% decode (109.5 to
122.1 tok/s, ABBA-ordered) on LFM2.5-230M-Q4_K_M, a different model from the
1.78x row above, so the two do not multiply. That gain is entirely conditional on
the change before it: measured without #320, the same kernel was flat (110.0 vs
110.5 tok/s), because the only Q6_K matrix in the decode path was one ffn_down
of 42. #320 makes the tied Q6_K embedding the LM head, and that is what the
faster loads then pay off on. The per-kernel breakdown, and what did not work,
are in benchmarks/BASELINE.md.
On CPU, rows dispatch through a persistent, affinity-pinned threadpool with
dynamic chunk-stealing rather than a per-GEMV fork-join. This fixes the
multi-core decode collapse on Android big.LITTLE and scales decode across the
performance cores (Tensor G5, LFM2 Q4_0, warm: CPU decode matches or beats
llama.cpp). Thread placement and count auto-detect per device; the CERA_*
override knobs are documented under
CPU threading & tuning.
Detailed methodology, per-model tables (decode + prefill vs llama.cpp), the
Accelerate/AMX BLAS results, and the backend optimization notes live in
benchmarks/README.md. Numbers there are tagged with
the commit and device they were measured on; some sections are older than
others, and the Android GPU row in particular predates the wgpu work above.
Licensed under either of Apache License, Version 2.0 or MIT License at your option.
A Rust-native LLM inference engine. Load a GGUF model and run it locally, on your laptop's CPU, an Apple GPU, a cross-platform Vulkan/DX12 GPU, a phone, or in the browser, from a single dependency-free core.
[!NOTE] The 0.7.0 release adds the native Qualcomm Hexagon NPU backend, dynamic CPU topology discovery and worker threadpool resizing, and core lifecycle enhancements. See the API guide for usage and compatibility limits, and Releases for published builds.
.gguf file, a
LeapBundles manifest, or a
bundle id; it can auto-download and cache models from Hugging Face.AudioPipeline in pure Rust, linked through a single unified binary across mobile and desktop.Dispatch is on the GGUF general.architecture string. Selected supported
families are listed below; tensor layouts and quantization must also match the loader:
| Architecture | Models | Modalities |
|---|---|---|
lfm2 | Liquid LFM2 / LFM2.5 (the canonical LeapBundles family) | text, vision, audio |
lfm2moe | Liquid LFM2.5-8B-A1B (routed mixture-of-experts) | text |
llama | LLaMA 2 / 3, and classic Mistral 7B (ships as GGUF arch llama) | text |
qwen2, qwen3 | Qwen2 / Qwen2.5 / Qwen3 | text |
qwen35 | Qwen 3.5 / Ornith 1.0 (interleaved Gated Delta Net hybrid) | text |
granite | IBM Granite 3.x, and the dense Granite 4.1 line (3b / 8b / 30b) | text |
minicpm, minicpm5 | MiniCPM / MiniCPM5 (MiniCPM-1B / 2B) | text |
nanbeige | Nanbeige 4.2 (looped-layer dense transformer) | text |
phi3, phi | Phi-3 / Phi-3.5 / Phi-4-mini (fused QKV, packed SwiGLU FFN) | text |
mistral3 | Ministral 3 (Norm YaRN RoPE, attention temperature scaling, Tekken BPE) | text |
bailingmoe3, bailingmoe | Ling 3.0 Tiny (hybrid KDA linear, MLA latent attention, and MoE) | text |
CPU loading also supports families such as Gemma 2/4, Olmo 2/3, Granite Hybrid, Falcon H1 and Mamba-2. See the loading dispatch for architecture aliases and backend admission rules.
Metal and wgpu support lfm2, lfm2moe, llama, qwen2, qwen3, granite,
minicpm, minicpm5, nanbeige, phi3, phi and mistral3 / ministral3
(YaRN is folded into the rope kernel's frequency table; attention temperature
scaling is not implemented on GPU, so those models are limited to the context
below its floor, the original context length, and warn when that caps a longer
request). Qualcomm Hexagon NPU supports lfm2, lfm2moe, the dense families
(llama, qwen2, qwen3, granite, gemma2, olmo2/olmo3, mistral3,
phi3 and others listed in docs/HEXAGON_NPU.md) and qwen35
(HexagonLfmModel); those paths are host-tested and still need device validation.
The listed bailingmoe3 family uses CPU; requesting an unsupported GPU or NPU
backend explicitly returns an error. Auto selection can fall back to CPU.
The LFM2 and supported dense-transformer paths use batched-GEMM prefill,
including on CPU, with tiled flash attention for long prompts where supported.
lfm2moe's routed feed-forward block is the one exception to that
batched-GEMM sentence, and it is deliberate: each token picks its own 4-of-32
experts, so an expert's weights are re-read once per routed token rather than
once per prompt. (The GPU backends still issue one dispatch per projection for
the whole chunk, not one per token; what they do not do is share a weight row
between two tokens that chose the same expert.) Grouping tokens by expert is a
follow-up, kept separate from getting the routing arithmetic right. On the two GPU backends its expert kernels
are also Q4_0-only, and its routed-FFN LoRA targets (the router and the
per-expert projections) apply on CPU only: an adapter carrying them is refused
rather than half-applied.
CeraEngine auto-attaches the
vision encoder; Session::append_image runs image → ViT → projector → prefill.
The ViT encoder runs on the GPU (Metal or wgpu) with a CPU fallback. Verified
against LFM2.5-VL-450M.lfm2-audio-v1): feed PCM audio in, and
(with a vocoder) decode audio out.cera::hotword), Silero VAD v5 (cera::vad), and OpenAI Whisper speech-to-text (cera::model::whisper).Cera dispatches to the fastest available backend at runtime (--device auto),
or you can pin one:
| Backend | --device | Platforms | Notes |
|---|---|---|---|
| CPU | cpu | everywhere | Scalar reference + NEON (aarch64) / AVX2 (x86_64) kernels; optional Accelerate/OpenBLAS via the blas feature; dynamic cpuset awareness |
| Native Metal | metal | macOS, iOS | Hand-written MSL shaders, single-encoder dispatch, GPU argmax |
| Qualcomm Hexagon NPU | hexagon | Android, Linux (aarch64) | FastRPC Unsigned Process Domain, shared DMA memory, repacked 32x32 Q4_0/Q8_0 matrix kernels on Snapdragon 8 Gen 2 / 8 Gen 3 / 8 Elite |
| wgpu | gpu | macOS, Linux, Windows, browser | WGSL shaders over Metal / Vulkan / DX12 / WebGPU |
--device auto prefers native Metal on macOS and iOS, Qualcomm Hexagon NPU on supported Snapdragon devices, and wgpu where a GPU is available, falling back to CPU otherwise. A routed-expert model too large for the DSP's mappable memory (LFM2.5-8B-A1B) skips the NPU under auto, since paging it is slower than the CPU; set CERA_HEXAGON_PAGE_EXPERTS=1, or ask for --device hexagon, to page it.
Cera provides a native backend for Qualcomm Hexagon NPUs and Compute DSPs on Snapdragon 8 Gen 2 (v73), Snapdragon 8 Gen 3 (v75), Snapdragon 8 Elite (v79), and next-generation Snapdragon architectures (v81/v85). See the Hexagon NPU Guide for full architecture and Android packaging details.
/dev/fastrpc-cdsp), enabling third-party Play Store applications to execute on HTP/HVX hardware without root privileges or vendor signing keys.rpcmem (DMA-BUF / ION) mapped into both CPU and DSP address spaces, eliminating bus copying.libcera-htp-v{73,75,79,81,85}.so) into the host binary and extracts them automatically at startup. Skeletons incorporate extended operators (Conv1D, ConvTranspose1D, Snake, UnaryStep, Sum).CERA_HEXAGON_BATCH_TENSORS=<n> caps each batch at n tensors for bisection), ping-pong scratch memory isolation, FastRPC latency QoS (FASTRPC_CONTROL_LATENCY = 100 µs), FastRPC driver wakelock (FASTRPC_CONTROL_WAKELOCK), and Q8_0 quantized KV cache (~47% memory reduction over F16).HtpOpCode::Argmax, opcode 62), reading only 4 bytes of token ID from rpcmem rather than copying or invalidating 128 to 256 KB of float32 logits, eliminating host CPU allocations and memory traffic during greedy decode.forward_prefill_logits_all). When paired with prompt-lookup drafting (ngram=2, k=4), speculative decoding lifts 350M decode throughput from 164.0 tok/s to 259.5 tok/s (peak 260.2 tok/s) and 2.6B decode from 26.7 tok/s to 47.7 tok/s (peak 48.5 tok/s) on Snapdragon 8 Elite.HexagonDepthformer executes all 8 autoregressive passes (48 transformer layers per audio frame) entirely on the NPU using HTP GEMV, RMSNorm, RoPE, and FlashAttnExt, alongside on-DSP audio detokenization (LayerNorm, linear GEMM, SwiGLU, Conv1D).Weights run in Q4_0, Q4_1, Q8_0, Q4_K, Q5_K, and Q6_K, plus dense F32. Activations are dynamically quantized to Q8_0 for fast integer GEMV on CPU.
Those are GGML tensor types, and dispatch is per tensor, so what decides
whether a file runs is the mix inside it, not its Q4_K_M-style label. K-quant
downloads usually work, but the label is not a guarantee: llama.cpp substitutes
Q5_0 / Q5_1 / IQ4_NL for tensors whose rows are not a multiple of 256, and cera
has no kernel for those. cera inspect --model <file> lists the per-tensor types,
and an unsupported one fails at weight resolution naming the tensor rather than
failing anonymously, with the type name too, for the types cera knows about (an
IQ type reports its numeric id).
Backends are not uniform, so a file that runs on one may not run on another. On
native Metal, Q4_1 works as a projection weight but not as token_embd /
output; --device auto falls back to wgpu or CPU on its own, while
--device metal reports the gap. F16/BF16
tensors parse, and are dequantized on the LoRA, vision, and audio paths, but the
transformer weight and token-embedding paths have no kernel for them; an
F16-weight LLM is not a supported configuration.
One Rust core, consumed from many places:
| Target | Crate / package | Consumers |
|---|---|---|
| Rust (Engine) | cera | any Rust project (cargo add cera) |
| Rust (API client) | cera-client | any Rust project (cargo add cera-client); OpenAI and OpenRouter endpoints |
| CLI | cera-cli | the cera binary |
| Kotlin / Swift / Python | cera-ffi (UniFFI) | JVM, Apple platforms (LLMs, ChatSession, VAD, KWS, Whisper) |
| Android | cera-ffi-kotlin | Android apps (AAR; includes Qualcomm Hexagon NPU support) |
| iOS / macOS | Package.swift (SwiftPM XCFramework) | Apple apps (.package(url:)), Metal GPU (Auto: Metal → CPU) |
| Flutter | cera_ffi_flutter | cross-platform apps; ships the native library per platform (LLMs, ChatSession, VAD, KWS, Whisper, Hexagon NPU) |
| Dart (no Flutter) | cera_ffi | CLI / server; bring your own cera-ffi cdylib (ChatSession, explicit loading) |
| Browser / Node | cera-wasm (@hyeons-lab/cera-wasm) | WebAssembly + WebGPU |
A complete SwiftUI example app (streaming chat + embeddings + LoRA) that consumes
the published Swift Package lives in examples/CeraChat;
it doubles as a real-device Metal validation harness.
Force the model's output to match a grammar, useful for JSON, tool calls, or any schema. Cera ships a byte-level GBNF engine (mirroring llama.cpp's) that masks the sampler each step so only grammar-valid tokens can be produced.
# Constrain generation to JSON (a token limit can still truncate it)
cera run -m model.gguf -p "List 3 colors as JSON" --json
# Any custom GBNF (inline or @file)
cera run -m model.gguf -p "..." --grammar @schema.gbnf
Supports literals, character classes, alternation, grouping, and repetition
(* + ? and bounded {n,m}). Available from every binding, not just the
CLI: in Rust set GenerateOpts.grammar to a compiled Grammar (Grammar::parse(gbnf)?),
while the Kotlin/Swift FFI (GenerateOpts.grammar) and browser/Node WASM
(GenerateOpts.setGrammar(gbnf)) take the GBNF string directly and compile it
natively, so mobile and web apps use the same grammar rules. A token limit or
cancellation can leave an incomplete value; parse and validate the final output.
Cera compiles a subset of JSON Schema directly to GBNF without external tooling:
let grammar = Grammar::from_json_schema_str(&schema_json)?; or opts = opts.with_json_schema(&schema_json)?;let constrainedOpts = try opts.withJsonSchema(schema)val constrainedOpts = opts.withJsonSchema(schema)constrained_opts = opts.with_json_schema(schema)chat.streamJson(opts, schemaJson)opts.setJsonSchema(schema) or const constrainedOpts = GenerateOpts.withJsonSchema(opts, schema)Pass the returned constrained options to generation. The Swift/Kotlin/Python
helpers and WASM withJsonSchema return copies; WASM setJsonSchema updates
opts in place.
Supports nested properties and required keys, bounded arrays, scalar enums,
local $defs/definitions and chained $ref resolution. allOf accepts only
supported scalar wrappers and compatible object merges. Numeric ranges, string
patterns and other unimplemented keywords are not enforced. See the
supported subset before relying on a schema.
Give the model a set of tools (OpenAI "function" schemas) and get the calls it
makes back out as structured data. Cera renders the tools into the model's chat
template and parses tool calls from the reply, format-aware, since the wire
format isn't interchangeable: LFM2/LFM2.5 emit Pythonic calls
([get_weather(city="Paris")]) while Hermes-style Qwen2.5/Qwen3 emit JSON
(<tool_call>{"name": …, "arguments": {…}}</tool_call>). The format is detected
from the GGUF architecture.
# Let the model decide whether/how to call a tool (free text or a call)
cera run -m model.gguf -p "What's the weather in Paris?" \
--tools '[{"name":"get_weather","description":"Get weather for a city",
"parameters":{"type":"object",
"properties":{"city":{"type":"string"}},"required":["city"]}}]'
# Constrain tool-call syntax after the model emits a call marker
cera run -m model.gguf -p "..." --tools @tools.json --constrain-tools
With --constrain-tools a lazy grammar trigger keeps generation free until
the model starts a tool call, then constrains function/argument names and outer
value syntax. Its separate tool grammar does not enforce required arguments,
uniqueness or nested item/property schemas. Prose and truncated calls remain
possible; parse and validate arguments before executing a tool. In
--tools mode stdout is machine-readable: only the JSON array of calls
([] when the model answered in prose); the assistant reply and timing stream to
stderr, so … --tools tools.json | jq just works.
Available from every binding, not just the CLI: Rust (cera::tools),
Kotlin/Swift (applyChatTemplateWithTools, parseToolCalls, toolGrammar,
detectToolFormat), and browser/Node WASM (the same names); see each crate's
README for the API surface.
Cera promotes the high-level SessionChat coordinator (cera::session::chat / ChatSession in FFI) to the public API:
Idle to PromptReady; generation reaches TurnComplete only on a profile terminal. Token limits, cancellation and nonterminal stops can leave Interrupted; zero-token/no-progress calls can preserve PromptReady for retry. Recovery may restore, reset or mark execution unusable.TemplateFamily, ProfileBuilder); unsupported continuation profiles are rejected.chat.set_tools(tools); tool calls are parsed into TurnResult.tool_calls and tool execution outputs are ingested with chat.ingest_tool_response(...).use cera::{Message, SessionPhase};
let mut chat = session.into_chat().map_err(|(_, err)| err)?;
chat.ingest(&Message::user("Hello!"))?;
let result = chat.complete(&opts)?;
println!("Assistant: {}", result.text);
// Continue only after the profile's terminal marker ended the turn.
if chat.phase() == SessionPhase::TurnComplete {
chat.ingest(&Message::user("What did I just say?"))?;
let reply = chat.complete(&opts)?;
println!("Assistant: {}", reply.text);
} else {
// Start a fresh conversation; alternatively replace the full history.
chat.reset()?;
}
Cera provides native reactive streaming wrappers tailored for each language ecosystem:
AsyncThrowingStream<String, Error> via chat.stream(opts: opts) and chat.streamJson(opts: opts, schemaJson: schema).Flow<String> via chat.stream(opts) and chat.streamJson(opts, schema).Iterator[str] via chat.stream(opts) and chat.stream_json(opts, schema).Stream<String> via chat.stream(opts) and chat.streamJson(opts, schema).chat.stream_text(&opts, |chunk| { ... })? yielding real-time text fragments into a callback closure.Stream completion does not imply a completed chat turn. Inspect the phase before the next user message; reset or replace interrupted conversations. Abandoning an active stream requests cancellation. Let that operation finish before reusing the handle. See streaming and cancellation.
CPU Session and Chat support resumable snapshots. Native Metal/Hexagon/wgpu Session
checkpoint and restore operations reject backend-owned state; browser
WebGpuSession has a separate device snapshot API.
SessionCheckpoint / ChatCheckpoint formats with magic headers (CERASCHK / CERACHAT), structural fingerprints and versioned headers. Fingerprints are not cryptographic model identities.checkpoint.save_to_file(path) and SessionCheckpoint::load_from_file(path) with atomic writes and clean error recovery.Version 0.6.2 and newer validates row geometry, KV precision and compression identity, including TurboQuant seeds. Recreate older f16/TurboQuant snapshots whose fingerprints lack compression identity; plain f32 fingerprints are unchanged. See checkpoint compatibility.
Runtime LoRA. Load a LoRA adapter, a llama.cpp GGUF (from
convert_lora_to_gguf) or a PEFT .safetensors, and attach it to a session. The
delta is applied at inference time (y += scale·B·(A·x)), never merged into the
weights, so the base model stays quantized and adapters hot-swap / unload per
request at ~no cost. Runs on CPU, Metal, and wgpu (batched-GEMM prefill +
decode) and is dimension-checked against the model at attach. The one gap is
lfm2moe's routed-FFN targets, which apply on CPU only; on a GPU backend such an
adapter is refused with CeraError::LoraUnsupportedByBackend rather than
partially applied.
Hidden-states extraction. Pull the per-token last-layer hidden state
(post-final-RMSNorm, the llama.cpp --pooling none vector) straight out of the
engine, reflecting the active adapter. This unblocks classifier / extractor /
embedding heads, e.g. a section router: LFM2.5 + a route_section LoRA + a
small linear head over the mean-pooled hidden state.
Both are available from every binding:
// Swift / Kotlin (UniFFI): same shape on both
let adapters = try LoraAdapters.fromSafetensors(path: p, alpha: nil)
try session.attachLora(adapters: adapters) // hot-swap-able
let pooled = try session.hiddenStatesMeanPooled(tokens: toks) // [hidden_size]
// Browser / Node (WASM)
const adapters = LoraAdapters.fromSafetensorsBytes(bytes, undefined);
session.attachLora(adapters);
const pooled = session.hiddenStatesMeanPooled(tokens); // Float32Array
Cera includes the first implementation of TurboQuant (arXiv:2504.19874, Google Research 2025: TurboQuant: Extreme KV Cache Compression) for LFM2, compressing the KV cache to ~3 bits/key + ~2 bits/value (~12× vs f32) with near-lossless quality and no calibration. On a 1.6B LFM2 model at 4K tokens that's ~192 MB → ~16 MB of KV, with decode staying within ±5% of f32.
The default KV cache is f16 on the CPU backend (llama.cpp's default cache type; --kv-cache-keys auto),
which halves the bytes a decode streams at depth; --kv-cache-keys f32 keeps full precision. Backends that
keep their own cache (wgpu, Metal, Hexagon) are unaffected.
Enable TurboQuant on the CLI:
cera run -m lfm2.gguf -p "Hello" --kv-cache-keys tq3 --device cpu
cera run -m lfm2.gguf -p "Hello" --kv-cache-keys tq3 --device gpu # wgpu
cera run -m lfm2.gguf -p "Hello" --kv-cache-keys tq3 --device metal # native Metal
Supported on all three backends (CPU, wgpu, and native Metal) for both
decode and chunked prefill. On the GPU backends the compressed cache lives in GPU
buffers and the prefix-cache snapshots are byte-compatible with the CPU's, so all
three write the same TQK1/TQV1 blob format. Two GPU-specific restrictions:
head_dim must be a power of two ≤ 128 and a multiple of 32 (every supported
model is 64 or 128), and only the both-sides mode (tq3) is available; the
single-sided debug modes tq3-keys / tq3-values fall back to the backend's
uncompressed KV there (f32 on wgpu, f16 on Metal) with a warning. --n-keep
context shift is not supported with any TurboQuant mode on any backend.
See cera/src/turboquant.rs for the algorithm (PolarQuant + QJL).
cera::kv_cache::KvPrefixCache retains token-prefix snapshots in warm memory and,
when disk caching is enabled and configured, a cold disk tier. Its insert_anchor
and find_deepest_semantic_anchor APIs let callers tag and select matching
prefix snapshots using caller-supplied boundary metadata. The standard engine
cache path does not automatically select semantic anchors.
FlatBuffers v2 persistence preserves each snapshot's existing precision, including TurboQuant data when already compressed. Callers choose compression before capturing snapshots; moving a snapshot to disk does not recompress it.
Cera includes support for DSpark (arXiv:2407.08608, 2024: DSpark: High-Throughput Speculative Decoding with Lightweight Drafters), accelerating inference throughput via parallel multi-token draft verification:
cera::model::dspark) that share embedding and GGUF weight backings with the target model without redundant memory mapping.Metal, Hexagon, and wgpu permit one live Session per loaded model. A second session
returns Busy until the first is dropped; reset and cancellation keep ownership.
CPU models continue to support shared weights across concurrent sessions. Load
separate GPU or NPU models for simultaneous conversations. The
session ownership walkthrough shows the public API, cleanup behavior
and executable checks.
LFM2-Audio transcription during a live GPU conversation uses a cached secondary model built from retained weights. This preserves conversation KV and costs additional model memory and first-use setup; the walkthrough includes a native Dart consumer for transcription and seeded generation.
The standalone FfiWhisperModel loads a Whisper GGUF from a path or owned bytes
and transcribes 16 kHz mono float PCM through Swift and Kotlin. Use
transcribeAsync for a background decode; whisperDefaultTranscribeOpts supplies
language, translation, timestamps, token limit and temperature defaults.
The Swift/Kotlin recording examples
are compiled and executed against the generated bindings. Build matching wrappers
and native libraries from the same revision.
# Exercise loading, nonempty decoding, async calls and ownership without downloads.
cargo test -p cera-ffi --test whisper_ffi --locked --offline
Each call handles at most 30 seconds; segment longer recordings first. This is
separate from LFM2-Audio's CeraEngine.transcribe. Dropping its Rust future
cooperatively cancels decoding. Kotlin propagates coroutine cancellation; the
pinned Swift wrapper does not propagate Task.cancel().
Cera includes a native, pure-Rust implementation of Silero VAD v5 (cera::vad), enabling zero-dependency, real-time voice activity detection across desktop, mobile, and web:
VadIterator) processing 512-sample frames (32ms at 16kHz) with state persistence.cera::vad::SileroVad), CLI (cera vad), Swift/Kotlin via UniFFI, and Dart/Flutter via cera_ffi.# Detect speech timestamps from a WAV file
cera vad --model models/silero_vad.gguf --audio speech.wav --json
# Convert official Silero VAD v5 ONNX model to GGUF
python scripts/convert_silero_vad.py silero_vad.onnx models/silero_vad.gguf
Cera includes a native, pure-Rust Keyword Spotting (KWS) engine (cera::hotword) designed for always-on, low-power wake word detection on mobile and edge devices:
kws.*.LogMelFrontEnd and HotwordDetector retain scratch buffers for acoustic features and inference. Event strings, growing stream buffers and foreign argument/result marshaling can still allocate; process chunks serially on a background audio worker.HotwordIterator): Interleaves Silero VAD gating (512 samples at 16 kHz or 256 at 8 kHz) with configurable hop KWS checks. It skips acoustic evaluation when the buffered window has no recent speech; other model sample rates disable VAD gating.cera::hotword), Kotlin/Android, Swift/iOS, Python via cera-ffi, and Flutter via cera_ffi_flutter.# Export and fold PyTorch wake word weights into a self-describing GGUF container
python scripts/convert_kws.py --checkpoint model.pt --output models/hey_liquid.gguf
# Train a custom wake word model with automated phonetic near-miss negative synthesis
python tools/wake_word/train_kws.py --phrase "Hey Liquid" --epochs 25 --output models/hey_liquid.gguf
Cera provides a pure-Rust implementation of OpenAI Whisper ASR (cera::model::whisper), exposed across all foreign language bindings via UniFFI (cera-ffi):
libcera_ffi.so or CeraFFI.xcframework) for wake word detection, speech activity gating, and full speech-to-text transcription without external C++ or JNI dependencies.transcribe_async Rust future signals a running decoder to stop at its next cancellation check. Kotlin propagates coroutine cancellation; the pinned Swift wrapper does not propagate Task.cancel(). See the Whisper examples and limitations.cera::model::whisper), Swift, Kotlin, Python, and Dart/Flutter.Cera provides a unified stateful streaming audio pipeline (cera::audio_pipeline::AudioPipeline and cera-ffi FfiAudioPipeline) connecting Silero VAD v5, Keyword Spotting, and Whisper ASR:
ListeningForHotword, ListeningForSpeech, SpeechActive, and Transcribing states with independent audio buffer ownership.max_utterance_ms without resetting Silero VAD recurrent hidden states across continuation segments.AudioPipelineBuilder::with_diarizer, or FfiAudioPipeline.fromFilesWithDiarizer), or the Nemotron-3-Diarization add-on (with_diarizer_nemotron3 / fromFilesWithDiarizerNemotron3, 8 speakers at 10 ms frames), and every transcribed utterance is followed by an UtteranceLabeled event with its speaker slot (0 to 3, or 0 to 7), once the diarizer has covered it. The diarizer hears the same samples as the pipeline, so both share one clock, and either diarizer can run on the Hexagon NPU (prefer_npu, with a GGUF converted using --tail-outtype q8_0; see scripts/sortformer/README.md and scripts/nemotron3_diarization/README.md). Compare the two side by side with cera diarize --compare.AudioPipeline), Swift, Kotlin, Python, and Dart/Flutter (FfiAudioPipeline).Cera supports direct loading and streaming execution from Hugging Face model repositories:
--hf org/repo).--quant-strategy fast-mse, hqq, quarot).$HOME/.cache/cera.# Run directly from a Hugging Face GGUF repository
cera run --hf LiquidAI/LFM2.5-1.2B-Instruct-GGUF --quant Q4_0 --prompt "Explain quantum computing."
# Quantize and run on-the-fly from a remote SafeTensors repository
cera run --hf LiquidAI/LFM2.5-350M --quant Q4_0 --quant-strategy fast-mse --prompt "Hello"
# Install the CLI (CPU-only build)
cargo install cera-cli --locked
# ...or with a GPU backend
cargo install cera-cli --locked --features metal # or: --features gpu
# ...or build from source
just release # optimized LTO build → target/release/cera
# Generate from a local GGUF
cera run --model model.gguf --prompt "Explain quantization in one sentence."
# Auto-download a bundle from Hugging Face and generate
cera run --bundle-id LFM2.5-1.2B-Instruct --quant Q4_0 --prompt "Hello"
# Interactive multi-turn chat (keeps the prefix cache warm across turns)
cera chat --bundle-id LFM2.5-1.2B-Instruct --quant Q4_0
# Or with no model source at all: pick one from the published catalog
# interactively (needs a terminal, and not --no-tui)
cera chat
# Pick an accelerator backend
cera run -m model.gguf -p "Hi" --device metal # or: gpu, npu, hexagon, cpu, auto
Using the library directly:
use cera::{CeraEngine, EngineConfig, GenerateOpts, Message, SessionConfig};
let engine = CeraEngine::from_path("model.gguf", EngineConfig::default())?;
let mut session = engine.new_session(SessionConfig::default())?;
// Raw prompt completion:
session.append_text("Once upon a time")?;
let opts = GenerateOpts { max_tokens: 128, ..Default::default() };
let summary = session.generate(&opts, &mut sink)?; // sink: your ModalitySink
// Or conversational chat coordination (delta-only prefill, live KV retention):
// Start a new conversation after the raw completion above.
session.reset()?;
let mut chat = session.into_chat().map_err(|(_, err)| err)?;
chat.ingest(&Message::user("What is the capital of France?"))?;
let reply = chat.complete(&opts)?;
println!("Assistant: {}", reply.text);
See the cera crate README for the full library API.
This checkout also exposes explicit loading alongside the existing constructors:
use cera::{ModelLoader, ModelSource, SessionConfig};
let model = ModelLoader::new(ModelSource::path("model.gguf")).build_generative()?;
let mut session = model.create_session(SessionConfig::default())?;
drop(model); // The session retains its loaded resources and live KV state.
session.append_text("Once upon a time")?;
Run the complete text-completion example:
cargo run -p cera --example explicit_loading -- model.gguf "Once upon a time".
For multi-turn chat, run the conversational chat example:
cargo run -p cera --example chat -- model.gguf.
The API refactor also provides runnable multi-language examples,
including conversational chat (Rust, Swift,
Kotlin, Python, Dart),
session continuation, and vision/draft loading.
The audio walkthrough demonstrates PCM input and output with locally generated test weights.
The remote companion examples run HF discovery, downloads, cache repair and retained vision/audio/draft execution against a local HTTP fixture.
The HF revision examples load changed GGUF revisions into separate cache entries, verify defaults and keep a CPU session live across another revision load. They also exercise failed metadata resolution and explicit commit checks.
The SafeTensors conversion examples exercise real conversion, corruption repair and option-sensitive cached reloads. The examples verify checkpoint bytes, pin inputs to one upstream commit, and refresh changed revisions. Retained models execute after replacement, and sessions continue after their parent model is released.
The persistent cache examples demonstrate same-path weight replacement, unchanged-weight disk reuse and retained live sessions.
| Command | Purpose |
|---|---|
run | One-shot inference: text, optional grammar/JSON or tool calling (--tools), plus image/audio input for VL/Audio bundles |
chat | Interactive multi-turn REPL with a persistent KV prefix cache. Given no model source at all on a terminal (and without --no-tui), opens a picker over the published catalog |
embed | Extract last-layer hidden-state embeddings: mean-pooled, or --per-token for the full matrix |
logits | Dump next-token logits over the vocabulary (single prefill), handy for cross-backend parity checks |
convert | Convert a local SafeTensors model directory to a quantized GGUF file (including d1_omni decision models) |
decide | Answer structured questions over text, image, or audio state using a bidirectional d1 decision model |
inspect | Dump a GGUF's metadata, tensor shapes, and resolved backend tier |
cpu | Print the host's CPU backend tier + detected SIMD features (no model needed) |
thermal | Print current and forecast device thermal headroom on Android API 30+ (unavailable elsewhere) |
tokenize | Encode text to token IDs (e.g. to compare against Hugging Face) |
bench | Measure decode/prefill throughput with p10/p50/p90/mean/stddev |
gemv-bench | Microbenchmark wgpu quantized GEMV kernels (--features gpu) across m,k shapes and verify CPU parity |
gemm-bench | Microbenchmark wgpu quantized GEMM kernels (--features gpu) across m,n,k shapes and verify CPU parity |
list-bundles | List bundles available on LiquidAI/LeapBundles |
list-hf | Discover and list GGUF model files in a Hugging Face repository |
download-bundles | Prefetch bundle manifests + model files without loading |
vad | Voice activity detection on audio files using Silero VAD v5 |
diarize | Speaker diarization on audio files using Streaming Sortformer or Nemotron-3-Diarization (plus --compare) |
transcribe | Speech-to-text transcription on audio files using OpenAI Whisper or LFM2-Audio |
compare-quants | Audit metadata, tensor inventory, quantization fidelity, and logit parity between Cera-converted models and reference community GGUFs |
Defaults are chosen to be correct-or-harmless across a wide range of hosts, not optimal on any one. The knobs below override them.
| Variable | Effect |
|---|---|
CERA_DECODE_THREADS=<n> or CERA_DECODE_THREADS=auto | Worker count for decode (per-token GEMV). A fixed <n> overrides the automatic sizing below (clamped to the detected count); auto selects it. |
CERA_DECODE_SIZING=off | Disable model-aware decode sizing (0 / false / off, case-insensitive) and fall back to the flat cap (detected perf cores, capped at 12). Use this to A/B the sizing on a new machine. |
CERA_DECODE_NARROW=<n> / CERA_DECODE_WIDE=<n> | Override the two widths the sizing picks between. Defaults derive from the physical core count: physical/2 capped at 12, and physical + physical/4 capped at 24; the narrow arm never exceeds the wide one. Setting either also forces sizing on where it would otherwise be declined; on a host whose physical core count is undetectable, both must be pinned. |
CERA_DECODE_BPD_KB=<n> | Bytes-per-dispatch threshold (decimal KB) separating the narrow arm from the wide one. Default 2500. Unlike the two above, this does not force sizing on where it is declined; it moves the threshold, it does not pin a width. |
CERA_THREADS=<n> | Overrides the detected performance-core count, which the decode/prefill pools are sized from. Clamped to the number of pinnable cores on hosts that have any, with a warning: past that the surplus workers run unpinned and contend with pinned ones that are spin-waiting (measured 35x slower on a Tensor G4). Not clamped where nothing gets pinned anyway, i.e. hosts with no affinity or CERA_PIN=0. |
CERA_PREFILL_THREADS=<n> | Prefill pool width on its own, without moving decode. May reach past the performance cores up to every pinnable core, for sweeping a part where widening might pay; it does not on the parts measured so far. Subject to the same clamp rule as CERA_THREADS, and to CERA_THREADS itself, which lowers the ceiling it is clamped against. |
CERA_PIN=0 | Disables worker affinity pinning, for host apps that manage thread placement themselves. Also lifts the clamps above, since with nothing pinned there is no oversubscription cliff to guard against. |
CERA_POOL_STATS=1 | Annotates each cera bench run with pool fan-out health: how many dispatches wanted more than one worker, and how many of those silently ran serially because the pool was busy. The counts are exact; the work percentage mixes units across dispatch kinds, so read the counts. |
CERA_CPU_TIER=<tier> | Caps the SIMD tier (e.g. avx2, avx512). May only downgrade, useful for A/B-ing a kernel path. |
CERA_ATTN_PREFETCH=<n> | Positions ahead to prefetch KV rows in CPU f16 decode attention on aarch64 (default 16; 0, off, false disables; clamped to 1024). |
CERA_ATTN_PREFETCH_L2=<n> | Positions ahead for L2 prefetch in CPU f16 decode attention on aarch64 (default 64; 0, off, false disables; clamped to 1024). |
CERA_Q6K_PREFETCH=<n> | Bytes ahead to software-prefetch 4 cache lines per block in aarch64 Q6_K GEMV on tensors >= 32 MB (default 16384; 0, off, false falls back to the 1-line 512 B prefetch; clamped to 1048576). |
CERA_LM_HEAD_NO_GEMM=1 | Puts the LM-head projection in forward_prefill_logits_all back on the per-row loop the batched GEMM replaced, so both halves of a speculative-decoding A/B run from the same binary. Measurement lever only: both paths compute the same projection, to within f32 accumulation order. |
CERA_Q4_DEC4=off | Disables the 4-row interleaved Q4_0 decode repack (0 / false / off), which costs one extra copy of each repacked weight (about +155 MB resident on the 450M, measured on a Galaxy S25 Ultra and not seen on an Apple-silicon Mac) and decodes bit-identically to the repacked prefill GEMMs. A memory/A-B lever. |
CERA_CPU_SMMLA_TILED=off | Disables 4-token tile activation interleaving for aarch64 Q4_0 smmla GEMMs (0 / false / off), restoring the columnar kernels. An A/B performance lever. |
CERA_CPU_ATTN_TILED=off | Disables the register-tiled NEON prefill attention (0 / false / off), restoring the whole-head blocked kernel. An A/B performance lever. |
CERA_VIT_INT8=off | Runs the CPU vision tower's linears on the f32 path instead of the int8 GEMM (roughly 2 to 3x slower, measured on an Apple-silicon Mac; 0 / false / off). |
CERA_VIT_PROFILE=1 | Prints per-phase wall time of each vision-tower encode (CPU and GPU). Off by default and free when off. |
CERA_GPU_PREBUILD=off | wgpu decode: stops finishing the next token's command buffer while the current one executes (0 / false / off). A/B lever; decode is about 20% slower with it off on Adreno 830. |
CERA_GPU_ATTN_SPLIT=off | wgpu decode: uses the single-kernel attention instead of the split-K attention (head_dim 64 only; 0 / false / off). |
CERA_WGPU_STREAM_MIN_N=<n> | wgpu prefill: smallest batch size routed to the streaming Q4_0 GEMM (default 1, i.e. every batch). |
CERA_WGPU_GEMM_DIRECT_B=off | wgpu prefill: disables direct-B staging and restores transposed f16 activation pass (0 / false / off). |
CERA_WGPU_GEMM_R1=1 | wgpu prefill: forces the 1-row streaming Q4_0 GEMM kernel everywhere (skips 2-row kernel). |
CERA_WGPU_FUSE_GATE_UP=off | wgpu prefill: disables fused gate/up streaming Q4_0 GEMM with SiLU epilogue (0 / false / off). |
CERA_WGPU_ATTN_F32=1 | wgpu prefill: forces the f32 attention kernel everywhere (skips 64-query fp16 attention kernel). |
CERA_VIT_STREAM=off / CERA_VIT_WARMUP=off | wgpu vision tower: CERA_VIT_STREAM=off restores the generic kernels instead of the fp16 streaming GEMMs and attention; CERA_VIT_WARMUP=off skips the blank-image warm-up encode at load (about 190 ms once). |
RUST_LOG=<filter> | Log level. Defaults to warn, which surfaces things like prefill falling back to the slow per-token path. |
Decode width is sized from the loaded model, because there is no single best value, and, less obviously, model size does not predict it. Measured on a Ryzen AI MAX+ 395 (16 physical / 32 logical), decode tok/s change going from 12 to 20 workers:
| Model | Weights | Bytes/dispatch | 12 → 20 |
|---|---|---|---|
| TinyStories-20M Q8_0 | 21 MB | 0.84 MB | −47% |
| SmolLM-135M Q4_0 | 92 MB | 0.51 MB | −11% |
| LFM2.5-350M Q8_0 | 379 MB | 3.83 MB | +29% |
| SmolLM-360M Q8_0 | 386 MB | 1.50 MB | −7% |
| Llama-3.2-1B Q8_0 | 1321 MB | 10.24 MB | +38% |
Note the middle two: near-identical weight bytes, opposite answers. What separates them is how many pool dispatches a token is split across; LFM2 issues 99 per token against SmolLM-360M's 257, so each dispatch amortizes 2.6× more work against the same fixed barrier cost. Below ~2.5 MB per dispatch decode is barrier-bound and wants fewer workers; above it decode is bandwidth-bound and wants more.
cera computes that ratio from the GGUF at load and picks a narrow or wide width
accordingly. A/B against the old flat cap of 12 (same binary, interleaved,
CERA_DECODE_SIZING=off vs on, the five models above at two prompt depths)
gave +19% mean over those 10 cells with no cell slower (small models gain
too: they get fewer than 12 workers). Scored differently, against each model's
own best measured width across the full 12-model set, the rule reaches 98.1% of
peak versus 90.1% for the flat cap; one combo near the threshold
(LFM2.5-350M Q4_0) lands on the narrow arm when its measurement mildly preferred
the wide one, which is the cost that average already includes.
The pool itself is a process-wide singleton, built once on the first decode dispatch, so the width comes from the first model loaded into a process and stays there for any loaded after it; it does not re-size per load. (The thread count was already inherited that way before this sizing existed.) Embedders running several models in one process should pick the knobs below for whichever model's decode matters, or load that one first.
This is calibrated on one x86 host. The sizing steps aside, leaving the previous flat-cap behaviour, in two cases. On heterogeneous parts, because decode there measured best across all big cores (that evidence is from ARM big.LITTLE; x86 hybrid parts decline by the same argument, without the direct measurement). And on hosts whose physical core count cannot be detected (Windows, BSD, Intel macOS), where deriving a width from the logical count would overshoot badly. Pinning a width forces it on anyway, for sweeping a machine it has not been tuned against: one arm is enough on a heterogeneous host, both are required where the physical core count is undetectable.
Apple Silicon is sized, not declined. M-series parts are heterogeneous in hardware, but cera detects only their P-cores, so neither decline case applies: an M4 Max gets 12 workers for large models (its full P-core count; there is no SMT for the wide arm to spend) and 6 for small ones. That narrow arm is the part of this that has not been measured off x86.
If you run one model repeatedly and care about decode latency, it is still
worth measuring: cera bench --model <path> with CERA_DECODE_THREADS set
sweeps a fixed width, and CERA_DECODE_SIZING=off gives you the old default to
compare against. Interleave the arms rather than sweeping; laptop clocks drift
enough to invent a trend that isn't there.
run, chat,
and bench (--spec and --no-spec).ModalitySink as they decode; Session::cancel() interrupts long prompts
responsively via chunked prefill.n_keep prefix pinning, on CPU
and GPU, keeps generation going past the context window.Cera is a Cargo workspace. The core library does GGUF parsing, quantization, the compute backends, the models, and the tokenizer; everything else is a thin adapter over it.
cera: core librarycera-cli: CLI binary (clap)cera-ffi: UniFFI bindings (Kotlin / Swift / Python)cera-ffi-kotlin: Android packaging (AAR)cera_ffi · cera_ffi_flutter: Dart bindings, and the Flutter plugin that ships a native library for themcera-wasm: wasm-bindgen browser / Node bindingscera-parity: cross-binding parity harness (runs one prompt through every binding and reports drift)See the cera crate README for the module layout, the model
trait, and the inference loop.
Cera is competitive with (and on decode, often faster than) llama.cpp on the LFM2 family. On an M1 Max with Q4_0 weights, the native Metal backend decodes roughly 2× faster than llama.cpp across tested VL and Audio models; prefill leads at short prompts and trails at long ones.
On the cross-platform wgpu backend, decode is 1.78x faster than it was
on the same model and machine (LFM2-VL-450M Q4_0, M1 Max: 63.4 to 112.8 tok/s),
from four changes: removing register spills in the quantized GEMV kernels,
merging the LFM2 conv block into one compute pass, deleting two per-token GPU
round trips that carried almost no work, and running the LM head on the weight as
GGUF stores it instead of a dequantized f16 copy (which also gives back ~79 MB of
VRAM on a 230M model). A fifth change since then, word-loading the Q6_K GEMV
instead of reading it byte-at-a-time, measured +11.6% decode (109.5 to
122.1 tok/s, ABBA-ordered) on LFM2.5-230M-Q4_K_M, a different model from the
1.78x row above, so the two do not multiply. That gain is entirely conditional on
the change before it: measured without #320, the same kernel was flat (110.0 vs
110.5 tok/s), because the only Q6_K matrix in the decode path was one ffn_down
of 42. #320 makes the tied Q6_K embedding the LM head, and that is what the
faster loads then pay off on. The per-kernel breakdown, and what did not work,
are in benchmarks/BASELINE.md.
On CPU, rows dispatch through a persistent, affinity-pinned threadpool with
dynamic chunk-stealing rather than a per-GEMV fork-join. This fixes the
multi-core decode collapse on Android big.LITTLE and scales decode across the
performance cores (Tensor G5, LFM2 Q4_0, warm: CPU decode matches or beats
llama.cpp). Thread placement and count auto-detect per device; the CERA_*
override knobs are documented under
CPU threading & tuning.
Detailed methodology, per-model tables (decode + prefill vs llama.cpp), the
Accelerate/AMX BLAS results, and the backend optimization notes live in
benchmarks/README.md. Numbers there are tagged with
the commit and device they were measured on; some sections are older than
others, and the Android GPU row in particular predates the wgpu work above.
Licensed under either of Apache License, Version 2.0 or MIT License at your option.
70 followers · starred Jun 2026