The field guide — one model file opened from its first byte: the header, the metadata, the tensor table, the padding, and how llama.cpp loads it
Inside a GGUF file
exhibit eighty-two The field guide
Published 2026-10-15 (UTC)
A small (human) team and a fleet of AI agents.
Many language models that run on a home machine arrive as a .gguf file, the format llama.cpp reads (and Ollama and LM Studio through it). This page opens one and teaches the format from the bytes up: the first 147 bytes read by hand, the way a reader would find them in a hex viewer; the metadata a file carries about itself and what a runtime does with each key; the table of its weights, and how a shape and a type become a byte count; the padding that lets the file be memory-mapped, and what that map means for load time and memory; llama.cpp's own loading sequence, step by step, from its source; and why this one self-describing file replaced three older formats on 2023-08-21. The receipts come from the 54.8 GB file we benched in October 2026, read with a short program of our own that never touched the weights.
ask about this page → assistant.strata2signal.com · in beta, still being tested
the short version
A GGUF file is a language model packed for programs built on the ggml library, llama.cpp first among them. It describes itself: four bytes that spell GGUF, a version number, a count of tensors (the model's weights, in named blocks) and a count of metadata entries; then the metadata, where the architecture, the context length, the whole tokenizer and the chat template ride inside the file; then a table giving every tensor's name, shape, type and position; then padding to a 32-byte boundary; then the weights. We read the header of the file we benched on [Qwen3.8-Flash-Next](https://research.strata2signal.com/flash-next-on-three-cards/) in October 2026: version 3, 67 metadata entries (10.9 MB of them the tokenizer), 1,223 tensors in 13 different storage types (ten of them compressed) inside a file tagged IQ3_S, 29 bytes of padding, and no draft head. For every one of the 1,223 tensors, the byte count worked out from its shape and its type's block size matched the file's own offsets, and the 29 bytes after the table are the only padding in 54.8 GB. llama.cpp opens such a file in a fixed order (magic, version, counts, keys, table, padding, then the weights by memory map or by copy), and its code at one commit is read beside the file below. llama.cpp merged the format on 2023-08-21, replacing GGML, GGMF and GGJT.
7,070 words, about 32 minutes to read.
The summary is this page’s own; what was dropped, and why, is in this page’s receipt file.
If you're new here: strata→signal is a small workshop (plus a friendly dog with a white patch) that builds on its own machines and writes up what it measures. Every outside fact below was read on 2026-10-14 (UTC) from the source credited beside it: the GGUF specification in the ggml repository (docs/gguf.md at commit 6af560d5, 2026-07-09), llama.cpp's pull requests, its gguf-py constants and its loader source (all at commit efe3626b, 2026-10-12), Hugging Face's GGUF page, and each program's own documentation. Every figure about a file was printed by our own reader, described below. Where we found no source, the claim is not here.
Six words this page leans on. A tensor is one block of a model's weights, with a name and a shape, like blk.0.attn_q.weight. A token is the unit a model reads and writes, a word or a piece of one; the tokenizer is the list of tokens and the rules for cutting text into them. A chat template is the text pattern that wraps a conversation before the model sees it. Quantization is storing weights in fewer bits than they were trained in; the tags (Q4_K_M, IQ3_S) have their own page, The compressed photograph, and this one does not re-explain them. To memory-map a file is to ask the operating system to treat it as if it were already in memory, so a program reads pages of it on demand instead of copying it in.
What a GGUF file is
The specification's first sentence: "GGUF is a file format for storing models for inference with GGML and executors based on GGML." ggml is the tensor library under llama.cpp, the C and C++ engine Georgi Gerganov created on 2023-03-10 (A short history of Ollama tells that part). Models are trained in PyTorch or another framework and converted; the file is what the converter writes.
The spec names three goals: "Single-file deployment: they can be easily distributed and loaded, and do not require any external files for additional information"; "Extensible"; and "mmap compatibility: models can be loaded using mmap for fast loading and saving". The line that explains the rest: "The key difference between GGJT and GGUF is the use of a key-value structure for the hyperparameters (now referred to as metadata), rather than a list of untyped values." Hugging Face's page on the format: "unlike tensor-only file formats like safetensors ... GGUF encodes both the tensors and a standardized set of metadata." A safetensors file is the weights; a GGUF file is the weights and the paperwork.
Four parts, then the weights
The spec writes the file as C structs. In reading order: the header, four bytes that "Must be GGUF at the byte level: 0x47 0x47 0x55 0x46", a 32-bit version ("Must be 3"), a 64-bit count of tensors and a 64-bit count of metadata entries; the metadata, that many pairs of a string key, a type code and a value, in thirteen value types (integers of 8 to 64 bits, two float widths, a boolean, a string, an array of any of those), under dotted keys like general.architecture and tokenizer.ggml.tokens; the tensor table, one entry per tensor with its name, dimensions, type (a number from a fixed list) and offset into the data; then padding to a multiple of the alignment, which is general.alignment when the file carries that key and 32 when it does not ("If the alignment is not specified, assume it is 32"); then the weights, where "The offset of each tensor's data must be a multiple of ALIGNMENT".
Everything a program needs to lay the model out comes before the first weight. A reader can stop there, which is what ours did. The next section finds each of the four parts in the real file's bytes.
The first 147 bytes, by hand
Open the file in a hex viewer (a program that shows a file's bytes as numbers, each byte as two base-16 characters: 47 is the letter G, 00 is zero) and the layout above is there in the first row. Two conventions make it readable. The spec says "Models are little-endian by default", which means a number wider than one byte is written lowest byte first: the four bytes 03 00 00 00 are the number 3, and the eight bytes c7 04 00 00 00 00 00 00 are hexadecimal 04c7, which is 1,223. And every string is written as its length in eight bytes and then its characters, with no terminating zero. Each value's type is a number the spec fixes, 0 to 12: 2 is a 16-bit unsigned integer, 5 a 32-bit signed integer, 8 a string and 9 an array.
Shard 1's first 147 bytes, as our reader printed them; "bytes" counts from zero:
| Bytes | Hex | Field | Reads as |
|---|---|---|---|
| 0–3 | 47 47 55 46 | magic, four characters | GGUF |
| 4–7 | 03 00 00 00 | version, 32-bit | 3 |
| 8–15 | c7 04 00 00 00 00 00 00 | tensor count, 64-bit | 1,223 |
| 16–23 | 43 00 00 00 00 00 00 00 | metadata count, 64-bit | 67 |
| 24–31 | 14 00 00 00 00 00 00 00 | first key's length, 64-bit | 20 |
| 32–51 | 67 65 6e 65 72 61 6c 2e 61 72 63 68 69 74 65 63 74 75 72 65 | the key, 20 characters | general.architecture |
| 52–55 | 08 00 00 00 | value type, 32-bit | 8, a string |
| 56–63 | 08 00 00 00 00 00 00 00 | string length, 64-bit | 8 |
| 64–71 | 71 77 65 6e 34 65 78 70 | the value, 8 characters | qwen4exp |
| 72–79 | 0c 00 00 00 00 00 00 00 | second key's length | 12 |
| 80–91 | 67 65 6e 65 72 61 6c 2e 74 79 70 65 | the key | general.type |
| 92–95 | 08 00 00 00 | value type | 8, a string |
| 96–103 | 05 00 00 00 00 00 00 00 | string length | 5 |
| 104–108 | 6d 6f 64 65 6c | the value | model |
| 109–116 | 16 00 00 00 00 00 00 00 | third key's length | 22 |
| 117–138 | 67 65 6e 65 72 61 6c 2e 73 61 6d 70 6c 69 6e 67 2e 74 6f 70 5f 6b | the key | general.sampling.top_k |
| 139–142 | 05 00 00 00 | value type | 5, a 32-bit signed integer |
| 143–146 | 14 00 00 00 | the value, 32-bit | 20 |
Three things fall out of these 147 bytes. The two counts are 64-bit, like every length in the file. In version 2 (dated below), the spec says, "Most countable values (lengths, etc) were changed from uint32 to uint64 to allow for larger models to be supported in the future." A key is read before its type and the type before its value, so a reader that does not know a key can still step over it: the type code (and, for a string or an array, the length after it) gives the width, and it moves on; that is what "Extensible" costs, eight bytes of length and four of type per entry. And general.architecture is the first pair in this file, and the first key llama.cpp asks for once the header is in: "do I know this model?" is answered before any other setting is read.
A whole header, every byte
Shard 2's header is 173 bytes, small enough to print whole, padding included. It is a GGUF file in miniature: the header, three metadata pairs, one tensor entry and the padding, each part of the layout once.
| Bytes | Hex | Field | Reads as |
|---|---|---|---|
| 0–3 | 47 47 55 46 | magic | GGUF |
| 4–7 | 03 00 00 00 | version | 3 |
| 8–15 | 01 00 00 00 00 00 00 00 | tensor count | 1 |
| 16–23 | 03 00 00 00 00 00 00 00 | metadata count | 3 |
| 24–31 | 08 00 00 00 00 00 00 00 | key length | 8 |
| 32–39 | 73 70 6c 69 74 2e 6e 6f | key | split.no |
| 40–43 | 02 00 00 00 | value type | 2, a 16-bit unsigned integer |
| 44–45 | 01 00 | value, 16-bit | 1 |
| 46–53 | 0b 00 00 00 00 00 00 00 | key length | 11 |
| 54–64 | 73 70 6c 69 74 2e 63 6f 75 6e 74 | key | split.count |
| 65–68 | 02 00 00 00 | value type | 2, a 16-bit unsigned integer |
| 69–70 | 02 00 | value | 2 |
| 71–78 | 13 00 00 00 00 00 00 00 | key length | 19 |
| 79–97 | 73 70 6c 69 74 2e 74 65 6e 73 6f 72 73 2e 63 6f 75 6e 74 | key | split.tensors.count |
| 98–101 | 05 00 00 00 | value type | 5, a 32-bit signed integer |
| 102–105 | c8 04 00 00 | value | 1,224 |
| 106–113 | 1b 00 00 00 00 00 00 00 | tensor name length | 27 |
| 114–140 | 70 65 72 5f 6c 61 79 65 72 5f 74 6f 6b 65 6e 5f 65 6d 62 64 2e 77 65 69 67 68 74 | tensor name | per_layer_token_embd.weight |
| 141–144 | 02 00 00 00 | number of dimensions, 32-bit | 2 |
| 145–152 | a0 00 00 00 00 00 00 00 | dimension 1, 64-bit | 160 |
| 153–160 | 00 d6 12 13 00 00 00 00 | dimension 2 | 320,001,536 |
| 161–164 | 14 00 00 00 | type, 32-bit | 20, IQ4_NL |
| 165–172 | 00 00 00 00 00 00 00 00 | offset into the data, 64-bit | 0 |
| 173–191 | 00 × 19 | padding to the next multiple of 32 | 19 zero bytes |
| 192 | the first byte of the weights | 192 = 6 × 32 |
The 16-bit values take two bytes (01 00 is 1), the 32-bit ones four. The tensor's offset is 0 because offsets count from the first aligned byte after the table, byte 192 here, not from the start of the file: "This offset is relative to tensor_data, not to the start of the file", the spec's comment says.
The keys that matter
The file is the IQ3_S build of Qwen3.8-Flash-Next from ISTA-DASLab's Qwen3.8-Flash-Next-GSQ-RCO-GGUF repository, the one every llama.cpp row on our three-card bench ran on: two shards, 54,817,524,224 and 28,800,138,432 bytes, at the repository revision ed59f920 (last changed 2026-09-29). Our reader is under 200 lines of Python with no libraries, written from the spec; a second short script printed the hex above and did the block arithmetic below. Between them they read shard 1's first 11,024,384 bytes (the header, the table and the 29 bytes of padding) and shard 2's first 192, and nothing after.
Shard 1 carries 67 keys. These are the ones a runtime needs, plus a few it only records, with what llama.cpp does with each, read from its source at commit efe3626b (the file is named in brackets):
| Key | What it means | In our file | What llama.cpp does with it |
|---|---|---|---|
| general.architecture | which model family the file is; every family-specific key below is prefixed with it | qwen4exp | read first [llama-model-loader.cpp]; the name is looked up in llama.cpp's list, and an unknown one stops the load with "unknown model architecture" [llama-model.cpp] |
| general.name | a display name | "Qwen3.8-Flash-Next GSQ-RCO" | printed in the load log [llama-model.cpp]; the tokenizer code also checks it for a few named model families that need special token handling [llama-vocab.cpp] |
| general.file_type | the file's headline compression, a number from a fixed list | 26, which the constants name MOSTLY_IQ3_S | the loader first guesses a type from whichever tensor type is most common (here BF16, 484 of the 1,224 tensors), then lets this key override the guess [llama-model-loader.cpp] |
| general.quantization_version | the version of the quantization scheme; the spec says a file with any quantized tensor must carry it | 2 | not read by the loader or the model code at this commit |
| general.alignment | the boundary tensors are padded to | absent, so 32 | when present it must be a 32-bit unsigned integer and a power of two, or the file is refused [gguf.cpp] |
| qwen4exp.context_length | the context, in tokens, the model was trained on | 262,144 | n_ctx_train |
| qwen4exp.embedding_length | the width of the hidden vector, n_embd | 2,560 | n_embd; the loader computes every tensor's expected shape from numbers like this one and checks the table against it |
| qwen4exp.block_count | the number of repeating layers, blk.0 to blk.47 | 48 | n_layer; the number of layers llama.cpp's -ngl option places on a card is measured against it, plus one for the output layer |
| qwen4exp.attention.head_count, .head_count_kv | attention heads, and the key/value heads they share | 24, 2 | n_head and n_head_kv; when the second is absent it defaults to the first |
| qwen4exp.attention.key_length, .value_length | the width of one head's key and value | 256, 256 | the per-head key and value widths |
| qwen4exp.rope.freq_base | the base frequency of the rotary position encoding | 10,000,000 | rope_freq_base_train; 10,000 when absent |
| qwen4exp.rope.dimension_count | how many of a head's dimensions the rotation touches | 64 | n_rot |
| qwen4exp.expert_count, .expert_used_count | experts per layer, and how many each token is routed to | 512, 10 | n_expert and n_expert_used |
| qwen4exp.expert_feed_forward_length | the width of one expert | 640 | the first dimension of each expert down-projection (640 × 2,560 × 512 below) and the second of the expert gate and up projections (2,560 × 640 × 512) |
| tokenizer.ggml.model | the tokenizer family | gpt2 | selects byte-pair encoding [llama-vocab.cpp]; llama would select the SentencePiece (SPM) type |
| tokenizer.ggml.pre | the pre-tokenizer: how text is split before the merges run | qwen35 | selects the matching rule set; a missing value draws the warning "GENERATION QUALITY WILL BE DEGRADED!" |
| tokenizer.ggml.tokens | every token, by id | 248,320 strings | the vocabulary, built into two lookup tables; "cannot find tokenizer vocab in model file" without it |
| tokenizer.ggml.merges | the byte-pair merge rules, in rank order | 247,587 strings | each split at its first space into a pair and ranked by its position; required for gpt2 ("cannot find tokenizer merges in model file") |
| tokenizer.ggml.token_type | one small integer per token (the spec: 1 normal, 2 unknown, 3 control, 4 user defined, 5 unused, 6 byte) | 248,320 integers | must be at least as long as the token list |
| tokenizer.ggml.bos_token_id, .eos_token_id, .add_bos_token | the start and end markers, and whether to add the start marker | 248,044; 248,046; false | the special token ids, and the add flag when present |
| tokenizer.chat_template | a Jinja template: how a conversation is laid out before the model sees it | 9,993 characters, beginning {%- set image_count = namespace(value=0) %} | handed back whole by llama_model_chat_template [llama-model.cpp]; a chat server fills it in |
| general.sampling.top_k, .top_p, .temp | suggested sampling defaults | 20, 0.95, 1.0 | named in llama.cpp's constants and offered by name through its model API (llama-model.cpp); this page did not trace which tool applies them |
| split.no, split.count, split.tensors.count | which shard this is, how many there are, and how many tensors across all of them | 0, 2, 1,224 | llama.cpp's own keys, not in the spec's list: the loader reads the count from the first shard, builds the sibling file names from the first shard's name, opens each, checks its split.no, and refuses a set whose tensors do not add up ("corrupted model") |
| quantize.imatrix.file, .dataset, .entries_count, .chunks_count | the quantizer's record of the importance-matrix file and the calibration data it used | imatrix-qwen3.8-flash-next.gguf; calib_qwen3.8-flash-next.tokids; 926; 1,000 | a record, not a setting; not in the spec's key list |
Three things to notice. The model card's numbers are in the file: 262,144 is Qwen's "Context Length: 262,144 natively". The tokenizer is not a sidecar: its three arrays take 10,933,145 of the metadata's 10,946,709 bytes, 99.9 % of it, and the whole header with the tensor table is 11.0 MB before a single weight. And the chat template rides inside too, 9,993 characters of Jinja, which is why an engine can serve a chat from this one file and nothing else; Ollama's docs put it as FROM /path/to/file.gguf in a Modelfile.
Shard 2, printed whole above, has the smallest header we have seen: 3 metadata entries, one tensor, 173 bytes of header, 19 of padding, the weights from byte 192. The tensor is per_layer_token_embd.weight, 160 by 320,001,536, type IQ4_NL, 28,800,138,240 bytes: 51,200,245,760 numbers at 4.5 bits each, the "51B n-gram embedding" of Qwen's model card. A shard knows it is a shard.
The tensor table
Shard 1's 1,223 tensors take 77,622 bytes of table, about 63.5 bytes an entry on average. Added up, their numbers come to 125,743,653,760, Qwen's "125B". By type:
| Type | Tensors | Bytes | Share of the weights |
|---|---|---|---|
| IQ4_NL | 86 | 18,445,824,000 | 33.7 % |
| IQ3_XXS | 34 | 10,918,297,600 | 19.9 % |
| IQ2_S | 40 | 10,747,904,000 | 19.6 % |
| IQ3_S | 20 | 7,208,960,000 | 13.2 % |
| Q2_0 | 9 | 2,123,366,400 | 3.9 % |
| Q6_K | 129 | 1,877,836,800 | 3.4 % |
| BF16 | 484 | 1,527,889,920 | 2.8 % |
| IQ4_XS | 45 | 1,398,906,880 | 2.6 % |
| Q5_K | 35 | 346,931,200 | 0.6 % |
| Q4_K | 47 | 198,696,960 | 0.4 % |
| F32 | 292 | 10,063,360 | 0.02 % |
| Q8_0, F16 | 1 each | 1,740,800 and 81,920 | under 0.01 % |
Bytes per tensor are the distance to the next tensor's offset, the last one's the distance to the end of the file; the thirteen types (twelve rows; Q8_0 and F16 share one) sum to the 54,806,499,840 bytes after the padding exactly. Type number 42 is not in the spec's list, which ends at 39; llama.cpp's constants call it Q2_0.
A file tagged IQ3_S holds 13.2 % of its weight bytes in IQ3_S. Its biggest tensor is the output layer, output.weight, in Q6_K (521,472,000 bytes); next come the expert down-projections like blk.0.ffn_down_exps.weight (640 × 2,560 × 512, 471,859,200 bytes), IQ4_NL in 39 of the 48 layers and Q2_0 in the other 9; the 484 BF16 tensors are 16-bit and small, 3.2 MB on average. ISTA-DASLab's card says why: its RCO method "Assigns one of K quantization types to each of N tensors under a total size budget". Over the whole shard that is 3.49 bits a number (bytes × 8 ÷ numbers; the card says 3.50 bpw). The compressed photograph found the same on a 12B: the tag is a recipe's centre, not its contents, and the file is the only place the recipe is written down.
Three entries, byte by byte
The table begins at byte 10,946,733, right after the 67th metadata value. Each entry is a name (its length, then its characters), a 32-bit count of dimensions, that many 64-bit dimensions, a 32-bit type code and a 64-bit offset. Three of shard 1's entries, the first, the third and the eighth, as the reader printed them:
| Bytes | Hex | Field | Reads as |
|---|---|---|---|
| 10,946,733–740 | 0d 00 00 00 00 00 00 00 | entry 1: name length | 13 |
| 10,946,741–753 | 6f 75 74 70 75 74 2e 77 65 69 67 68 74 | name | output.weight |
| 10,946,754–757 | 02 00 00 00 | dimensions | 2 |
| 10,946,758–765 | 00 0a 00 00 00 00 00 00 | dimension 1 | 2,560 |
| 10,946,766–773 | 00 ca 03 00 00 00 00 00 | dimension 2 | 248,320 |
| 10,946,774–777 | 0e 00 00 00 | type | 14, Q6_K |
| 10,946,778–785 | 00 00 00 00 00 00 00 00 | offset | 0 |
| 10,946,847–854 | 15 00 00 00 00 00 00 00 | entry 3: name length | 21 |
| 10,946,855–875 | 6f 75 74 70 75 74 5f 68 63 5f 6e 6f 72 6d 2e 77 65 69 67 68 74 | name | output_hc_norm.weight |
| 10,946,876–879 | 01 00 00 00 | dimensions | 1 |
| 10,946,880–887 | 00 28 00 00 00 00 00 00 | dimension 1 | 10,240 |
| 10,946,888–891 | 00 00 00 00 | type | 0, F32 |
| 10,946,892–899 | 00 08 79 1f 00 00 00 00 | offset | 528,025,600 |
| 10,947,139–146 | 1a 00 00 00 00 00 00 00 | entry 8: name length | 26 |
| 10,947,147–172 | 62 6c 6b 2e 30 2e 66 66 6e 5f 64 6f 77 6e 5f 65 78 70 73 2e 77 65 69 67 68 74 | name | blk.0.ffn_down_exps.weight |
| 10,947,173–176 | 03 00 00 00 | dimensions | 3 |
| 10,947,177–184 | 80 02 00 00 00 00 00 00 | dimension 1 | 640 |
| 10,947,185–192 | 00 0a 00 00 00 00 00 00 | dimension 2 | 2,560 |
| 10,947,193–200 | 00 02 00 00 00 00 00 00 | dimension 3 | 512 |
| 10,947,201–204 | 14 00 00 00 | type | 20, IQ4_NL |
| 10,947,205–212 | 00 e8 cd 35 00 00 00 00 | offset | 902,686,720 |
The first entry is 53 bytes, the third 53, the eighth 74: the table has no fixed row width, which is why a reader must walk it from the start. ggml treats the first dimension as the length of a row: output.weight is 248,320 rows of 2,560 numbers, one row per token in the vocabulary; output_hc_norm.weight is a single row of 10,240; the expert tensor is 512 experts of 2,560 rows of 640.
From a shape to a byte count
Nothing in the entry says how many bytes the tensor takes. That comes from its type. ggml stores quantized weights in fixed-size blocks, and each type declares two numbers in llama.cpp's ggml-common.h: how many weights a block holds, and how many bytes the block takes, scale included. ggml.c lists them in a table (blck_size and type_size), and gguf.cpp turns them into a row's byte stride as it reads the table: the first dimension divided by the block's weights, times the block's bytes. The thirteen types in this file:
| Type | Weights per block | Bytes per block | Bits per weight | Tensors in shard 1 |
|---|---|---|---|---|
| F32 | 1 | 4 | 32 | 292 |
| F16 | 1 | 2 | 16 | 1 |
| BF16 | 1 | 2 | 16 | 484 |
| Q8_0 | 32 | 34 | 8.5 | 1 |
| Q6_K | 256 | 210 | 6.5625 | 129 |
| Q5_K | 256 | 176 | 5.5 | 35 |
| Q4_K | 256 | 144 | 4.5 | 47 |
| IQ4_NL | 32 | 18 | 4.5 | 86 |
| IQ4_XS | 256 | 136 | 4.25 | 45 |
| IQ3_S | 256 | 110 | 3.4375 | 20 |
| IQ3_XXS | 256 | 98 | 3.0625 | 34 |
| IQ2_S | 256 | 82 | 2.5625 | 40 |
| Q2_0 | 64 | 18 | 2.25 | 9 |
Bytes per block are each struct's declared size: a Q6_K block is 128 bytes of low quant bits, 64 of high bits, 16 one-byte scales and a 2-byte scale, 210 in all ("Effectively 6.5625 bits per weight", the header's own comment); an IQ4_NL block is a 2-byte scale and 16 bytes of 4-bit values, 18. In most of these types the fraction above a whole number of bits is the block's scales; in IQ3_S and IQ2_S a quarter of a bit of it is extra high bits of the weights themselves (8 bytes a block). A tag's bits per weight is not the file's: the whole shard runs at 3.49 because its recipe mixes all thirteen.
So, for the three entries above: output.weight is Q6_K, so 2,560 ÷ 256 = 10 blocks a row, × 210 bytes = 2,100 bytes a row, × 248,320 rows = 521,472,000 bytes, which is exactly the next tensor's offset. output_hc_norm.weight is F32: 10,240 × 4 = 40,960 bytes. blk.0.ffn_down_exps.weight is IQ4_NL: 640 ÷ 32 = 20 blocks × 18 bytes = 360 bytes a row, × (2,560 × 512 = 1,310,720) rows = 471,859,200 bytes. Shard 2's one tensor: 160 ÷ 32 = 5 blocks × 18 = 90 bytes a row, × 320,001,536 rows = 28,800,138,240 bytes, the file's size minus 192.
Our second script did this sum for all 1,223 tensors in shard 1 and compared each with the distance to the next offset: 1,223 of 1,223 agreed. Every one of those byte counts is itself a multiple of 32, so the file needs no padding between tensors, and the 29 bytes after the table are the only padding in 54.8 GB. That is also llama.cpp's own check: its reader refuses a tensor whose row is "not a multiple of block size", and refuses a file in which any tensor's offset is not exactly the padded sum of the sizes before it (its log line names the tensor, the offset it found and the offset it expected). So a GGUF file that llama.cpp will open keeps its tensors in table order, back to back; the spec itself asks only that each offset be a multiple of the alignment and the gaps padded.
Twenty-nine bytes, and the memory map
Shard 1's table ends at byte 11,024,355. The next multiple of 32 is 11,024,384 (32 × 344,512), so the file carries 29 bytes there, and every one of the 1,223 offsets after it is a multiple of 32; our reader checked each. Shard 2 pads 19 bytes, from 173 to 192. The spec's rule: "Where required, the file is padded with 0x00 bytes to the next multiple of general.alignment"; our reader read the 29 and the 19, and every one is zero. llama.cpp's reader does the same arithmetic (a seek to the padded position) before recording "this is where the data section starts".
That is the whole mechanism behind "mmap compatibility". To memory-map a file is to ask the operating system to treat it as a stretch of the program's memory. Nothing is copied at that moment; the system hands back an address, and the first time the program touches a page of it (4,096 bytes on our machine) the system reads that page from disk into its cache. A page never touched is never read. A page the system needs room for can be dropped and read again later, because the file still has it. llama.cpp maps a whole file in one call, read-only and shared (mmap(NULL, file->size(), PROT_READ, flags, fd, 0) in its llama-mmap.cpp, with flags set to MAP_SHARED), tells the kernel the reads will be sequential, and when prefetching is on asks for the pages up front (MAP_POPULATE).
Why the 32-byte boundary matters: the map starts on a page boundary, and 4,096 is a multiple of 32, so a tensor that begins at a multiple of 32 in the file begins at a multiple of 32 in memory, where ggml's kernels want it. The engine then needs no copy: tensor number 8 is the mapped address plus 11,024,384 plus 902,686,720, and it computes from there. On shard 1 that map is 13,383,185 pages; the header is the first 2,692 of them, and the first weight begins exactly half-way through the 2,692nd page, at its byte 2,048.
What the map means for the clock and for memory. A mapped load returns fast because little has been read yet, so llama.cpp's source says it keeps timing: "loading time will be recalculated after the first eval, so we take page faults deferred by mmap() into consideration". The pages belong to the system's file cache, not to the program's own allocation, and the mapping can be larger than the memory installed; our three-card page measured it on the 64 GB laptop: "llama.cpp on the laptop mapped about 61.8 GiB against 61.23 reported (a memory map can exceed the memory installed; the system pages the file in and out)". llama.cpp's --load-mode help text lists six choices, among them: "auto: mmap, unless a device does not support it"; "mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)"; "mlock: force system to keep model in RAM rather than swapping or compressing"; and "mmap+mlock". A separate --lazy-mode reads "the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)", on by default "only for tensors larger than 4 GiB"; which tensors qualify is set per architecture in code this page did not read, so whether shard 2's one 28.8 GB tensor is read that way is not said here.
With a card in the machine the map is only half the story. A tensor placed on the card is copied out of the map into the card's memory (ggml_backend_tensor_set); a tensor kept on the processor is used in place, its buffer made from the mapped address itself. After loading, llama.cpp unmaps everything before the first and after the last tensor it kept in place. The input layer always stays on the processor (step 10 below), so even when every other layer went to a card, the stretch of the map that holds the input layer stays mapped, and the system is free to drop the pages outside it.
The alignment came in on 2023-03-30 with the pull request that let llama.cpp memory-map its weights (#613, "Make loading weights 10-100x faster": "changing the file format so we can mmap() weights directly into memory without having to read() or copy them"); the spec's history section calls that aligned format GGJT. GGUF kept the alignment and added the metadata. The format allows a map; the engine chooses. Our bench's llama.cpp rows ran with the map on, which llama.cpp's own log says costs speed (that page notes it), while the Strata engine pins the experts in the computer's own memory.
How llama.cpp loads one, step by step
What follows is read from llama.cpp's source at commit efe3626b (2026-10-12, the latest commit touching its GGUF constants on the day we read it); the file is named at each step. The order is the order of the code.
- Open the file and read the header (ggml/src/gguf.cpp, called from the loader with no_alloc set, so no weight is read). The first four bytes are compared with GGUF: "invalid magic characters: '%c%c%c%c', expected 'GGUF'". Then the version: 0 is a "bad GGUF version"; a version whose low 16 bits are all zero suggests the bytes are the other way round ("is there a mismatch between the host and model endianness?"); 1 is "no longer supported, please use a more up-to-date version"; above 3, the message is that "this software only supports up to version" 3. Then the two counts, each checked against an upper bound so that a table of that many entries can be indexed.
- Read the metadata. For each pair, the key (an empty or duplicate key stops the load), the type, and the value, arrays as a type, a count and the values. Then general.alignment is looked up: absent means 32; present, it must be a 32-bit unsigned integer and a power of two.
- Read the tensor table. For each entry: the name (duplicates refused), the dimension count, the dimensions, the type (it must be in the known range, and the row length "a multiple of block size"), the offset. The row's byte stride is computed here, from the block table above.
- Seek to the data. The reader skips the padding to the alignment and records where the data begins, then walks the table once more, checking that each tensor's offset is exactly the padded sum of the sizes before it. The weights themselves stay unread.
- Name the architecture (src/llama-model-loader.cpp). general.architecture is read and the matching key prefix chosen. An unknown name is not refused yet: it is refused after step 7, when the model object is made (src/llama-model.cpp: "unknown model architecture").
- Open the other shards. split.count from the first file says how many; the sibling names are built from the first file's name (the code's own example: "given <name>-00002-of-00004.gguf, returns list of all 4 splits"); each is opened, its header read the same way, its split.no checked against its place; the tensors from all shards are counted against split.tensors.count, and "corrupted model: %d tensors expected but %d found" ends a short set.
- Log it, and settle the file type. "loaded meta data with %d key-value pairs and %d tensors from %s (version %s)", where 3 prints as "GGUF V3 (latest)". The type with the most tensors becomes the guessed file type, then general.file_type overrides the guess when present.
- Hyperparameters into settings (load_hparams). Context length, embedding length, block count, expert counts, head counts, rope base and the rest, read into the model's settings as the key table above says.
- Build the tokenizer (load_vocab, src/llama-vocab.cpp). tokenizer.ggml.model picks the kind; .pre the rules; .merges are split and ranked; .tokens fill the id-to-text and text-to-id tables; .token_type and the special ids follow.
- Decide where each layer lives (load_tensors). -ngl is the "max. number of layers to store in VRAM, either an exact number, 'auto', or 'all'" (VRAM is the card's own memory), and counts the output layer as one more ("plus 1 for the 'output' layer"). The input layer always stays on the processor ("there is very little benefit to offloading the input layer"); the repeating layers from max(n_layer + 1 − n_gpu_layers, 0) upward go to the cards, divided among several by the --tensor-split proportions; the output layer is placed last. For each tensor the expected shape is computed from the hyperparameters and compared with the table's entry.
- Map or read the files (init_mappings, load_all_data). With the map on, each file is mapped whole; buffers on the processor are made from the mapped address and tensors point into them; tensors bound for a card are copied from the map. With the map off, each tensor is read from the file into its buffer, or streamed to the card through 1 MB pinned staging buffers. The log line "offloaded %d/%d layers to GPU" comes before any tensor is loaded; once the last one is in place, the map's unused ranges are released.
- Done, with the clock still running. The load time is recomputed after the first evaluation, to count the page faults the map deferred.
Steps 1 to 4 are the GGUF format; steps 5 to 12 are what llama.cpp chooses to do with it. The spec fixes the file; it says nothing about which layer goes where.
The draft head that is not there
Our three-card page says llama.cpp ran "with no draft head because the file we benched carries no draft-head block". The file can show what that means.
Qwen's model card lists the model as "125B with 6B activated, plus 51B n-gram embedding and 4B MTP". MTP is multi-token prediction: a small extra network, the draft head, that guesses tokens ahead for the main model to check. llama.cpp names what it would look for: a metadata key qwen4exp.nextn_predict_layers and tensors named like blk.<n>.nextn.eh_proj (its gguf-py constants at commit efe3626b). Our reader searched shard 1's 67 keys and 1,223 tensor names, and shard 2's 3 and 1, for nextn, mtp, draft, eh_proj, enorm and hnorm. Every list came back empty.
A draft head can travel two ways. The spec's naming convention gained an mtp- prefix on 2026-05-21 for "Multi-Token Prediction heads (speculative-decoding draft module, intended to be loaded alongside a base model of matching architecture and version)", adding that "oftentimes the MTP weights can be distributed inside the base model". Neither holds here. ISTA-DASLab's repository at the revision we benched lists no mtp- file (read 2026-10-14); the same model's repository under ggml-org's own account lists three, mtp-Qwen3.8-Flash-Next- in BF16, Q4_0 and Q8_0 (last changed 2026-10-13). llama.cpp's support for this model's draft head merged on 2026-10-01 (#29761, "Qwen4Exp: add MTP"), after the commit our bench pinned; its author wrote in the thread that day that the head "does not share the tok_embd if it's loaded as a separate GGUF". The Strata engine (github.com/Niko1221/Strata; despite the similar name, an unrelated project, with which strata2signal has no affiliation) reads its draft layer from files of its own in an mtp/ folder, per its DETAILS page, which is how it drafted on our bench from the same two shards while llama.cpp could not.
Where it came from
The spec's "Historical State of Affairs" names three formats before it: "GGML (unversioned): baseline format, with no versioning or alignment", "GGMF (versioned): the same as GGML, but with versioning", and "GGJT: Aligns the tensors to allow for use with mmap". Their shared flaw: "There's no way to identify which model architecture a given model is for, because that information isn't present", and "Adding or removing any new hyperparameters is a breaking change, which is impossible for a reader to detect without using heuristics". The hyperparameters were numbers in a fixed order; a new one went on the end and broke every older reader. The byte walk above shows the cure: every value now travels with its name and its type.
The dates, each from the pull request itself:
| Date (UTC) | What happened |
|---|---|
| 2023-03-30 | llama.cpp #613 merged: weights memory-mapped, tensors aligned; the GGJT era |
| 2023-06-25 | ggml #302 opened by philpax: "a complete specification for the proposed GGUF file format, which should generically describe models to be loaded by any compatible executor" |
| 2023-07-26 | llama.cpp #2398, "GGUF", opened by Georgi Gerganov: "extending it with key-value pairs meta information"; "a breaking change, meaning that all existing ggml models will no longer be compatible after merging" |
| 2023-08-21 | #2398 merged: every model to be re-converted |
| 2023-08-27 | #2821 merged, "gguf : add 64-bit support (GGUF v2)", "backward compatible with v1" |
| 2023-10-20 | #3552 merged, "Enable llama.cpp on s390x big endian platform"; the spec calls this version 3, "which introduces big-endian support" |
| 2023-11-01 | ggml #302 merged: the spec lands as docs/gguf.md, where it still lives |
So "August 2023" is the merge; the spec was drafted from June and merged in November, and the version number has not moved since October 2023: the file we opened, converted in 2026, is version 3. The spec says the version "should only be increased for structural changes to the format"; new facts go into metadata keys. That is the design's point: a reader finds general.architecture first and can refuse a model it does not know, which the spec says the old formats could not ("existing programs cannot intelligently fail upon encountering new architectures").
Who reads it
- llama.cpp, the format's home: its README's quick start, after the installer, runs a model in one command, llama cli -hf followed by the name of a GGUF repository on Hugging Face.
- Ollama imports one with FROM /path/to/file.gguf in a Modelfile and "does not quantize GGUF models during import"; its post of 2026-06-05 says version 0.30 brought "GGUF model compatibility through llama.cpp".
- LM Studio's command line filters its downloads with --gguf or --mlx; its front page says it runs "with MLX and llama.cpp under the hood".
- The Strata engine "uses parts of llama.cpp / ggml" (its README); in its low-RAM mode it "maps the model's GGUF files themselves", checking "every tensor's name, type, shape, offset and bounds" first: the tensor table doing its job.
- vLLM read GGUF in its own code from v0.5.5 and moved that into a separately installed plugin in v0.24.0, dated on A short history of vLLM.
- Hugging Face "has a viewer for GGUF files that lets a user check out metadata & tensors info (name, shape, precision)": the same header this page read.
A second file, through Ollama
Models in an Ollama library can be GGUF files under other names, as these two are, and ollama show <model> --verbose prints their headers. Two of ours (Ollama 0.32.14): mistral:v0.1, Mistral 7B, and mistral-small3.2:24b, the model that reads strangers' text at our doors:
| mistral:v0.1 | mistral-small3.2:24b | |
|---|---|---|
| general.architecture, general.file_type | llama, 2 (MOSTLY_Q4_0) | mistral3, 15 (MOSTLY_Q4_K_M) |
| context length; layers | 32,768; 32 | 131,072; 40, plus 24 in a vision tower |
| tokenizer.ggml.model, tokens | llama, 32,000 | gpt2, 131,072 |
| tensors by type | 291: Q4_0 225, F32 65, Q6_K 1 | 585: Q4_K 253, F16 148, F32 131, Q6_K 53 |
Neither listing shows a tokenizer.chat_template, and that says nothing about the files: Ollama's server deletes that key from the metadata it returns (server/routes.go, line 2067 at commit 7a5ae12e, 2026-10-13) and keeps the conversation pattern as a TEMPLATE of its own, "the full prompt template to be passed into the model" in its Modelfile reference; ollama show <model> --template prints it.
What this page does not say
It reads headers; it measures nothing. It does not say which compression is better, or what thirteen types in one file cost in accuracy; The compressed photograph measured one ladder. It does not time a memory-mapped load against a copied one. The block sizes and the loading sequence are llama.cpp's at one commit; the spec fixes the file, not an engine's reading of it, and this page does not say what any other engine does with each key. It opens two files from one bench and two from one library, not the format's thousands. And it names the draft head's absence in one file at one revision; a later revision could carry one.
Who ran this, and thanks
ISTA-DASLab (the two GGUF shards we opened, their GSQ-RCO quantization of Qwen3.8-Flash-Next, under the base model's licence); Qwen (the model and its card, Qwen Community License 1.0); Georgi Gerganov, philpax and the ggml authors (llama.cpp and ggml, MIT; the GGUF specification; the loader source read line by line here); Mistral AI (the two models in our Ollama library, each under the Apache License 2.0 its Ollama listing shows); Ollama (MIT); the Strata engine's author (github.com/Niko1221/Strata; MIT); Hugging Face (the GGUF page and the repository listings); Python's standard library (struct, which read every integer on this page). None of them owed us anything. A small (human) team chose the file, pressed the keys and read the pages; a fleet of AI agents wrote the reader, ran it and drafted the prose.
Read first-hand on 2026-10-14 (UTC):
- The GGUF specification: docs/gguf.md in the ggml repository at commit 6af560d5 (2026-07-09; the mtp sidecar joined its naming convention in #1496, 2026-05-21); ggml #302, the specification's pull request (opened 2023-06-25, merged 2023-11-01)
- llama.cpp pull requests: #613 (merged 2023-03-30), #2398 (opened 2023-07-26, merged 2023-08-21), #2821 (merged 2023-08-27), #3552 (merged 2023-10-20), #29761 (merged 2026-10-01, and its author's comment of the same day)
- llama.cpp source at commit efe3626b (2026-10-12): gguf-py/gguf/constants.py (the type numbers, MOSTLY_IQ3_S = 26, Q2_0 = 42, the nextn names, the general.sampling keys); ggml/src/gguf.cpp and ggml/include/gguf.h (the header reader: magic, version, counts, keys, table, padding, offsets); ggml/src/ggml-common.h and ggml/src/ggml.c (the block structs and the type table); src/llama.cpp, src/llama-model-loader.cpp, src/llama-model.cpp, src/llama-vocab.cpp and src/llama-mmap.cpp (the load order, the splits, the hyperparameters, the tokenizer, the layer placement, the map); common/arg.cpp (the --load-mode, --lazy-mode and -ngl help text); the README, as of 2026-10-14
- Hugging Face: GGUF ("GGUF was developed by @ggerganov"; the viewer; the safetensors sentence); the model page Qwen/Qwen3.8-Flash-Next (the parts and the context length); the repository listings of ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF (revision ed59f920, last changed 2026-09-29; the RCO sentence and the 3.50 in its bpw column) and ggml-org/Qwen3.8-Flash-Next-GGUF (last changed 2026-10-13)
- Ollama: Importing a model and the Modelfile reference; Improved performance and model support with GGUF, 2026-06-05; server/routes.go at commit 7a5ae12e (2026-10-13), line 2067; ollama show --verbose on Ollama 0.32.14, run 2026-10-14
- LM Studio: its lms get reference (lmstudio.ai/docs/cli/get) and its front page (lmstudio.ai), as read 2026-10-14
- The Strata engine: the README and docs/DETAILS.md, as read 2026-10-14
- This workshop's own released pages: Qwen3.8-Flash-Next on three cards ("carries no draft-head block", the memory-map notes), the full bench (the two shards' sizes and hashes), The compressed photograph, A short history of Ollama, A short history of vLLM
Corrections and later measurements will be added below, each dated (UTC), with a window at both ends where one applies and saying in plain words what it counts.