HomeJournalThis post

Safetensors vs GGUF for Local LLMs

Compare model-file anatomy, runtime ownership, quantization, conversion lineage, and corrupt offsets before one local release ships.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

Safetensors vs GGUF is the model-file decision to make before a local release is signed, copied, or loaded. This comparison turns tensor layout, quantization ownership, runtime compatibility, conversion loss, and corrupt offsets into one reviewable release choice.

Safetensors vs GGUF starts at the byte boundary

The Safetensors format puts a bounded JSON header before tensor bytes and describes names, dtypes, shapes, and non-overlapping offsets. The Safetensors implementation makes that narrow tensor serialization contract inspectable, which is useful when the surrounding training or inference stack owns architecture and tokenizer files separately.

The GGUF model format packages typed metadata and tensors for executors in the ggml ecosystem. Its GGUF specification covers magic, versioning, key-value metadata, tensor information, alignment, and data; a release review should verify those fields instead of treating the filename extension as proof.

The committed fixtures are generated teaching headers with tiny payloads, not downloaded models. They demonstrate parsing, overlap rejection, and bounds checks only, so Safetensors vs GGUF results here say nothing about model quality, real load time, or universal executor performance.

The byte receipt now begins with generated Uint8Array fixtures: an eight-byte Safetensors header length, a bounded JSON map, GGUF magic, version 3, power-of-two alignment, typed teaching metadata, and payload ranges. It is structural evidence, not a load-time benchmark.

Two binary envelopesSafetensors begins with an eight-byte JSON length; the teaching GGUF subset begins with magic, version, alignment, metadata length, padding, then tensors.8-byte length → JSON → tensor bytesGGUF · v3 · align · metadataoverlap mutationvalid boundaryoverflow mutation
Parsed byte boundaries
FixtureVerified prefixRejected mutation
Safetensors8-byte length + bounded JSONoverlap and overflow
GGUF subsetGGUF, v3, power-of-two alignmentbad magic, version, alignment
Figure 1: The two generated formats expose different headers but share explicit tensor-range checks.

Decide who owns architecture and tokenizer metadata

A Safetensors file can carry tensor-specific metadata, but a usable release commonly needs configuration, tokenizer assets, generation settings, adapters, and code outside that file. That separation is valuable when the same weights travel between frameworks, provided the release manifest binds every auxiliary object by digest and version.

GGUF is designed to carry richer model and tokenizer metadata near quantized tensor data for compatible local runtimes. The Hub GGUF integration is a useful interoperability reference, yet each executor remains the authority on which architecture keys and quantization types it actually supports.

Safetensors vs GGUF therefore asks whether the release unit is framework-oriented tensor exchange or an executor-oriented local bundle. Write that ownership choice before conversion, because missing tokenizer or chat-template lineage can invalidate otherwise intact weights.

Auxiliary ownership is explicit in the output. The Safetensors side lists configuration, tokenizer, and generation files; the bounded GGUF subset carries architecture, tokenizer, and quantization fields. Neither list claims that every executor accepts the resulting release.

Treat quantization as a lossy lineage edge

Safetensors can store tensors at many dtypes, including already quantized representations when a consumer understands their layout, but it does not turn one arbitrary quantization scheme into a portable runtime contract. GGUF standardizes metadata around the quantization families used by its ecosystem, making local LLM deployment more direct when the target executor supports the chosen type.

Quantization changes values and may change tensor packing, so the converted artifact deserves a new digest, format declaration, tool version, parameters, and evaluation receipt. The quantization quality budget belongs beside this edge because a smaller artifact is not automatically an acceptable behavioral release.

Do not sign only the source and imply the derived file inherited identical semantics. In Safetensors vs GGUF, conversion lineage should connect source closure, converter identity, requested quantization, produced tensor inventory, and the downstream evaluation that authorized shipping.

Conversion is recorded as a lossy edge from a generated canonical tensor artifact to a generated Q4 derivative. The receipt requires input and output digests, converter identity, quantization, and an evaluation record before Safetensors vs GGUF becomes a shipping decision.

Runnable artifact — Generate two bounded teaching headers, parse their ranges, and prove that overlap plus overflow mutations are rejected.

import assert from "node:assert/strict";
const encoder=new TextEncoder(),decoder=new TextDecoder();
const u32=(view,offset,value)=>view.setUint32(offset,value,true);
const makeSafetensors=(tensors)=>{const header=encoder.encode(JSON.stringify({__metadata__:{framework:"teaching"},...Object.fromEntries(tensors.map(t=>[t.name,{dtype:"F32",shape:[(t.end-t.start)/4],data_offsets:[t.start,t.end]}]))}));const payloadBytes=Math.max(...tensors.map(t=>t.end),0);const bytes=new Uint8Array(8+header.length+payloadBytes);new DataView(bytes.buffer).setBigUint64(0,BigInt(header.length),true);bytes.set(header,8);return bytes};
const parseSafetensors=(bytes)=>{const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);const headerLength=Number(view.getBigUint64(0,true));assert.ok(headerLength>0&&headerLength<=4096&&8+headerLength<=bytes.length,"bounded header");const header=JSON.parse(decoder.decode(bytes.subarray(8,8+headerLength)));const payloadBytes=bytes.length-8-headerLength;const tensors=Object.entries(header).filter(([name])=>name!=="__metadata__").map(([name,value])=>({name,start:value.data_offsets[0],end:value.data_offsets[1]})).sort((a,b)=>a.start-b.start);const errors=[];for(let i=0;i<tensors.length;i++){const t=tensors[i];if(!Number.isSafeInteger(t.start)||!Number.isSafeInteger(t.end)||t.start<0||t.end<=t.start||t.end>payloadBytes)errors.push("out-of-bounds:"+t.name);if(i&&t.start<tensors[i-1].end)errors.push("overlap:"+t.name)}return{format:"safetensors",headerLength,payloadBytes,tensors,metadata:header.__metadata__,errors,valid:errors.length===0}};
const makeGguf=()=>{const metadata=encoder.encode(JSON.stringify({architecture:"teaching-transformer",tokenizer:"generated-tokenizer",quantization:"Q4_K_M",tensors:[{name:"embed",offset:0,bytes:32},{name:"head",offset:32,bytes:32}]}));const alignment=32,prefix=16+metadata.length,padding=(alignment-prefix%alignment)%alignment,bytes=new Uint8Array(prefix+padding+64),view=new DataView(bytes.buffer);bytes.set(encoder.encode("GGUF"),0);u32(view,4,3);u32(view,8,alignment);u32(view,12,metadata.length);bytes.set(metadata,16);return bytes};
const parseGguf=bytes=>{const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),magic=decoder.decode(bytes.subarray(0,4)),version=view.getUint32(4,true),alignment=view.getUint32(8,true),metadataLength=view.getUint32(12,true),errors=[];if(magic!=="GGUF")errors.push("bad-magic");if(version!==3)errors.push("bad-version");if(alignment<8||(alignment&(alignment-1)))errors.push("bad-alignment");if(metadataLength>4096||16+metadataLength>bytes.length)errors.push("bad-metadata-length");const metadata=errors.includes("bad-metadata-length")?{}:JSON.parse(decoder.decode(bytes.subarray(16,16+metadataLength)));const payloadStart=Math.ceil((16+metadataLength)/alignment)*alignment,payloadBytes=bytes.length-payloadStart,tensors=(metadata.tensors||[]).map(t=>({name:t.name,start:t.offset,end:t.offset+t.bytes})).sort((a,b)=>a.start-b.start);for(let i=0;i<tensors.length;i++){if(tensors[i].start<0||tensors[i].end>payloadBytes)errors.push("out-of-bounds:"+tensors[i].name);if(i&&tensors[i].start<tensors[i-1].end)errors.push("overlap:"+tensors[i].name)}return{format:"gguf-teaching-subset",magic,version,alignment,metadataLength,payloadStart,payloadBytes,metadata,tensors,errors,valid:errors.length===0}};
const safetensors=parseSafetensors(makeSafetensors([{name:"embed",start:0,end:16},{name:"head",start:16,end:32}])),gguf=parseGguf(makeGguf()),overlap=parseSafetensors(makeSafetensors([{name:"embed",start:0,end:16},{name:"head",start:12,end:32}]));const overflowFull=makeSafetensors([{name:"embed",start:0,end:16},{name:"head",start:16,end:40}]);const overflow=parseSafetensors(overflowFull.subarray(0,overflowFull.length-8));
const receipt={scope:"bounded generated teaching formats, not complete production parsers",safetensors,gguf,mutations:{overlap:overlap.errors,overflow:overflow.errors},auxiliaryOwnership:{safetensors:["config.json","tokenizer.json","generation_config.json"],gguf:["architecture","tokenizer","quantization metadata"]},conversionLineage:{canonical:"generated-safetensors",derived:"generated-gguf-q4",converter:"teaching-converter@1",lossy:true,requiredEvidence:["input digest","output digest","quantization","evaluation receipt"]}};
assert.equal(safetensors.valid,true);assert.equal(gguf.valid,true);assert.ok(overlap.errors.includes("overlap:head"));assert.ok(overflow.errors.includes("out-of-bounds:head"));assert.deepEqual([...encoder.encode(gguf.magic)],[71,71,85,70]);assert.equal(gguf.payloadStart%gguf.alignment,0);console.log(JSON.stringify(receipt,null,2));console.log("PASS: binary fixtures validate headers offsets ownership and lineage");

Match memory mapping to the target executor

Both formats support layouts that can work with memory-mapped access, but format possibility and runtime behavior are separate facts. Page faults, alignment, eager metadata parsing, tensor ordering, decompression, device transfer, and operating-system cache state all affect a real session.

Use the memory-mapped model loading guide to design a workload-specific measurement instead of repeating a generic zero-copy claim. A cold start, warm reopen, partial layer access, and concurrent load can exercise different bottlenecks even when bytes are technically mappable.

This Safetensors vs GGUF fixture checks offsets and declared alignment without timing either parser. That boundary is deliberate: a valid file anatomy can still be a poor match for an executor, while a fast happy-path demo can still accept corrupt or incomplete release metadata.

Memory mapping remains outside this fixture. Alignment and safe offsets are verified, while page faults, cache warmth, device transfer, parser allocation, and executor latency remain measurements for the target runtime rather than conclusions inferred from file anatomy.

Auxiliary ownership mapSafetensors leaves configuration and tokenizer files in a release closure; GGUF metadata keeps selected runtime fields beside tensors.config.jsontokenizer.jsonarchitecture keyquantizationtensor fileGGUF bundleconversion lineage
Safetensors closure
Tensor bytes plus separately digested configuration, tokenizer, and generation assets.
GGUF teaching closure
Tensors plus architecture, tokenizer, and quantization metadata for a chosen executor.
Dashed bridge
A lossy derived artifact with converter and evaluation evidence.
Figure 2: File format does not erase the need to name who owns every release asset.

Reject overlap, overflow, and ambiguous offsets

A defensive parser reads declared lengths with overflow-safe arithmetic, caps metadata before allocation, validates UTF-8 or typed values, and checks every tensor range against the payload. Sort ranges by start, reject overlap, and ensure shape multiplied by dtype width agrees with the declared span where the format contract requires it.

The synthetic corrupt Safetensors-like fixture moves one tensor into another and beyond the payload. The runnable inspector must reject both errors, while the bounded GGUF-like fixture records magic, version, alignment, quantization label, and tensor spans without pretending to implement the complete specification.

Safetensors vs GGUF should include malformed-input policy because model files arrive through caches, downloads, mirrors, and conversion tools. Validation does not replace signing the complete model release graph; it complements authenticity with structural safety.

Two committed corruptions exercise different failures: one tensor overlaps its predecessor, and another declared end extends beyond a deliberately truncated payload. Both errors must survive into the printed receipt instead of collapsing into a generic invalid-file message.

Convert only after choosing a canonical release

Choose which artifact is canonical and which is derived. A training exchange may preserve framework-oriented tensors as the source closure and publish several GGUF variants for local runtimes; a local application may instead treat one tested GGUF build as the shipped unit while retaining upstream weights for reproducibility.

Each conversion record should name input digests, output digest, converter repository and revision, command or parameters, quantization, tensor inventory, warnings, and verification results. Packaging model files as OCI artifacts can keep these related objects and attestations discoverable without collapsing them into one opaque blob.

Safetensors vs GGUF has no universal winner because canonicality follows the release's editing and execution boundary. What matters is that a reviewer can reconstruct which bytes were evaluated and which conversion introduced irreversible change.

Canonicality is a release-policy choice, so the example names its source and derivative without declaring a universal winner. Safetensors vs GGUF should be revisited whenever the converter, quantization family, auxiliary closure, or chosen executor changes.

Use a workload matrix instead of format folklore

For framework exchange, favor the representation understood by the training and transformation tools while keeping code and auxiliary assets explicit. For quantized inference in a compatible llama.cpp-family executor, GGUF often owns more of the ready-to-run metadata; verify the exact architecture, quantization, and feature support in the selected runtime.

For archival use, prefer the closure whose specification, dependencies, provenance, and migration path your team can sustain. For multi-runtime distribution, it may be correct to publish more than one derivative, as long as every file has its own identity and none is mislabeled as bit-equivalent.

The Safetensors vs GGUF decision matrix is a requirements worksheet, not benchmark evidence. Fill rows for tensor editing, quantization, auxiliary ownership, memory mapping, executor support, corruption policy, license, and conversion exit before selecting a format.

The workload matrix separates framework exchange, local quantized inference, conversion fan-out, and archival review. A format earns a row only after the actual consumer, required metadata, modification path, and refusal behavior have been written beside it.

Workload decision ledgerFour release jobs map to canonical artifact, required evidence, and refusal condition.framework exchangequantized inferenceconversion fan-outarchival reviewcanonical bytesrefusal trigger
  1. Exchange: favor portable tensor ownership and a complete auxiliary closure.
  2. Inference: require executor support for metadata and quantization.
  3. Conversion: retain both digests and the lossy edge.
  4. Archive: preserve enough structure to verify without executing a model.
Figure 3: Safetensors vs GGUF becomes answerable only after the release job is named.

Ship the decision with a reproducible receipt

Run the inspector against the committed teaching fixtures, then substitute a small rights-cleared release fixture whose expected tensors and metadata are known. Record parser version, accepted format version, tensor count, byte spans, alignment, rejected mutations, and the chosen canonical-versus-derived relationship.

Preserve the format receipt beside OCI model packaging and the signed release closure. If an executor update changes accepted metadata or a format revision changes invariants, rerun the structural suite before assuming old decisions still hold.

Safetensors vs GGUF becomes actionable when the answer names one release, one target runtime set, one canonical artifact, and one tested conversion policy. The generated receipt proves only its declared structural invariants, which is exactly the evidence this decision needs before broader behavioral testing begins.

The generated synthetic teaching receipt contains the accepted fixtures, both hostile mutations, parser boundary, ownership map, and conversion lineage. Those objects are small enough to inspect manually and deterministic enough to compare in continuous integration.

Audit executor compatibility as a versioned matrix

A format can be structurally valid and still unusable in the selected runtime. Build a matrix whose rows name executor and version, architecture, tensor dtype or quantization, tokenizer ownership, context features, adapter support, device backends, and conversion source; every supported cell should point to a reproducible smoke test rather than a marketing compatibility claim.

Test load rejection as carefully as load success. Missing metadata, unknown quantization, an unsupported architecture key, a mismatched tokenizer, or a stale converter should stop the release with a precise diagnostic instead of falling through to indeterminate output. Keep the structural inspector before the runtime test so corrupt bytes never become an executor experiment.

Safetensors vs GGUF can change when an executor release expands support, even if the format specifications remain stable. Date the matrix, preserve the old decision, and rerun behavioral evaluations after any conversion or runtime update; compatibility means the exact release closure executed under the named version, not that a project once advertised the extension.

Executor compatibility is intentionally a versioned follow-up matrix. This article verifies only a generated teaching subset, so new GGUF keys or Safetensors consumers require fresh fixtures rather than an updated support claim based on extension names.

Review the choice as an operational lifecycle

Distribution introduces mirrors, resumable downloads, caches, partial files, disk pressure, cleanup, and rollback. Publish content length and digest, download to a temporary name, validate before atomic promotion, and ensure eviction treats model, tokenizer, configuration, license, and evaluation receipt as one closure rather than leaving an attractive but incomplete weight file.

Define how long derived formats remain supported and how a user returns to the prior known-good artifact. A conversion pipeline should be rerunnable from the canonical release, yet its output must stay pinned while a deployed version is active; silently rebuilding the same tag with a newer converter destroys reproducibility even if the new bytes look equivalent.

Safetensors vs GGUF is finally an ownership question across authoring, conversion, packaging, runtime, and support. Record who can approve a new format version, who maintains the executor matrix, which corruption cases block promotion, and when the next review occurs so a thoughtful one-time comparison becomes a durable release practice.

The scheduled review checks specifications, converter behavior, artifact hashes, and the linked quality budget. A new updated date is warranted only after those checks run; editorial freshness cannot substitute for rerun release evidence.