anamnesis 0.7.2

Parse any tensor format, recover any precision — framework-agnostic FP8/GPTQ/AWQ/BnB dequantization, NPZ parsing, and PyTorch .pth conversion for Rust
Documentation

anamnesis

CI crates.io docs.rs MSRV license unsafe: deny

ἀνάμνησις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

New to anamnesis?

  • I have a model file and want to see inside itamn inspect, or the Inspect before you parse tutorial — format, tensors, shapes, dtypes, size, header-only.
  • I want to recover full precision from a quantized modelDequantize a GGUF model to BF16: a k-quant becomes standard BF16 safetensors, loadable in candle / burn / tch.
  • I want to move a model to another formatConvert a model between formats: any input → safetensors / gguf / bnb-nf4 in one command, quantized inputs auto-chained through BF16.
  • I'm parsing untrusted / user-uploaded files on a serverParsing untrusted input: the inspect → check → parse-under-limits recipe — typed errors, no panic, no SIGBUS.
  • I'm calling anamnesis from RustLibrary quick start.
  • I'm waiting for Pythonpip install anamnesis lands 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

cargo install anamnesis --features cli,pth,gguf

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

[dependencies]
anamnesis = { version = "0.7", features = ["gguf", "pth"] }

Inspect any format header-only (no weight data loaded):

let info = anamnesis::parse_gguf("model.gguf")?.inspect();
println!("{} tensors, {} bytes", info.tensor_count, info.total_bytes);

Dequantize a quantized model to bit-exact BF16 safetensors, in memory:

use anamnesis::{parse, TargetDtype};
let bytes = parse("model-fp8.safetensors")?.remember_to_bytes(TargetDtype::BF16)?;

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 anamnesis::{inspect_npz_from_reader, parse_npz_with_limits, ParseLimits};

// 1. Inspect cheaply — header-only, bounded; never materialises tensor data.
let info = inspect_npz_from_reader(&mut 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 {
    return Err(/* 413 Too Large */);
}

// 3. Parse under a `ParseLimits` matched to this worker — enforced fail-fast,
//    before each allocation.
let limits = ParseLimits::default()
    .with_max_total_bytes(my_ram_budget)
    .with_max_item_count(my_tensor_cap)
    .with_max_decompression_ratio(1000); // reject a DEFLATE entry inflating >1000×
let tensors = parse_npz_with_limits(path, &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 safetensors · gguf · bnb-nf4
NPZ (already full precision) safetensors · gguf · bnb-nf4
PyTorch .pth 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 .pth parsing: 11–31× faster than torch.load() on torchvision models; NPZ at 3.6 GB/s (17.7× the npyz crate).
  • Conversion: npz → safetensors 6.75×, pth → safetensors 5.18×, safetensors → BnB-NF4 2.67× vs the Python ecosystem default.
  • Multi-threaded dequant: whole-model remember / convert run ~3–4× faster across CPU cores via the default-on parallel feature (std::thread::scope, no new dependency) — byte-identical output at any thread count, with a modest min(cores, 4) default (opt out with default-features = false, or tune with --threads). Since v0.7.2 the GGUF-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 GGUF stack: whole-model GGUF dequantisation is 17–28× faster than gguf-py single-threaded and 34–53× at the default thread budget. Not a like-for-like output: gguf-py returns float32, anamnesis returns BF16 — half the bytes, and the narrower type. What is verified is that anamnesis's BF16 is bit-identical to gguf-py's float32 correctly rounded to BF16 (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, halving gguf-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.

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 inspectremember → 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 (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. F32 recovers the exact dequantised value — today's BF16 rounds 80–97 % of them — and lands before the bindings freeze a dtype contract.
  • Phase 8 — Python bindings (v0.8.0): pip install anamnesis — 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) for hf-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