anamnesis
ἀνάμνησις: Parse any format, recover any precision.
A framework-agnostic Rust library and CLI for tensor-file work: parse
.safetensors, .gguf, .npz, and PyTorch .pth; dequantize quantized
weights back to bit-exact BF16 (FP8 · GPTQ · AWQ · BitsAndBytes · all 22 GGUF
block types); inspect any format header-only without loading weights; and convert
between formats, all hardened for untrusted input.
Published on crates.io; pre-1.0, so the API may still evolve; see CHANGELOG and ROADMAP.
Table of Contents
- Install
- CLI Commands
- Try it
- Library quick start
- Parsing untrusted input
- Formats & quantization support
- Performance & validation
- Documentation
- What's next
- Used by
- License
- Development
New to anamnesis?
- I have a model file and want to see inside it →
amn inspect, or the Inspect before you parse tutorial: format, tensors, shapes, dtypes, size, header-only.- I want to recover full precision from a quantized model → Dequantize a GGUF model to BF16: a k-quant becomes standard
BF16safetensors, loadable in candle / burn / tch.- I want to move a model to another format → Convert a model between formats: any input →
safetensors/gguf/bnb-nf4in one command, quantized inputs auto-chained throughBF16.- I'm parsing untrusted / user-uploaded files on a server → Parsing untrusted input: the
inspect → check → parse-under-limitsrecipe: typed errors, no panic, noSIGBUS.- I'm calling anamnesis from Rust → Library quick start.
- I'm waiting for Python →
pip install anamnesislands in v0.8.0 (What's next); the interop contract is already frozen.Common questions live in the FAQ; every command and flag is in the CLI Reference.
Install
Installs both anamnesis and amn (short alias). Pick the formats and schemes
you need via feature flags (gptq, awq, bnb, npz, pth, gguf, ollama,
indicatif); the full list is in the CLI Reference.
CLI Commands
| Command | |
|---|---|
amn parse <file> |
Parse and summarize a model file (.safetensors, .pth, .npz, .gguf) |
amn inspect <file> |
Show format, tensor counts, size estimates, dtypes, byte order |
amn remember <file> |
Dequantize to BF16 (safetensors) or convert .pth/.gguf → .safetensors |
amn convert <file> --to <target> |
Convert any input to safetensors / gguf / bnb-nf4 through one dispatch |
Aliases: amn info = amn inspect, amn dequantize = amn remember. Format
detection is automatic (by extension, then magic bytes for .bin). Every flag,
the full convert matrix, output-path rules, and the ollama: URL scheme are in
the CLI Reference.
Try it
$ amn inspect model-fp8.safetensors # what precision is in here?
Format: Fine-grained FP8 (E4M3), 128x128 blocks
Quantized: 1 tensors (weights) + 1 scale tensors (F32)
Passthrough: 1 tensors (norms, embeddings)
Size: 96 B (FP8) -> 144 B (BF16)
Lethe took: ~48 B of precision
$ amn remember model-fp8.safetensors # recover it: FP8 → bit-exact BF16
Parsing... 3 tensors, Fine-grained FP8 (E4M3), 128x128 blocks
Recalling... 1 tensors
Output: model-bf16.safetensors (144 B)
$ amn inspect ollama:llama3.2:1b # header-only on a real GGUF, no weights loaded
Format: GGUF v3
Arch: llama
Tensors: 147
Total size: 1.22 GB
Dtypes: F32, Q8_0
Alignment: 32 bytes
$ amn convert model.npz --to safetensors # any input → any target, one dispatch
Converting model.npz -> model-bf16.safetensors (NPZ -> safetensors)
Wrote 5 tensors -> model-bf16.safetensors
The ollama: scheme (built with --features ollama) resolves
ollama:<model>:<tag> straight to the local Ollama cache's GGUF blob; see the
CLI Reference.
Library quick start
[]
= { = "0.7", = ["gguf", "pth"] }
Inspect any format header-only (no weight data loaded):
let info = parse_gguf?.inspect;
println!;
Dequantize a quantized model to bit-exact BF16 safetensors, in memory:
use ;
let bytes = parse?.remember_to_bytes?;
For untrusted input, prefer the copy-based parse_bytes / parse_*_from_reader
entry points (see below). Full API on docs.rs.
Parsing untrusted input
A .safetensors, .npz, .pth, or .gguf file is attacker-controllable: it
can declare arbitrary tensor counts, shapes, lengths, or expansion ratios.
anamnesis is parse-first so a multi-tenant or edge host can reject a hostile
or too-large file against limits it chooses, before committing memory. The
recipe is inspect → check policy → parse under limits:
use ;
// 1. Inspect cheaply: header-only, bounded; never materialises tensor data.
let info = inspect_npz_from_reader?;
// 2. Reject early against YOUR environment's policy: no parse, no allocation.
if info.total_bytes > my_ram_budget || info.tensors.len > my_tensor_cap
// 3. Parse under a `ParseLimits` matched to this worker, enforced fail-fast,
// before each allocation.
let limits = default
.with_max_total_bytes
.with_max_item_count
.with_max_decompression_ratio; // reject a DEFLATE entry inflating >1000×
let tensors = parse_npz_with_limits?;
Every parser has a parse_*_with_limits sibling. The four ParseLimits axes
(all opt-in; ParseLimits::default() is unbounded, tighten-only over the
built-in per-format floors):
| Axis | Builder | Bounds |
|---|---|---|
| Single allocation | with_max_single_alloc |
the largest single header-declared buffer |
| Cumulative heap | with_max_total_bytes |
the running sum across the whole file (many-small-items blow-up) |
| Item count | with_max_item_count |
declared tensors / arrays / KV entries / archive members |
| Decompression ratio | with_max_decompression_ratio |
a compressed entry's uncompressed:compressed ratio (.npz zip-bomb cap) |
Error taxonomy. A rejection's kind is its AnamnesisError variant, so a
host branches without string-matching: a budget/cap breach (a ParseLimits axis
or an always-on permanent floor like MAX_PKL_SIZE) is LimitExceeded { limit, message }
(→ 413); a malformed/truncated file is Parse (→ 400); a .pth pickle
referencing a GLOBAL outside the torch.* allowlist is DisallowedGlobal { module, name }
(a security signal); a recognised-but-unimplemented format/dtype is Unsupported;
I/O failures are Io. The v0.8.0 Python bindings map these one-to-one:
AnamnesisError |
Python exception |
|---|---|
Parse |
ParseError |
Unsupported |
UnsupportedError |
LimitExceeded |
LimitExceededError |
DisallowedGlobal |
SecurityError |
Io |
builtin OSError |
No panic, no abort. No public parse/inspect entry point panics or aborts on
any input: a hostile file is always a clean Err, never an unwinding panic and
never a SIGBUS (the copy-based parse_bytes / parse_*_from_reader paths use
no memory map). Pinned in stable CI by tests/no_panic.rs and the cargo fuzz
harness.
Walkthrough: Inspect before you parse. The per-version hardening history is in Validation → robustness timeline.
Formats & quantization support
| Format | Inspect | Parse | Dequantize → BF16 | Quantize | Convert to |
|---|---|---|---|---|---|
| safetensors | ✓ | ✓ | ✓ FP8 · GPTQ · AWQ · BitsAndBytes | ✓ BnB-NF4 (Lethe) | safetensors · gguf · bnb-nf4 |
| GGUF | ✓ | ✓ | ✓ all 22 block types | n/a | safetensors · gguf · bnb-nf4 |
| NPZ | ✓ | ✓ | n/a (already full precision) | n/a | safetensors · gguf · bnb-nf4 |
PyTorch .pth |
✓ | ✓ | n/a | n/a | safetensors · gguf · bnb-nf4 |
Every dequant and quant kernel is bit-exact (0 ULP) against the canonical
library's own code (bitsandbytes, AutoAWQ, GPTQModel, PyTorch's fp8 cast,
gguf-py), not a hand-rolled oracle. Header-only inspection works over any
reader (in-memory, HTTP-range, custom transport): safetensors needs only Read;
the ZIP/GGUF formats need Read + Seek. amn convert routes every
(input × target) pair through an in-memory BF16 hub, so a quantised input
auto-dequantises on the way to any target and gguf → gguf recovers precision in
place (preserving the source metadata KV). Non-safetensors formats and each scheme
are feature-gated (gguf, npz, pth, gptq, awq, bnb).
→ Full tested-model tables, per-kernel speeds, conversion benchmarks, and methodology: Validation & tested models.
Performance & validation
Representative measured results (release build, target-cpu=native, best-of-5):
- Dequantization: 2.7–54× faster than the reference Python/PyTorch path, bit-exact (0 ULP) across FP8 / GPTQ / AWQ / BnB / 22 GGUF block types.
- PyTorch
.pthparsing: 11–31× faster thantorch.load()on torchvision models; NPZ at 3.6 GB/s (17.7× thenpyzcrate). - Conversion:
npz → safetensors6.75×,pth → safetensors5.18×,safetensors → BnB-NF42.67× vs the Python ecosystem default. - Multi-threaded dequant: whole-model
remember/convertrun ~3–4× faster across CPU cores via the default-onparallelfeature (std::thread::scope, no new dependency), giving byte-identical output at any thread count, with a modestmin(cores, 4)default (opt out withdefault-features = false, or tune with--threads). Since v0.7.2 theGGUF-input path is parallelised too, at ~1.9× on the reader stage, lower than the safetensors path because the conversion hub owns one buffer per tensor, measured and explained here. - vs the Python
GGUFstack: whole-modelGGUFdequantisation is 17–28× faster thangguf-pysingle-threaded and 34–53× at the default thread budget. Not a like-for-like output:gguf-pyreturnsfloat32, anamnesis returnsBF16, which is half the bytes and the narrower type. What is verified is that anamnesis'sBF16is bit-identical togguf-py'sfloat32correctly rounded toBF16(0 ULP, all 22 kernels), so the two agree on the numbers and differ only in the delivered width. Since that width difference is itself worth ~2× of memory traffic on a bandwidth-bound workload, halvinggguf-py's time as a generous correction still leaves ~9–14× and ~17–26×.
These are guarded against regression by CodSpeed continuous
benchmarking in CI, plus dev-only tracks: Criterion runtime
benchmarks, dhat-rs peak-heap assertions
that hold each kernel to its documented # Memory ceiling, and an Ollama
cross-validation, none of which ship in the published crate.
Numbers, fixtures, and method: Validation & tested models
and docs/perf-experiments.md.
Verifying the correctness claims yourself
The crate published to crates.io is 0.60 MiB and excludes tests/, because
the cross-validation corpus is 6.3 MiB of binary goldens that no consumer's
build can reach. The corpus ships with every
GitHub Release
instead: the Source code (tar.gz) asset carries tests/ verbatim.
# from a release tarball or a clone
That runs the cross_validation_* suites and prints which reference produced
each family's goldens (gguf-py, bitsandbytes, AutoAWQ, GPTQModel,
PyTorch, NumPy). Be clear on what it establishes: it checks anamnesis against
goldens committed to this repository, and does not re-derive them. The
script explains how to do that stronger check, which for GGUF needs nothing
but pip install gguf numpy and no model download.
Documentation
| Doc | |
|---|---|
| CLI Reference | Every subcommand, flag, the convert matrix, output-path rules, the ollama: scheme |
| FAQ | Common questions: install, feature flags, formats, inspect vs parse, dequantizing/converting, untrusted input |
| Validation & tested models | Cross-validation tables, per-kernel speeds, conversion benchmarks, peak-heap assertions, hardening timeline |
| Tutorial: Inspect before you parse | The inspect → check → parse safety pattern; rejecting a hostile file; bounding memory with ParseLimits |
| Tutorial: Dequantize a GGUF model to BF16 | inspect → remember → verify: a GGUF k-quant becomes standard BF16 safetensors |
| Tutorial: Convert a model between formats | amn convert --to: any input → safetensors / gguf / bnb-nf4 through the BF16 hub, with GGUF-KV stamping |
| Python interop contract | The frozen v0.8.0 binding contract: panic-safety, the NumPy/BF16 ownership model |
| Performance experiments | Measured perf hypotheses, methods, and outcomes (incl. rejected ones) |
What's next
- Phase 7.3, caller-chosen output dtype for
convert(v0.7.3): the dequantisation output type becomes a parameter (BF16/F32/F16), monomorphised like a C++ template argument, with a runtime dispatch at the API edge.F32drops the narrowing step entirely, so the output is the referencef32itself (today'sBF16rounds 80–97 % of values), cross-validated againstgguf-pywith no rounding in between for the first time. - Phase 7.4, the same for
remember(v0.7.4):TargetDtypegainsF32andF16, and the FP8 / GPTQ / AWQ / BnB kernels become generic over the output type. Lands before the bindings freeze a dtype contract, so a Python caller can ask for a plainnp.float32array without an optional dependency. - Phase 8, Python bindings (v0.8.0):
pip install anamnesis, with typed exceptions, owned NumPy arrays,ml_dtypes.bfloat16. The interop contract is already frozen.
Full plan in ROADMAP.md; progress in CHANGELOG.md.
Used by
- candle-mi: Mechanistic interpretability toolkit for language models
- hf-fetch-model: Download, inspect, and compare HuggingFace models from Rust; uses anamnesis to parse cached tensor-file metadata (
.safetensors/ GGUF / NPZ / PyTorch.pth) forhf-fm inspect --cached
License
Licensed under either of Apache License, Version 2.0 or MIT License at your option.
Development
- Exclusively developed with Claude Code
- Continuous performance benchmarking with CodSpeed
- Git workflow managed with Fork