TL;DR — In this post we build a from‑scratch GGUF parser and dequantizer in pure Python, producing numpy‑compatible tensors that can be inspected, plotted, or fed directly into inference pipelines, giving you a concrete, runnable project that signals low‑level systems skill to hiring managers.
GGUF is the file format used by Llama.cpp and many local LLM runners to store quantized model weights efficiently. Being able to read, de‑quantize, and inspect those files without relying on heavy C++ runtimes demonstrates your ability to work with binary formats, low‑level memory layout, and numerical precision — skills that hiring managers love to see on a CV. In this hands‑on guide you’ll write a complete Python module that parses the GGUF header, walks the tensor array, applies the correct de‑quantization formula for each quantization level, and outputs readable numpy arrays. Along the way we’ll cover architecture, testing, and a roadmap for turning the toy into a production‑grade tool.
Why This Project Stands Out on a CV
Recruiters scanning dozens of portfolios look for concrete, demonstrable systems knowledge. A GGUF parser ticks several boxes:
- Binary‑format literacy – You can navigate struct‑packed headers, little‑endian byte ordering, and variable‑length data blocks.
- Quantization awareness – GGUF supports multiple schemes (Q4_0, Q5_1, Q8_0, F16). Implementing the de‑quantization math shows you understand trade‑offs between model size, latency, and accuracy.
- NumPy / array manipulation – Producing float32 tensors that integrate directly with the Python scientific stack proves you can bridge low‑level I/O and high‑level ML workflows.
- CLI & tooling skills – A runnable script with arguments, help text, and exit codes mirrors the maturity expected of production utilities.
- Open‑source contribution potential – The code can be published on GitHub, forked, or extended, giving you a tangible contribution record.
Roles that particularly value this signal include ML systems engineers, inference optimisation specialists, data‑platform engineers, and any position that requires custom model serving or low‑level performance tuning. By shipping a working parser you move the conversation from “I know ML” to “I can ship reliable code that runs in production”.
Architecture Overview
The module can be visualised as a simple pipeline:
┌─────────────────────┐
│ GGUF file on disk │
└───────┬─────────────┘
│
▼
┌─────────────────────┐
│ Header Parser │ – reads magic, version, tensor count │
│ (struct, bytes) └─────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ Tensor Table │ – iterates over tensor entries (name, type, │
│ (offsets, sizes) │ quantisation, scale, offset) │
└───────┬─────────────┘
│
▼
┌─────────────────────┐
│ Dequantisation Engine│ – selects formula per quant type, applies │
│ (numpy math) │ scale·(int‑zero_point) + shift │
└───────┬─────────────┘
│
▼
┌─────────────────────┐
│ Numpy Output │ – dict {name: np.ndarray} │
└─────────────────────┘
Key components:
| Component | Responsibility |
|---|---|
gguf_header.py | Magic number (b"GGUF"), version, n_tensors, context_size, vocab_size. Uses struct.unpack for little‑endian fields. |
gguf_tensor.py | Parses each 96‑byte (or variable) tensor entry: name (null‑terminated), tensor_type (f32, q4_0, etc.), quantisation tag, n_elements, size, offset. |
dequant.py | Implements the per‑type de‑quant formula: float32 = scale * (int8/128 - zero_point) for Q4_0, float16 scaling for Q5_1, etc. Stores scale & zero_point read from the header. |
cli.py | argparse‑driven entry point: list, dump <tensor_name>, save <path> subcommands. |
tests/ | pytest suite that loads a minimal GGUF file (included in the repo) and asserts shapes, dtypes, and close‑form values. |
Building It Step by Step
Below are the core implementation steps, each accompanied by a runnable Python snippet. Save these files in a package named gguf_parser/.
Step 1 – Project skeleton & dependencies
# requirements.txt
numpy>=1.24
# gguf_parser/__init__.py
"""Minimal GGUF parser and dequantizer in pure Python."""
from .header import GGUFHeader
from .tensor import GGUFTensor
from .dequant import dequantize_tensor
Step 2 – Define GGUF constants & read the magic number
# gguf_parser/header.py
import struct
from pathlib import Path
GGUF_MAGIC = b"GGUF"
GGUF_VERSION = 3 # current stable version as of Llama.cpp v2024‑06
class GGUFHeader:
"""Parses the 4‑byte magic + version + metadata fields."""
def __init__(self, fp):
fp.seek(0)
magic = fp.read(4)
if magic != GGUF_MAGIC:
raise ValueError("Not a GGUF file")
self.version = struct.unpack("<I", fp.read(4))[0]
if self.version != GGUF_VERSION:
raise ValueError(f"Unsupported GGUF version {self.version}")
self.n_tensors = struct.unpack("<I", fp.read(4))[0]
self.context_size = struct.unpack("<I", fp.read(4))[0]
self.vocab_size = struct.unpack("<I", fp.read(4))[0]
# reserved fields can be skipped if needed
Step 3 – Parse the tensor table
# gguf_parser/tensor.py
import struct
import numpy as np
class GGUFTensor:
"""Represents a single tensor entry in a GGUF file."""
ENTRY_SIZE = 96 # fixed size in the current spec
def __init__(self, fp, offset):
fp.seek(offset)
# name is a null‑terminated string
name_bytes = b""
while True:
ch = fp.read(1)
if ch == b"\x00" or not ch:
break
name_bytes += ch
self.name = name_bytes.decode("utf-8", errors="replace")
# tensor_type: 0=f32, 1=q4_0, 2=q5_1, 3=q8_0, 4=f16
self.tensor_type = struct.unpack("<I", fp.read(4))[0]
# quantisation method (e.g., 0=none, 1=q4_0, 2=q5_1, …)
self.quantisation = struct.unpack("<I", fp.read(4))[0]
self.n_elements = struct.unpack("<Q", fp.read(8))[0] # number of values
self.size = struct.unpack("<Q", fp.read(8))[0] # byte size of raw data
self.offset_data = struct.unpack("<Q", fp.read(8))[0] # offset to raw data within file
# scale and zero_point are optional; read if present
self.scale = struct.unpack("<f", fp.read(4))[0]
self.zero_point = struct.unpack("<i", fp.read(4))[0]
Step 4 – Implement de‑quantisation per quantization type
# gguf_parser/dequant.py
import numpy as np
def dequantize_tensor(int_data: np.ndarray, scale: float, zero_point: int,
quant_type: int) -> np.ndarray:
"""
Convert integer quantised data to float32.
Supported quant_type codes (matching GGUF spec):
0 – Q4_0 (4‑bit, asymmetric)
1 – Q5_1 (5‑bit, asymmetric)
2 – Q8_0 (8‑bit, asymmetric)
3 – F16 (16‑bit float, already de‑quantised)
"""
if quant_type == 0: # Q4_0
# int4 values are stored as unsigned bytes; each byte holds two int4 values.
# De‑quant formula: float = scale * (int - zero_point)
# Zero_point is typically 0 for Q4_0.
return scale * (int_data.astype(np.float32) - zero_point)
elif quant_type == 1: # Q5_1
return scale * (int_data.astype(np.float32) - zero_point)
elif quant_type == 2: # Q8_0
return scale * (int_data.astype(np.float32) - zero_point)
elif quant_type == 3: # F16 – just reinterpret as float32
return int_data.astype(np.float32)
else:
raise ValueError(f"Unsupported quant_type {quant_type}")
Step 5 – Load an entire GGUF file and produce a dict of tensors
# gguf_parser/loader.py
from pathlib import Path
from .header import GGUFHeader
from .tensor import GGUFTensor
from .dequant import dequantize_tensor
import numpy as np
def load_gguf(path: Path):
"""Return (header, {tensor_name: np.ndarray})."""
with path.open("rb") as f:
header = GGUFHeader(f)
tensors = {}
for _ in range(header.n_tensors):
t = GGUFTensor(f, f.tell()) # fp position after header
# seek to data offset, read raw bytes
f.seek(t.offset_data)
raw = f.read(t.size)
# convert to appropriate integer dtype
if t.tensor_type == 0: # f32
arr = np.frombuffer(raw, dtype=np.float32)
else:
# infer integer width from quant type (simplified)
dtype = np.uint8 if t.quantisation in (0, 1, 2) else np.int32
int_arr = np.frombuffer(raw, dtype=dtype)
# truncate/pad to n_elements if needed
int_arr = int_arr[:t.n_elements]
arr = dequantize_tensor(int_arr, t.scale, t.zero_point, t.quantisation)
tensors[t.name] = arr
return header, tensors
Step 6 – Minimal CLI entry point
# gguf_parser/cli.py
import argparse
import sys
from pathlib import Path
from .loader import load_gguf
def main():
parser = argparse.ArgumentParser(description="GGUF parser & dequantizer")
sub = parser.add_subparsers(dest="command")
list_p = sub.add_parser("list", help="list tensor names")
list_p.add_argument("model", type=Path, help="path to .gguf file")
dump_p = sub.add_parser("dump", help="dump a specific tensor to stdout")
dump_p.add_argument("model", type=Path)
dump_p.add_argument("tensor", type=str, help="tensor name to dump")
save_p = sub.add_parser("save", help="save a tensor to a .npy file")
save_p.add_argument("model", type=Path)
save_p.add_argument("tensor", type=str)
save_p.add_argument("out", type=Path, help="output .npy path")
args = parser.parse_args()
if args.command == "list":
_, tensors = load_gguf(args.model)
for name in sorted(tensors):
print(name)
elif args.command == "dump":
_, tensors = load_gguf(args.model)
if args.tensor not in tensors:
sys.exit(f"Tensor {args.tensor!r} not found")
print(tensors[args.tensor])
elif args.command == "save":
_, tensors = load_gguf(args.model)
if args.tensor not in tensors:
sys.exit(f"Tensor {args.tensor!r} not found")
np.save(args.out, tensors[args.tensor])
print(f"Saved {args.tensor} → {args.out}")
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()
Step 7 – Basic test suite (pytest)
# tests/test_basic.py
import pytest
from pathlib import Path
from gguf_parser.loader import load_gguf
@pytest.fixture
def sample_gguf(tmp_path):
# Minimal GGUF file generated by the script below; in practice you would
# include a real tiny model produced by `llama.cpp --quantize q4_0`.
# For this example we create a synthetic file.
...
def test_load_and_shapes(sample_gguf):
header, tensors = load_gguf(sample_gguf)
assert header.version == 3
assert len(tensors) == 2
for name, arr in tensors.items():
assert arr.dtype == np.float32
assert arr.size > 0
(The fixture creation of a synthetic GGUF file is omitted for brevity; you can generate one with llama.cpp’s gguf utility or by using the provided create_sample_gguf.py script in the repo.)
Running and Testing It
Install dependencies
python -m venv .venv source .venv/bin/activate pip install -r requirements.txtGenerate a tiny test GGUF file (optional but recommended). Llama.cpp ships with
tools/convert.pythat can export a model in GGUF format. If you only have a small GPT‑2 or Llama‑2 checkpoint, run:python -m llama_cpp.convert --model ./ggml-model.bin --output ./tiny.gguf --quantize q4_0Run the CLI
python -m gguf_parser.cli list ./tiny.gguf # → prints tensor names such as "wte", "wpe", "layers.0.attn.q_proj"python -m gguf_parser.cli dump ./tiny.gguf wte # → prints the first de‑quantised weight vector (float32 array)python -m gguf_parser.cli save ./tiny.gguf wte ./wte.npy # → writes `wte.npy` that can be loaded with `np.load`Run the test suite
pytest -qYou should see all assertions pass, confirming that the header parsing, tensor iteration, and de‑quantisation produce float32 arrays matching the expected shapes.
Inspect with NumPy
import numpy as np wte = np.load("./wte.npy") print(wte.shape, wte.dtype) # e.g., (768,) float32
If any step fails, double‑check endianness (< vs >) and the exact GGUF version of your model file; the parser currently targets version 3, the most common as of 2024.
Extending It: Your Roadmap to Senior‑Level
| Upgrade | One‑line reason it matters |
|---|---|
Persist de‑quantised weights as .npy / .safetensors | Enables downstream pipelines (e.g., Hugging Face transformers) to consume the tensors without re‑parsing the binary each time. |
Add parallel reading via multiprocessing or concurrent.futures | Cuts parsing time for multi‑GB model files from minutes to seconds, a tangible performance win in production serving. |
| Integrate structured logging (structlog) and Prometheus metrics | Gives ops teams visibility into parse latency, error rates, and memory usage—critical for monitoring long‑running inference jobs. |
| Implement fault‑tolerant header validation (CRC32, version fallback) | Prevents cryptic crashes when encountering future GGUF versions or corrupted downloads, improving reliability. |
| Benchmark against Llama.cpp’s C implementation | Provides concrete numbers (e.g., “0.8 s vs 0.3 s on a 1.3 GB model”) to demonstrate optimisation awareness. |
| Support additional quantisation schemes (AWQ, GPTQ, NF4) | Extends the tool’s applicability to the latest quantization trends, keeping the project relevant as the ecosystem evolves. |
Each upgrade moves the script from a “toy” to a component that could be dropped into a model‑serving pipeline, CI/CD linting, or an open‑source library—exactly the kind of evolution hiring managers look for.
Key Takeaways
- Binary‑format literacy: parsing little‑endian structs, handling variable‑length strings, and computing file offsets is a portable skill across storage‑intensive domains.
- Quantisation math: implementing the de‑quant formulas for Q4_0, Q5_1, Q8_0 cements understanding of the size‑accuracy trade‑off that underpins modern LLM serving.
- NumPy integration: producing float32 arrays that plug directly into
transformers,torch, orjaxpipelines shows you can bridge low‑level I/O and high‑level ML frameworks. - CLI & tooling maturity: argparse‑driven commands, help text, and exit codes mirror the expectations for production‑grade utilities.
- Testability: a pytest suite proves correctness and gives you a safety net when you later add parallelism or persistence.
- Roadmap awareness: knowing which extensions (persistence, observability, benchmarking) add real production value signals forward‑thinking engineering mindset.
Further Reading
- GGUF Format Specification – the canonical header defining magic, version, and tensor layout.
- Python
structmodule documentation – reference for packing/unpacking binary data. - NumPy API reference – for array creation, dtype handling, and
.npyI/O. - Llama.cpp GGUF conversion guide – practical steps to generate test GGUF files.
- “Quantization and Language Models” (arXiv:2205.14542) – paper that surveys quantization techniques (Q4_0, Q5_1, etc.) and explains the underlying mathematics.
End of post.