GGUF export & Ollama bundles¶
NNx ships a writer for GGUF — the on-disk format consumed by llama.cpp, Ollama, LM Studio, and the broader llama.cpp-derived inference ecosystem. The handoff in one line:
Train in NNx, serve via llama.cpp / Ollama — export the
TransformerNNto a.gguf, optionally bundle it with a Modelfile, andollama createregisters the model locally.
This is intentionally a one-way handoff (NNx -> GGUF). The reverse direction (GGUF -> NNx) isn't covered here — every other Python tool also writes GGUF; nothing reads it back into a training-shaped framework.
1. Install¶
The writer is opt-in — pull in the upstream gguf package via the
gguf-write extra:
pip install "thekaveh-nnx[gguf-write]" # adds gguf>=0.19.0
gguf is the same Python writer every other GGUF producer uses, so
the artifact is byte-compatible with every GGUF reader in the ecosystem.
2. Public surface¶
| Symbol | Notes |
|---|---|
nnx.interop.write_gguf |
Write a .gguf from a TransformerNN + tokenizer. F16 / F32 / BF16 directly; sub-F16 needs llama-quantize. |
nnx.interop.export_ollama_modelfile |
Emit a directory containing model.gguf + Modelfile ready for ollama create. |
nnx.interop.gguf.SUPPORTED_QUANTIZATIONS |
The pure-Python quantizations — ("F32", "F16", "BF16"). |
3. Quickstart — GGUF¶
from nnx import NNTokenizerParams, NNTransformerParams, TransformerNN, train_bpe # noqa: F401
from nnx.interop import write_gguf
# Assume `net` is a trained TransformerNN and `tokenizer` is the matching
# NNTokenizerParams. write_gguf packs both into a single .gguf.
write_gguf(net, tokenizer, "out/model.gguf") # F16 by default
That's the whole API. Tensor naming (token_embd.weight,
blk.{i}.attn_q.weight, ...) and the GGUF metadata (context_length,
block_count, embedding_length, feed_forward_length,
head_count, rope_freq_base, ...) are emitted under llama.cpp's
canonical key namespace.
The fused-QKV projection in NNx's TransformerNN.attn.w_qkv is split
into three tensors (attn_q, attn_k, attn_v) on write so that
llama.cpp's reader sees the layout it expects.
4. Quantization¶
| Label | Path |
|---|---|
F32 / F16 / BF16 |
Pure-Python via gguf directly. |
Q8_0 / Q6_K / Q5_K_M / Q4_K_M / Q4_0 / IQ4_XS / ... |
Two-step: write F16 here, then run the C++ llama-quantize binary. |
NNx deliberately doesn't shell out to llama-quantize automatically —
the binary's path varies by install layout (Homebrew, pip install
llama-cpp-python, source build) and silently swallowing a quantization
failure is worse than asking the user to run one command.
The recipe:
# 1. NNx writes F16
python -c "from nnx.interop import write_gguf; write_gguf(net, tok, 'out/model.gguf')"
# 2. llama-quantize is shipped by llama-cpp-python
pip install llama-cpp-python
llama-quantize out/model.gguf out/model.Q4_K_M.gguf Q4_K_M
If you ask write_gguf(..., quantization="Q4_K_M") directly it raises
ImportError with the recipe in the message.
5. Architecture tag¶
By default the writer stamps general.architecture = "nnx_transformer".
This is readable by patched / forked llama.cpp builds and by any
inference stack that reflects on metadata rather than hard-coding the
arch name.
For stock llama.cpp readers, the NNx tensor layout (RMSNorm + RoPE +
SwiGLU + tied embeddings + per-head RoPE rotation) matches the LLaMA
family closely enough that you can pass architecture="llama":
write_gguf(net, tok, "out/model.gguf", architecture="llama")
Verify against your target reader version before deploying — small divergences (e.g. RMS-norm epsilon) can show up as numerical drift even when the tensor shapes match.
6. Ollama bundles¶
export_ollama_modelfile produces a directory ready for ollama create:
from nnx.interop import export_ollama_modelfile
export_ollama_modelfile(
net,
tokenizer,
out_dir="out/ollama_bundle",
system="You are a tiny storytelling model.",
parameters={
"temperature": 0.8,
"top_k": 40,
"top_p": 0.95,
"stop": ["<eos>"], # list -> repeated PARAMETER stop lines
},
template="{{ .Prompt }}", # optional Go-template chat layout
)
Layout produced:
out/ollama_bundle/
model.gguf
Modelfile
tokenizer.json # (only if you saved it there via NNTokenizerParams.of(path=...), as examples/18 does)
The Modelfile looks like:
FROM ./model.gguf
PARAMETER temperature 0.8
PARAMETER top_k 40
PARAMETER top_p 0.95
PARAMETER stop <eos>
TEMPLATE """{{ .Prompt }}"""
SYSTEM """You are a tiny storytelling model."""
Then:
cd out/ollama_bundle
ollama create my-nnx-model -f Modelfile
ollama run my-nnx-model
7. End-to-end examples¶
| Example | Demonstrates |
|---|---|
examples/17_export_transformer_to_gguf.py |
Build TransformerNN -> write F16 GGUF -> round-trip via gguf.GGUFReader. |
examples/18_publish_to_ollama.py |
Bundle GGUF + Modelfile + parameters for ollama create. |
8. Scope¶
The writer covers TransformerNN (NNx's decoder-only LM). Other
architectures (FeedFwd, the GNNs, diffusion nets) aren't applicable to
GGUF — GGUF is a llama.cpp-family format. The TransformerNN scope is
intentionally narrow: TinyStories-class single-GPU LM. Bigger models
work as long as the tensor layout stays the same (multi-head causal
attention, RMSNorm, SwiGLU, RoPE, tied embeddings).