Skip to content

GGUF, Safetensors and Model File Formats

5 min read · updated August 3, 2026

A model file is a container for numbers, which sounds like it should be a boring subject. One of the formats in common use is a serialised Python program, and loading it runs that program. That is the reason this page exists.

The formats you will meet

FormatDescription
.bin / .pt / .pthThe legacy PyTorch checkpoint: a zip archive containing tensor data plus a Python pickle describing how to rebuild the object. Unpickling executes code by design. Still found on older repositories and in research artefacts.
.safetensorsA JSON header giving each tensor a name, dtype, shape and byte range, followed by the raw bytes. No code, no execution, memory-mappable so a large file loads without being copied through RAM. The default for Hugging Face releases.
.ggufllama.cpp’s single-file container: key-value metadata plus quantised tensors, self-describing enough that one file needs no accompanying tokenizer or config. The format of the local-inference world.
GPU-native quantised weightsAWQ, GPTQ and similar, usually distributed as safetensors with extra metadata. Aimed at batching servers on datacentre GPUs rather than at laptops; not interchangeable with GGUF.
ONNX, MLX, CoreMLRuntime-specific packagings for cross-platform runtimes and for Apple hardware. Conversion targets rather than distribution formats — you meet them when a particular runtime requires one.

Why the legacy format is a security problem

Python’s pickle protocol is not a data format. It is a small stack machine, and one of its opcodes calls an arbitrary function by name. A checkpoint can therefore contain instructions that run when the file is deserialised — before any inference happens, before you see a token, with the privileges of whoever ran the loader.

Nothing about this requires a sophisticated attack. The payload is just a callable and its arguments, sitting in a file that looks like every other checkpoint. Model hubs scan for known-bad patterns, and scanning is a mitigation rather than a guarantee.

If you must load a legacy checkpoint, load it in a way that refuses to execute:

import torch
sd = torch.load("model.bin", map_location="cpu", weights_only=True)
# weights_only restricts unpickling to plain tensors and containers.
# Recent PyTorch versions default it to True; older ones do not.

# then convert once and never touch the .bin again
from safetensors.torch import save_file
save_file(sd, "model.safetensors")

Better still, prefer a repository that already publishes safetensors, and treat a weights-only-.bin release from an unknown uploader the way you would treat an unsigned executable from the same source.

It is worth being clear about why this format persisted so long. It was never designed as a distribution format; it was designed to save and restore Python objects inside one trusted process, where executing code on load is a feature rather than a hazard. The ecosystem then started shipping those files across the internet to strangers, which changed the threat model completely without changing the format. That is the whole story, and it is why the fix was a new container rather than a patch.

What safetensors guarantees

The design is deliberately unambitious, which is the point. A length prefix, a JSON header, then bytes. Parsing it cannot invoke anything, because there is nothing in the format capable of naming a function.

  • Zero-copy loading. Tensors are laid out contiguously so the file can be memory-mapped straight into the address space. This is why a large safetensors model can start faster than its size suggests.
  • Lazy access. A loader can read one tensor without materialising the rest, which is what makes sharded loading and selective offload practical.
  • No behavioural metadata. Architecture, tokenizer and chat template live in separate files. Downloading only the .safetensors gets you numbers and nothing that can run them.
  • Not a licence, not a provenance system. The format says nothing about who made the weights or whether they were tampered with. Verify the hash you were given; the container will not do it for you.

What GGUF carries beyond tensors

GGUF solves a different problem: shipping one file that a local runtime can use with no other files present. So it carries metadata the safetensors format deliberately leaves out — the architecture and its hyperparameters, the full tokenizer including merges and special tokens, the chat template, rope parameters, and the quantisation type of every tensor.

Two consequences follow. The good one: a single download works, and the correct chat template travels with the model instead of being guessed by whatever loaded it. The one to keep in mind: that metadata is behaviour. A GGUF with a mangled template or wrong rope settings produces a model that seems mysteriously worse, and nothing in the file signals the difference. Inspect rather than assume:

# metadata is printed at load time
llama-server -m model.gguf -c 4096 --verbose 2>&1 | head -60

# look for: general.architecture, block_count, attention.head_count_kv,
# context_length, rope.freq_base, tokenizer.chat_template,
# and the per-tensor quant types near the end

On the security axis GGUF is much closer to safetensors than to pickle: there is no code in the container and no execution during parsing. It is not magic — a container parser is still C++ handling untrusted input, and parser bugs have been found and fixed — so the ordinary rule applies. Prefer files from the model’s own publisher or a repackager you have reason to trust, and check the hash.

Practical rules

  • Local, single user, CPU or consumer GPU: GGUF. Everything in that ecosystem expects it.
  • Batching server on datacentre GPUs: safetensors, either at full precision or in a GPU-native quantised form the server supports. GGUF support in those servers ranges from absent to unhappy.
  • Fine-tuning: safetensors at the training precision. Convert to GGUF at the end, as a deployment step, never as an intermediate.
  • Anything you did not produce: record the SHA of the exact file next to the model name in your configuration. Quantised repackagings are frequently re-uploaded, and “the same model behaved differently after we redeployed” is a hash question before it is anything else.
  • Never unpickle from a source you would not run a script from. It is the same act.
  • Expect several files, not one. Large safetensors models are sharded, with an index file mapping tensor names to shards. Downloading a subset produces a confusing load error rather than an obvious one — fetch the whole repository, or use a client that follows the index.
GGUF, Safetensors and Model File Formats · Multigrid