← chapter index
Part 13 — Model Formats: Hugging Face, GGUF & SafeTensors

Model Formats: Hugging Face, GGUF & SafeTensors

How model checkpoints are packaged, versioned, quantized, and loaded from disk before a single token is generated.

When people say they “downloaded a model,” they usually mean they downloaded a small file collection that describes a transformer and stores its learned tensors. Inference begins long before the first logits vector appears. It begins with bytes on disk: metadata, tokenizer state, tensor names, tensor shapes, data types, and sometimes quantized blocks laid out specifically to help a CPU or GPU stream weights efficiently.

That storage layer matters because it sets the rules for model startup. A good format tells you what the architecture is, where each tensor lives, how large it is, whether you can memory-map it, whether it is safe to load from untrusted sources, and whether the file is optimized for raw training checkpoints or for practical inference. A bad format makes every one of those steps more fragile.

For transformer inference, the important idea is simple: the file format is the contract between training-time artifacts and runtime code. Your loader needs to reconstruct enough structure that the question "What is the capital of France?" can be turned into token IDs, projected through embeddings, passed across the layer stack, and finally mapped back into a word such as Paris. The storage format does not answer the question, but it determines how quickly and safely you can wake the model up so it can answer.

Core idea: the same model can often exist in several packaging formats. The learned information is conceptually the same, but startup speed, memory behavior, portability, and security can differ dramatically depending on whether the weights are stored as SafeTensors, GGUF, legacy PyTorch pickle, or older GGML files.

Why inference engineers care about file formats

During training, checkpoint formats are often chosen for compatibility with a framework. During inference, the priorities shift. You care about fast load time, compact storage, predictable tensor naming, friendly memory access patterns, and safe distribution. A framework developer can afford a richer object model because they already live inside Python and PyTorch. An inference engine written in C, Rust, C#, Java, or even a shell script would much rather see explicit tensor metadata and raw bytes.

That is why so many format conversations sound like systems conversations. Can the runtime mmap the file instead of copying it? Can it stream just the tensors it needs? Can it verify shapes before allocating big buffers? Can it avoid deserializing arbitrary Python objects? Can it place quantized blocks directly into a kernel that dequantizes on the fly? The answers determine cold-start behavior and often determine whether a model is pleasant or miserable to deploy.

There is also a trust boundary. Many model files are downloaded from public registries, mirrors, or community forks. If the format lets arbitrary code execute while loading, the loader is not merely reading data. It is running whatever the file author encoded. That is acceptable inside a trusted training workflow and unacceptable for general-purpose checkpoint distribution. One reason SafeTensors gained traction so quickly is that it replaced “load and hope” with “parse declared metadata and then read raw tensor bytes.”

The Hugging Face ecosystem

The modern default distribution point for open model checkpoints is the Hugging Face Hub. Conceptually it is a model registry: the machine-learning equivalent of Docker Hub or a Git hosting service specialized for datasets, tokenizers, checkpoints, configs, and model documentation. A model lives in a repository, that repository has revisions, and clients can download a pinned snapshot by tag, branch, or immutable commit hash.

That repository usually contains more than “the weights.” It contains the descriptive envelope around the weights. A model card explains what the model is, how it was trained, what license applies, what prompt format it expects, whether it has known safety or quality limitations, and sometimes which downstream tasks it was tuned for. A config.json file describes architecture parameters such as hidden width, layer count, number of attention heads, rotary settings, vocabulary size, and special token IDs. Tokenizer files describe how raw text becomes token IDs and back again.

This packaging model is extremely practical. An inference engine can grab exactly the files it needs, and a human can inspect the rest. You can read the model card before trusting the checkpoint. You can compare config revisions across releases. You can see whether a repo offers FP16 SafeTensors, GGUF quantizations, or both. You can pin your deployment to a known-good revision instead of “whatever the author pushed last night.”

Hub conceptWhat it means in practiceWhy inference cares
RepositoryA versioned container for model artifactsKeeps config, tokenizer, and weights aligned
Model cardHuman-readable metadata and usage notesExplains prompt format, license, and caveats
RevisionBranch, tag, or immutable commit hashLets production pin exact artifacts for reproducibility
LFS-backed filesLarge binary artifacts stored efficientlyCommon for multi-gigabyte tensor shards
Download clientCLI, Git, HTTP, or library integrationSupports automated deployment and caching

Versioning is more important than it first appears. If your engine loads config.json from one revision and weights from another, you can end up with mismatched tensor names, incompatible shapes, or a tokenizer that encodes prompts differently from the model that was fine-tuned. Mature inference stacks therefore pin a full snapshot. “Model X at commit Y” is a safer deployment unit than “latest files in repo Z.”

The transformers Python library is Hugging Face’s main framework-facing interface, but for this chapter it is background context rather than the star. Its value here is that it popularized a clean repo layout: config, tokenizer, and checkpoint artifacts with stable names. Even if you are building a tiny custom engine and never import the library, you are still benefiting from that ecosystem standardization.

A typical model download: what files you actually get

A real model directory is usually a small inventory rather than a single file. Some repos stay minimal; others include several tokenizer variants, generation defaults, chat templates, quantized derivatives, conversion notes, or multiple checkpoint families. Still, a common baseline pattern appears often enough to memorize.

FilePurposeInference relevance
config.jsonArchitecture descriptionTells the loader expected tensor shapes and runtime limits
generation_config.jsonSuggested decoding defaultsOptional; useful for sample settings but not core model math
tokenizer.jsonFull tokenizer graph, vocab, merges, normalizer rulesOften enough to implement text encoding/decoding alone
tokenizer.modelSentencePiece or equivalent serialized tokenizer modelCommon in LLaMA-family checkpoints
tokenizer_config.jsonTokenizer settings and conventionsSpecial tokens, cleanup, truncation behavior
special_tokens_map.jsonBOS/EOS/PAD and related token IDsNeeded for correct prompt framing and stopping
model.safetensors or shardsRaw learned tensorsThe main weight payload
model.safetensors.index.jsonShard map for large checkpointsMaps tensor names to shard files
*.ggufSelf-contained inference-oriented packagePopular for CPU and quantized deployment

The most important small file is usually config.json. It tells your loader what kind of transformer it is about to reconstruct. A LLaMA-like config might declare the hidden dimension, intermediate FFN size, number of decoder layers, number of attention heads, number of key/value heads, maximum context length, RoPE base settings, vocabulary size, RMSNorm epsilon, and whether the model ties input embeddings to the output projection.

{
  "architectures": ["LlamaForCausalLM"],
  "hidden_size": 4096,
  "intermediate_size": 11008,
  "num_hidden_layers": 32,
  "num_attention_heads": 32,
  "num_key_value_heads": 8,
  "vocab_size": 32000,
  "max_position_embeddings": 4096,
  "rms_norm_eps": 1e-5,
  "rope_theta": 10000.0,
  "tie_word_embeddings": false
}

Those fields are not decorative. They are the blueprint your runtime uses to interpret tensor shapes. If hidden_size is 4096, then the token embedding matrix probably has 4096 columns, each RMSNorm gain vector probably has length 4096, and each attention projection must eventually map between widths derived from that value. The file does not store the matrices themselves, but it tells you what matrices the loader should expect to find in the checkpoint.

Depending on the model family, tokenization metadata may live in a single tokenizer.json file or be split across a SentencePiece tokenizer.model plus extra JSON settings. For BPE-style systems, the important ingredients are the vocabulary and the merge table: the ordered rules that say which adjacent symbol pairs can be fused into larger units. Inference code does not need the training history of the tokenizer, but it absolutely needs the final ruleset.

The heavyweight files are the tensors. In a full-precision Hugging Face-style release, they often appear as one or more SafeTensors shards. In a CPU-focused deployment release, they may appear as one or more GGUF files. Either way, the payload is conceptually the same: named arrays such as token embeddings, query/key/value projection matrices, output projections, FFN matrices, normalization gains, and sometimes the final lm_head if it is not tied to embeddings.

Weight file formats

A model weight file is, at bottom, a serialized collection of named tensors with their shapes and data types. The format determines how tensor names are encoded, where the metadata lives, whether the data can be memory-mapped, whether loading is safe from arbitrary code execution, and how easy it is to shard or stream the checkpoint. The runtime goal is always the same: build a map from tensor name to (dtype, shape, byte range) and then expose the raw tensor bytes to the inference kernels.

Think of the format as the warehouse schema. Two warehouses may contain the same crates, but one may have a searchable index, explicit shelf coordinates, and forklift-friendly aisles, while another stores the crates in a pile that only a specific Python program knows how to unpack. Inference engines prefer the first kind of warehouse.

SafeTensors was created by Hugging Face as a safer alternative to pickle-based PyTorch checkpoint files. The key security property is negative rather than positive: loading the file does not require evaluating arbitrary Python objects. The loader reads explicit metadata, validates it, and then treats the payload as raw tensor bytes. That alone makes it attractive for public checkpoint distribution.

The on-disk layout is intentionally simple. The first 8 bytes store the header length as an unsigned little-endian integer. Next comes a JSON header whose keys are tensor names and whose values declare the tensor dtype, shape, and data_offsets. After that header, the file contains a flat blob of raw tensor bytes. Because the byte ranges are explicit, a runtime can jump directly to a tensor without deserializing everything before it.

{
  "model.embed_tokens.weight": {
    "dtype": "F16",
    "shape": [32000, 4096],
    "data_offsets": [0, 262144000]
  },
  "model.layers.0.self_attn.q_proj.weight": {
    "dtype": "F16",
    "shape": [4096, 4096],
    "data_offsets": [262144000, 295698432]
  }
}

That layout is naturally memory-mappable. A loader can map the file once, parse the header, and then hand later code a pointer or slice corresponding to the requested tensor region. On machines where virtual memory behaves well, that means the operating system only pages in the parts you touch. For very large checkpoints, that is much nicer than eagerly reading tens of gigabytes into heap memory.

Large models are usually sharded across multiple files such as model-00001-of-00004.safetensors, model-00002-of-00004.safetensors, and so on, accompanied by an index JSON file that maps each tensor name to its shard. Sharding is a distribution convenience and sometimes a filesystem necessity; from the runtime’s perspective it just means the tensor map spans several files instead of one.

u64 header_len = read_u64_le(file);
string header_json = read_bytes(file, header_len);
map meta = parse_json(header_json);

for each (name, info) in meta {
    dtype = info["dtype"];
    shape = info["shape"];
    start = info["data_offsets"][0];
    end   = info["data_offsets"][1];
    tensor_table[name] = {
        .dtype = dtype,
        .shape = shape,
        .file_offset = 8 + header_len + start,
        .nbytes = end - start
    };
}

The practical advantage is that the format is boring in exactly the right way. A non-Python runtime can implement a SafeTensors loader in an afternoon, and a security-conscious deployment can accept public model files without granting them code execution privileges.

GGUF, the GPT-Generated Unified Format created by ggerganov for llama.cpp, is designed around inference rather than framework checkpoints. The big idea is self-containment: weights, tokenizer assets, and model metadata can all live inside one file. That means a CPU inference runtime can often open a single GGUF file and immediately know the architecture type, vocabulary size, context length, RoPE parameters, quantization layout, and tensor inventory without reaching for sidecar JSON files.

The file structure is more binary and more specialized than SafeTensors. A typical GGUF file begins with magic bytes and a version, followed by counts for metadata entries and tensors. Then come metadata key/value pairs, tensor descriptors, alignment padding, and finally the tensor data region. Metadata can include values such as architecture name, block size, tokenizer model, BOS/EOS IDs, maximum context length, and per-tensor quantization types.

GGUF is particularly important because it embraces quantized inference as a first-class use case. Different tensors can use different formats inside the same file: for example some may be Q4_0, some Q6_K, some Q8_0, and a few sensitive tensors may remain F16 or F32. That flexibility lets conversion pipelines preserve precision where it matters and save memory where it does not.

dequantized_value = (quantized_int - zero_point) × scale

That formula is the simplest mental model for dequantization. In many GGUF schemes the exact packing is more involved than one integer plus one zero-point for each scalar. Instead, values are grouped into blocks, often 32 weights at a time, and the block shares scale metadata. The runtime reads a compressed block, reconstructs approximate real-valued weights for that block, and immediately uses them inside a dot product instead of expanding the entire matrix up front.

The format names reflect those block schemes. You will encounter families such as Q4_0, Q4_1, Q5_0, Q5_1, Q8_0, Q6_K, Q4_K_M, Q4_K_S, Q2_K, and newer IQ variants. The exact math varies, but the engineering pattern is consistent: pack several low-bit values together, store scales and sometimes minima or zero-points alongside them, and design the layout so CPU kernels can stream through blocks predictably.

That is why GGUF dominates CPU inference. It is self-describing, single-file friendly, mmap-friendly, and explicitly optimized for quantized reads. A lightweight engine can open it, inspect metadata, wire up tokenizer state, and start generating without a framework-specific stack. On consumer hardware where bandwidth and RAM are the real constraints, that design is a major practical advantage.

Legacy PyTorch .bin checkpoint files are usually pickle containers wrapped around tensor objects. They are still common on older models and in historical tutorials, but they are an awkward fit for general inference distribution. The file can carry not just tensors but arbitrary Python object graphs, and unpickling may execute attacker-controlled code.

That does not make the format useless. Inside a trusted PyTorch workflow it was convenient and deeply integrated. But as the ecosystem matured, the drawbacks became harder to justify. Today the general direction is clear: SafeTensors for mainstream framework checkpoints, GGUF for self-contained quantized inference, and legacy pickle only when compatibility with older releases forces your hand.

GGML was the predecessor format in the llama.cpp ecosystem. You still encounter it in old model downloads, forum posts, and conversion scripts, but it has largely been superseded by GGUF. The reason is not fashion. GGUF adds richer metadata and a cleaner, more extensible structure.

Legacy GGML files often depended on external configuration knowledge. The runtime might need to know out-of-band which architecture the file represented, what tokenizer to pair with it, or how to interpret some model-specific details. GGUF closed that gap by bundling the descriptive context alongside the weights. If you are building a new inference stack today, treat GGML as historical baggage rather than a target format.

The summary is straightforward. If you are staying close to the Hugging Face framework ecosystem and want a safe checkpoint format, SafeTensors is the default answer. If you want an inference-oriented package, especially for CPU-friendly quantized deployment, GGUF is usually the better answer. If you see PyTorch .bin or GGML, assume you are dealing with history, compatibility, or both.

Quantization formats in detail

Quantization means representing weights with fewer bits than their original training precision. Instead of storing every weight as FP16, BF16, or FP32, you compress them into lower-bit integers plus enough scale metadata to reconstruct an approximate real value during inference. The motivation is bluntly physical: smaller weights consume less disk, less RAM, and less memory bandwidth. Since large-model inference is often bandwidth-bound, that can translate directly into more tokens per second.

The trade-off is approximation error. When you drop from FP16 to INT8, INT4, or even INT2-style encodings, you are admitting that a weight does not need its full original precision to remain useful. Some weights survive that compression gracefully; others are more fragile. Good quantization schemes spend extra metadata or extra bits where the model is sensitive and compress harder where redundancy is abundant.

GranularityHow scales are assignedStrengthWeakness
Per-tensorOne scale for an entire tensorVery compact metadataOften too coarse for large matrices with varied ranges
Per-channelOne scale per output channel or rowBetter accuracy for structured matricesMore metadata and slightly more loader complexity
Per-blockOne scale per small block of valuesGood balance of accuracy and compressionRequires block-aware kernels

Scale and zero-point are the standard reconstruction tools. In a symmetric scheme, quantized integers are centered around zero and a scale converts integer magnitude back into approximate real magnitude. In an asymmetric scheme, a nonzero offset or minimum value is also stored so the integer range can better cover skewed distributions. The exact representation changes by format, but the conceptual contract stays the same: encode many real numbers using a small set of integers plus a little side information.

q = round(x / scale)                symmetric case
x ≈ q × scale

q = round(x / scale) + zero_point  asymmetric case
x ≈ (q - zero_point) × scale

GGUF’s block quantization schemes take that idea and tune it for transformer weight matrices. Instead of dequantizing an entire matrix into FP16 before multiplication, the kernel reads a block, reconstructs a short vector of approximate values, multiplies it against the activation fragment, and accumulates into a higher-precision sum. That keeps the compressed representation resident for as long as possible.

GGUF familyRough intentTypical trade-off
Q8_08-bit block quantizationNear-full quality with modest savings
Q5_0 / Q5_15-bit schemes with block metadataGood compromise between RAM and quality
Q4_0 / Q4_1Classic 4-bit formatsPopular for fitting larger models onto consumer CPUs
Q4_K_M / Q4_K_SK-quant family with more sophisticated block handlingOften better quality per byte than older simple Q4
Q6_KHigher-accuracy K-quant variantMore memory than Q4 but often noticeably stronger
Q2_K and IQAggressive low-bit optionsTiny footprint, but quality loss is more likely

The accuracy curve is not linear. Going from FP16 to INT8 is often mild. Going from INT8 to good 4-bit block quantization can still be surprisingly usable. Going beyond that becomes much more model- and task-dependent. Attention projections, embeddings, and output heads may react differently from FFN weights. That is why mixed strategies are common: keep a few critical tensors in higher precision and quantize the rest more aggressively.

For inference engineers, the right question is not “what is the smallest file?” but “what is the smallest file that still behaves acceptably on my workload?” The answer depends on prompt style, domain specificity, model family, and hardware. A chat demo may tolerate more degradation than a code model or a summarizer evaluated against a reference set. The file format needs to expose enough metadata that your runtime can tell which quantization path each tensor requires.

Loading a model from disk

Once you have the files, the loading flow is conceptually mechanical. Open the metadata source. Parse the architecture description. Build a tensor map from names to offsets, shapes, and data types. Allocate or map storage. Validate that every expected tensor exists and that every discovered tensor is compatible with the config. Then wire the results into the inference structures your runtime expects.

open checkpoint
parse config / metadata
for each tensor descriptor:
    record name, dtype, shape, byte_offset, byte_length

for each required tensor in architecture:
    assert tensor exists
    assert shape matches expected dimensions
    assert dtype is supported by this backend

if eager:
    read tensor bytes into runtime-owned memory
else if mmap:
    keep file mapped and store views into tensor regions

build layer structs
attach tokenizer
ready for prefill and decode

Lazy loading and eager loading are the first major choice. Eager loading copies tensors into memory immediately. That is simple and sometimes necessary, especially if you want to transform layouts or upload weights into GPU memory. Lazy loading keeps the file mapped and only touches pages when the operating system faults them in. For large CPU inference workloads, lazy loading can drastically reduce startup cost and peak RAM pressure.

Memory mapping is especially friendly to formats with explicit offsets such as SafeTensors and GGUF. A loader can parse small metadata up front and leave the bulk tensor data in place. The runtime still pays for page faults and disk I/O eventually, but it avoids one extra copy and lets the operating system manage caching. On repeated runs, the page cache may even make the second load feel almost instant.

Sharded loading adds one more layer. In a Hugging Face SafeTensors checkpoint, the sharding may simply be file-level packaging: tensor A lives in shard 1, tensor B in shard 2. In a multi-GPU runtime, sharding can become an execution decision as well. You might place half the transformer layers on one GPU and half on another, or split especially large matrices using tensor parallelism. In both cases, the loader must know not just what the tensor is, but where the bytes live and which device or process should own them.

Validation stepWhat to checkWhy it matters
Tensor presenceAll required names existMissing norms or projections cause immediate runtime failure
Shape agreementConfig-derived dimensions match stored shapesPrevents silent misalignment in matmuls
Dtype supportBackend can handle F16, BF16, Q4, etc.Avoids discovering unsupported quantization mid-generation
Tokenizer consistencyVocab size and special tokens line upPrevents broken prompt encoding and EOS handling
Metadata sanityContext length, RoPE settings, architecture typeWrong metadata can produce subtle but severe output drift

Notice how much of loading is really verification. The files do not merely need to be readable; they need to agree with each other. A model can fail long before any arithmetic if the loader pairs the wrong tokenizer with the wrong weights, assumes a tied output head when the checkpoint stores a separate one, or misreads a quantized tensor as raw FP16. Good inference code treats “parse” and “validate” as separate stages.

The running example: “What is the capital of France?” as a file-loading story

The prompt itself is tiny, but it touches every piece of model packaging. First the loader reads config.json or GGUF metadata and learns, for example, that this is a 32-layer decoder with hidden width 4096, 32 attention heads, 8 key/value heads, a vocabulary of 32,000 items, and a RoPE-based attention scheme. Those numbers tell the runtime what tensor shapes it should expect and what kernels it will need.

Next the tokenizer assets are loaded. The text "What is the capital of France?" is normalized and segmented according to the model’s exact tokenization rules. Depending on the model, France may be one token, several subword pieces, or a token that changes if a leading space is present. The question mark may be its own token. None of this is optional trivia. If your tokenizer encoding differs from the training-time tokenizer, the model is being asked a subtly different question.

ArtifactRole in the France example
config.json or GGUF metadataTells the runtime the model dimensions and attention geometry
tokenizer.json / tokenizer.modelEncodes the prompt into token IDs and later decodes output IDs back to text
embedding matrixTurns each token ID into the first hidden vector
layer weightsProvide the Q/K/V, output, and FFN matrices used throughout the forward pass
lm_headProjects the final hidden state into vocabulary logits so Paris can win

Finally, the weight files provide the actual matrices multiplied during inference. The embedding rows corresponding to the prompt tokens are fetched. Every layer’s attention and feed-forward weights are applied. The final normalized hidden state is compared against the vocabulary projection, and the output distribution peaks around tokens associated with Paris. Change the file format and you have changed the loading machinery, but not the conceptual path from prompt to answer.

That is the durable lesson of this chapter. Model formats are not mere packaging trivia. They define how trustworthy, portable, and efficient the checkpoint is as an artifact. If you understand how Hugging Face repos, SafeTensors, GGUF, tokenizer files, and quantization metadata fit together, you are much closer to understanding what an inference engine really does before any actual inference begins.