Find a model's real context window in the GGUF header (no llama.cpp)
Contents
A stdlib-only Python script reads a GGUF file's metadata header and prints the context length the model was trained for, plus its RoPE scaling keys (YaRN factor, original context), so you can set the server's context to match.
- Tested on
- GEEKOM A9 Mega, Fedora 45, Python 3.15.0rc3; GEEKOM AE8, Fedora 44, Python 3.14.8 (2026-10-16). GGUF versions 2 and 3, single-file and split models.
- Needs
- Python 3 on the machine that holds the
.gguffile (or SSH access to it). No llama.cpp, no pip packages, no GPU. - Time
- 5 min.
- Risk
- none. The script opens the file read-only and reads only the header; the model file is not modified (checked in Verify).
Before you start#
- Python 3 is installed where the model lives:
$ ssh a9 'python3 --version; cat /etc/fedora-release' Python 3.15.0rc3 Fedora release 45 (Forty Five) - Placeholders used below:
<FILE.gguf>— path to the model file. For a split model, the shard named…-00001-of-<N>.gguf.<HOST>— SSH host that stores the model, if it is not the machine you type on.<SERVER_LOG>— your inference server's log file.<ORG>/<MODEL>— the upstream Hugging Face repo of the original (non-GGUF) model, for the optional cross-check.
- Backups: none needed. Nothing is written.
Steps#
Save the script as
ggufctx.py.#!/usr/bin/env python3 """Print a GGUF file's trained context length and RoPE scaling keys. Reads only the metadata header (no tensor data), stdlib only, never writes. Usage: python3 -I ggufctx.py <FILE.gguf> """ import struct import sys # GGUF metadata value types (ggml docs/gguf.md, enum gguf_metadata_value_type) FIXED = {0: "<B", 1: "<b", 2: "<H", 3: "<h", 4: "<I", 5: "<i", 6: "<f", 7: "<?", 10: "<Q", 11: "<q", 12: "<d"} STRING, ARRAY = 8, 9 def wanted(key): return (key in ("general.architecture", "general.name") or key.endswith(".context_length") or ".rope.scaling." in key or key.startswith("split.")) def main(path): with open(path, "rb") as f: def read(n): b = f.read(n) if len(b) != n: sys.exit(f"{path}: header ends early (truncated file?)") return b def num(fmt): return struct.unpack(fmt, read(struct.calcsize(fmt)))[0] def string(): return read(num("<Q")).decode("utf-8", "replace") def value(t): if t == STRING: return string() if t == ARRAY: et, n = num("<I"), num("<Q") if et in FIXED: read(n * struct.calcsize(FIXED[et])) # skip, works on pipes too else: for _ in range(n): value(et) return f"<array, {n} items>" if t not in FIXED: sys.exit(f"{path}: unknown value type {t}") return num(FIXED[t]) if read(4) != b"GGUF": sys.exit(f"{path}: not a GGUF file (first 4 bytes are not 'GGUF')") version, n_tensors, n_kv = num("<I"), num("<Q"), num("<Q") print(f"gguf version {version}, {n_tensors} tensors, {n_kv} metadata keys") for _ in range(n_kv): key = string() v = value(num("<I")) if wanted(key): print(key, v) if __name__ == "__main__": if len(sys.argv) != 2: sys.exit("usage: python3 -I ggufctx.py <FILE.gguf>") main(sys.argv[1])Check that it parses:
$ python3 -I ggufctx.py usage: python3 -I ggufctx.py <FILE.gguf>Expected: the usage line (exit code 1). A
SyntaxErrormeans the paste lost indentation.Run it on the model file. On the machine that has the file:
python3 -I ggufctx.py <FILE.gguf>$ python3 -I ggufctx.py ~/models/llama-2-7b.Q4_0.gguf gguf version 2, 291 tensors, 19 metadata keys general.architecture llama general.name LLaMA v2 llama.context_length 4096If the file is on another box, pipe the script over SSH instead of copying it there (
-makes Python read the script from stdin; the file path is the remote one):ssh <HOST> 'python3 -I - <FILE.gguf>' < ggufctx.py$ ssh a9 'python3 -I - ~/models/Laguna-XS-2.1-GGUF/Laguna-XS-2.1-Q8_0.gguf' < ggufctx.py gguf version 3, 678 tensors, 61 metadata keys general.architecture laguna general.name Laguna XS 2.1 laguna.context_length 262144 laguna.rope.scaling.type yarn laguna.rope.scaling.factor 32.0 laguna.rope.scaling.original_context_length 8192 laguna.rope.scaling.yarn_attn_factor 1.3465735912322998 laguna.rope.scaling.yarn_beta_fast 64.0 laguna.rope.scaling.yarn_beta_slow 1.0Expected: a
gguf versionline, thengeneral.architectureand one<arch>.context_lengthline. On the 35.6 GB Laguna file the run took 0.070 s wall time, because only the header is read. Errors: see Failure modes, rows 1-4.For a split model, run it on shard 1. Only the first shard carries the model metadata:
$ ssh a9 'python3 -I - ~/models/Laguna-S-2.1-GGUF/Laguna-S-2.1-UD-Q5_K_XL-00001-of-00003.gguf' < ggufctx.py gguf version 3, 0 tensors, 72 metadata keys general.architecture laguna general.name Laguna-S-2.1 laguna.context_length 262144 laguna.rope.scaling.type yarn laguna.rope.scaling.factor 32.0 laguna.rope.scaling.original_context_length 8192 laguna.rope.scaling.yarn_attn_factor 1.0 laguna.rope.scaling.yarn_beta_fast 32.0 laguna.rope.scaling.yarn_beta_slow 1.0 split.no 0 split.tensors.count 814 split.count 3Expected:
split.no 0(numbering starts at 0) and thecontext_lengthline. In this file shard 1 holds no tensors at all, only metadata. If you see onlysplit.*lines, you ran a later shard: see Failure modes, row 5.Read the numbers.
<arch>.context_lengthis the model's context window. The GGUF spec defines it as the "length of the context (in tokens) that the model was trained on". In both files cross-checked here (Laguna-XS, Qwen3-30B-A3B) it equalsmax_position_embeddingsin the original model'sconfig.json. That is usually the ceiling to serve at, but a model card can state a smaller native window (Qwen3 example below), so read the card too (Verify).<arch>.rope.scaling.type yarnwithfactorandoriginal_context_lengthmeans the window was reached by YaRN-extending a shorter base: Laguna-XS is 32.0 × 8192 = 262144. In all seven YaRN files on this box (factors 16 to 128: DeepSeek-V4-Flash, Mistral-Small-4, three gpt-oss, two Laguna),factor × original_context_lengthequalscontext_length; gpt-oss-120b is 32.0 × 4096 = 131072:$ ssh a9 'python3 -I - ~/models/gpt-oss-120b-MXFP4.gguf' < ggufctx.py gguf version 3, 687 tensors, 36 metadata keys general.architecture gpt-oss general.name gpt-oss-120b gpt-oss.context_length 131072 gpt-oss.rope.scaling.type yarn gpt-oss.rope.scaling.factor 32.0 gpt-oss.rope.scaling.original_context_length 4096 gpt-oss.rope.scaling.yarn_beta_fast 32.0 gpt-oss.rope.scaling.yarn_beta_slow 1.0- No
rope.scalinglines: the file stores no scaling.context_lengthis then whatever the config declares, which is not always the native window the card names:The Qwen3-30B-A3B card says "32,768 natively" and explains that$ ssh a9 'python3 -I - ~/models/Qwen3-30B-A3B-Q4_K_M.gguf' < ggufctx.py gguf version 3, 579 tensors, 37 metadata keys general.architecture qwen3moe general.name Qwen3 30B A3B qwen3moe.context_length 40960max_position_embeddings= 40,960 reserves 32,768 tokens for output plus 8,192 for prompts; itsconfig.jsonhasmax_position_embeddings40960 andrope_scalingnull. - The
yarn_*keys are inputs your inference engine has to read. A build that ignores them runs the model with its own defaults (see Notes, llama.cpp #30331).
Compare with the context your server actually uses. The configured value is not proof; read the number the server prints at load. With llama-server (
-c/--ctx-size;0means "loaded from model"), it is then_ctx_slotline in the server log:$ ssh a9 'grep -H n_ctx_slot ~/bench-archive/serve/laguna-xs-q8-vulkan-new-2026101*.log' /home/user/bench-archive/serve/laguna-xs-q8-vulkan-new-20261016T050201.log:0.09.628.876 I srv load_model: initializing, n_slots = 1, n_ctx_slot = 131072, kv_unified = 'false' /home/user/bench-archive/serve/laguna-xs-q8-vulkan-new-20261016T143358.log:0.10.328.547 I srv load_model: initializing, n_slots = 1, n_ctx_slot = 262144, kv_unified = 'false'For your own server:
grep n_ctx_slot <SERVER_LOG>. Expected: the served value equalscontext_lengthfrom step 2. The first log above is a server started at 131072 for a model whose header says 262144: half the trained window. The second is the same model restarted at 262144. A lower served value is legitimate only when you chose it (memory budget); otherwise raise-c(or your engine's equivalent) tocontext_length.
Verify#
- The script did not touch the model file (modification time and size before and after a run):
$ ssh a9 'stat -c "%y %s" ~/models/Laguna-XS-2.1-GGUF/Laguna-XS-2.1-Q8_0.gguf' && ssh a9 'python3 -I - ~/models/Laguna-XS-2.1-GGUF/Laguna-XS-2.1-Q8_0.gguf > /dev/null' < ggufctx.py && ssh a9 'stat -c "%y %s" ~/models/Laguna-XS-2.1-GGUF/Laguna-XS-2.1-Q8_0.gguf' 2026-10-11 02:48:44.038754178 +0300 35597116800 2026-10-11 02:48:44.038754178 +0300 35597116800 - Optional cross-check against the original model's
config.json(max_position_embeddingsand the rope block should agree with the header):curl -sL https://huggingface.co/<ORG>/<MODEL>/resolve/main/config.json | python3 -I -c 'import json,sys; c=json.load(sys.stdin); print("max_position_embeddings", c.get("max_position_embeddings")); print("rope", c.get("rope_scaling") or c.get("rope_parameters"))'Expected: same context, factor, original length and betas as the GGUF. Also read the model card's context section: it can name a native window below$ curl -sL https://huggingface.co/poolside/Laguna-XS-2.1/resolve/main/config.json | python3 -I -c 'import json,sys; c=json.load(sys.stdin); print("max_position_embeddings", c.get("max_position_embeddings")); print("rope", c.get("rope_scaling") or c.get("rope_parameters"))' max_position_embeddings 262144 rope {'full_attention': {'rope_theta': 500000.0, 'rope_type': 'yarn', 'factor': 32.0, 'original_max_position_embeddings': 8192, 'beta_slow': 1.0, 'beta_fast': 64.0, 'attention_factor': 1.3465735902799727, 'partial_rotary_factor': 0.5}, 'sliding_attention': {'rope_type': 'default', 'rope_theta': 10000.0, 'partial_rotary_factor': 1.0}}max_position_embeddings(Qwen3-30B-A3B, step 4).attention_factordiffers in the 9th decimal (1.3465735902799727 vs 1.3465735912322998) because GGUF stores it as float32. Gated repos need a Hugging Face token; this one did not.
Undo / rollback#
- Nothing to undo: the script only reads (Verify shows the file's mtime and size unchanged). Remove the script with
rm ggufctx.py.
Failure modes#
| Symptom (exact message) | Cause | Fix |
|---|---|---|
<FILE>: not a GGUF file (first 4 bytes are not 'GGUF') | The path is not a GGUF (seen on a MANIFEST.jsonl next to the model). | Point it at the .gguf file. |
FileNotFoundError: [Errno 2] No such file or directory: '<FILE>' (after a traceback) | Wrong path. Over SSH the path is resolved on <HOST>, not locally. | Check the path with ls on the box that holds the file. |
Some keys print, then <FILE>: header ends early (truncated file?) | The file ends inside the header: a partial download (reproduced by feeding the script the first 4096 bytes of the Laguna file). | Compare the file size with the source's size; re-download. |
usage: python3 -I ggufctx.py <FILE.gguf> | No file argument (over SSH: the path is missing after -). | Pass exactly one path. |
Only split.no 1, split.tensors.count …, split.count … lines; no context_length | You ran shard 2 or later of a split model; those shards carry 3 keys only. | Run it on …-00001-of-<N>.gguf (step 3). |
Server: error: request (132149 tokens) exceeds the available context size (131072 tokens), try increasing it | The server was started below the trained window (131072 served, 262144 in the header). | Restart with the context set to context_length if memory allows (step 5). |
Field notes#
- 2026-10-16, a9-mega (GEEKOM A9 Mega, Fedora 45, Python 3.15.0rc3; models in
~/models):- The A9 model benchmark had served Laguna-XS-2.1 Q8_0 at 131072 since its intake screen on 2026-10-11, the default for every screen there. On the 2026-10-16 arm (llama.cpp master, Vulkan) two test runs stopped on the server's "exceeds the available context size (131072 tokens)" error, at 132149 and 131338 requested tokens. Reading the header showed
context_length 262144(YaRN 32 × 8192), so the server had been running the model at half its trained window. - A filter on keys ending in
context_lengthalone shows 262144 andoriginal_context_length 8192but hides the scaling type and factor; a filter oncontext/ropeshows the full YaRN block. The script above printsrope.scaling.*for that reason. - Skipping arrays with
seek()fails withio.UnsupportedOperation: File or stream is not seekable.when given a process substitution (<(head -c 4096 …)). Skipping by reading fixed that; the truncated input now ends with "header ends early". - The two cells were re-run at 262144 the same afternoon (server log at 14:33 shows
n_ctx_slot = 262144). Both finished: t14 PASS at 99 points, t15 FAIL at 92 points on its code, not on the cap. - Split check on Laguna-S-2.1 UD-Q5_K_XL: shard 1 has 0 tensors and 72 keys, shard 2 has 468 tensors and 3 keys.
- The A9 model benchmark had served Laguna-XS-2.1 Q8_0 at 131072 since its intake screen on 2026-10-11, the default for every screen there. On the 2026-10-16 arm (llama.cpp master, Vulkan) two test runs stopped on the server's "exceeds the available context size (131072 tokens)" error, at 132149 and 131338 requested tokens. Reading the header showed
- 2026-10-16, AE8 (GEEKOM AE8, Fedora 44, Python 3.14.8): local run on
llama-2-7b.Q4_0.gguf(GGUF version 2):llama.context_length 4096, no scaling keys.
Notes#
context_lengthis what the model was trained for. Serving above it needs extra RoPE scaling set on the server, which is outside this manual. Absentrope.scalingkeys do not forbid that: the same benchmark ran Qwen3-30B-A3B (header 40960, no scaling keys) at 128k with YaRN x4 set by the server configuration, not by the file.- llama.cpp before release b11598 (2026-10-13) did not load Laguna's
yarn_beta_fast/yarn_beta_slowkeys; #30331 ("model: load YaRN beta metadata for laguna models") added that. Laguna-XS stores 64.0 foryarn_beta_fast. If a header carriesyarn_*keys, check that your engine version reads them for that architecture. - llama.cpp master defaults
n_ctxto 0 ("context the model was trained with"), but--fit(on by default) may lower an unset context to fit device memory, down to--fit-ctx(default 4096). An explicit-c 0also switches that reduction off (it sets the fit minimum to the maximum), so-c 0or-c <context_length>both pin the trained window. Either way, read then_ctx_slotline. [UNVERIFIED: read from source (common/arg.cpp), not observed on the A9.] - The script assumes little-endian files, the GGUF default. The spec says a big-endian file cannot be detected from the header; read as little-endian, its version 3 would print as 50331648. [UNVERIFIED: no big-endian file tested.]
- Alternative:
gguf-dumpfrom theggufPython package prints the whole header. [UNVERIFIED: not run here; needs pip.]
Filed under: local-llm, llm, llama.cpp, python, gguf
Sources#
- GGUF spec, ggml
docs/gguf.md— header layout, value-type enum (the script's type table),[llm].context_lengthandrope.scaling.*definitions, endianness note. - llama.cpp
common/arg.cppandcommon/common.h—-c/--ctx-size("0 = loaded from model"),--fit,--fit-ctxdefaults (master, read 2026-10-16). - llama.cpp #30331 — load YaRN beta metadata for Laguna; shipped in release b11598.
- Qwen/Qwen3-30B-A3B model card and
config.json— "32,768 natively";max_position_embeddings40,960 = output reserve + prompt. - poolside/Laguna-XS-2.1
config.json— upstream context and YaRN parameters used in Verify; GGUF from bartowski/Laguna-XS-2.1-GGUF.