Files
nvfp4_gguf_quant/gguf_info.py
T
2026-11-01 16:31:33 -05:00

383 lines
13 KiB
Python

#!/usr/bin/env python3
"""Print a summary of a GGUF file from its header.
The weight bytes are not read. Metadata and the tensor directory are enough
for the architecture, quantization, tokenizer, and tensor list.
"""
from __future__ import annotations
import argparse
import struct
from collections import Counter
from pathlib import Path
# GGML type id -> name, and (values per block, bytes per block).
GGML_TYPES = {
0: ("F32", 1, 4),
1: ("F16", 1, 2),
2: ("Q4_0", 32, 18),
3: ("Q4_1", 32, 20),
6: ("Q5_0", 32, 22),
7: ("Q5_1", 32, 24),
8: ("Q8_0", 32, 34),
9: ("Q8_1", 32, 36),
10: ("Q2_K", 256, 84),
11: ("Q3_K", 256, 110),
12: ("Q4_K", 256, 144),
13: ("Q5_K", 256, 176),
14: ("Q6_K", 256, 210),
15: ("Q8_K", 256, 292),
16: ("IQ2_XXS", 256, 66),
17: ("IQ2_XS", 256, 74),
18: ("IQ3_XXS", 256, 98),
19: ("IQ1_S", 256, 50),
20: ("IQ4_NL", 32, 18),
21: ("IQ3_S", 256, 110),
22: ("IQ2_S", 256, 82),
23: ("IQ4_XS", 256, 136),
24: ("I8", 1, 1),
25: ("I16", 1, 2),
26: ("I32", 1, 4),
27: ("I64", 1, 8),
28: ("F64", 1, 8),
29: ("IQ1_M", 256, 56),
30: ("BF16", 1, 2),
34: ("TQ1_0", 256, 54),
35: ("TQ2_0", 256, 66),
39: ("MXFP4", 32, 17),
40: ("NVFP4", 64, 36),
41: ("Q1_0", 128, 18),
42: ("Q2_0", 64, 18),
}
FILE_TYPES = {
0: "ALL_F32",
1: "MOSTLY_F16",
2: "MOSTLY_Q4_0",
3: "MOSTLY_Q4_1",
7: "MOSTLY_Q8_0",
8: "MOSTLY_Q5_0",
9: "MOSTLY_Q5_1",
10: "MOSTLY_Q2_K",
11: "MOSTLY_Q3_K_S",
12: "MOSTLY_Q3_K_M",
13: "MOSTLY_Q3_K_L",
14: "MOSTLY_Q4_K_S",
15: "MOSTLY_Q4_K_M",
16: "MOSTLY_Q5_K_S",
17: "MOSTLY_Q5_K_M",
18: "MOSTLY_Q6_K",
19: "MOSTLY_IQ2_XXS",
20: "MOSTLY_IQ2_XS",
21: "MOSTLY_Q2_K_S",
22: "MOSTLY_IQ3_XS",
23: "MOSTLY_IQ3_XXS",
24: "MOSTLY_IQ1_S",
25: "MOSTLY_IQ4_NL",
26: "MOSTLY_IQ3_S",
27: "MOSTLY_IQ3_M",
28: "MOSTLY_IQ2_S",
29: "MOSTLY_IQ2_M",
30: "MOSTLY_IQ4_XS",
31: "MOSTLY_IQ1_M",
32: "MOSTLY_BF16",
36: "MOSTLY_TQ1_0",
37: "MOSTLY_TQ2_0",
38: "MOSTLY_MXFP4_MOE",
39: "MOSTLY_NVFP4",
40: "MOSTLY_Q1_0",
41: "MOSTLY_Q2_0",
1024: "GUESSED",
}
_SCALAR = {0: 1, 1: 1, 2: 2, 3: 2, 4: 4, 5: 4, 6: 4, 7: 1, 10: 8, 11: 8, 12: 8}
_INTEGER = {0: "<B", 1: "<b", 2: "<H", 3: "<h", 4: "<I", 5: "<i", 7: "<?", 10: "<Q", 11: "<q"}
_FLOAT = {6: "<f", 12: "<d"}
def format_size(num_bytes: int) -> str:
size = float(num_bytes)
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
if size < 1024 or unit == "TiB":
return f"{int(size)} B" if unit == "B" else f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} TiB"
def _read(handle, n: int) -> bytes:
data = handle.read(n)
if len(data) != n:
raise ValueError("truncated GGUF header")
return data
def _read_string(handle) -> bytes:
size = struct.unpack("<Q", _read(handle, 8))[0]
if size > 64 * 1024 * 1024:
raise ValueError("GGUF string is too long")
return _read(handle, size)
def _read_scalar(handle, value_type: int):
if value_type == 8:
return _read_string(handle).decode("utf-8", "replace")
if value_type not in _SCALAR:
raise ValueError(f"unknown GGUF value type {value_type}")
raw = _read(handle, _SCALAR[value_type])
if value_type in _INTEGER:
return struct.unpack(_INTEGER[value_type], raw)[0]
if value_type in _FLOAT:
return struct.unpack(_FLOAT[value_type], raw)[0]
return None
def _read_array(handle, keep: bool):
subtype = struct.unpack("<I", _read(handle, 4))[0]
count = struct.unpack("<Q", _read(handle, 8))[0]
if subtype in _SCALAR and not keep:
handle.seek(_SCALAR[subtype] * count, 1)
return f"{_type_name(subtype)}[{count}]"
if subtype == 8 and not keep:
for _ in range(count):
_read_string(handle)
return f"string[{count}]"
values = []
limit = 8 if subtype == 8 else 16
for index in range(count):
if subtype == 8:
text = _read_string(handle).decode("utf-8", "replace")
if index < limit:
values.append(text)
continue
if subtype not in _SCALAR:
raise ValueError(f"unsupported GGUF array type {subtype}")
item = _read_scalar(handle, subtype)
if index < limit:
values.append(item)
if count > limit:
values.append(f"... {count} items")
return values
def _type_name(value_type: int) -> str:
names = {
0: "u8", 1: "i8", 2: "u16", 3: "i16", 4: "u32", 5: "i32",
6: "f32", 7: "bool", 8: "string", 10: "u64", 11: "i64", 12: "f64",
}
return names.get(value_type, str(value_type))
def load_gguf(path: Path) -> tuple[dict, list[dict], int]:
metadata: dict = {}
tensors: list[dict] = []
with path.open("rb") as handle:
magic, version = struct.unpack("<II", _read(handle, 8))
if magic != 0x46554747:
raise ValueError(f"{path} is not a GGUF file")
if version < 2:
raise ValueError(f"{path} uses GGUF version {version}, which this reader does not support")
tensor_count, kv_count = struct.unpack("<QQ", _read(handle, 16))
for _ in range(kv_count):
key = _read_string(handle).decode("utf-8", "replace")
value_type = struct.unpack("<I", _read(handle, 4))[0]
if value_type == 9:
keep = "token" not in key and "merges" not in key and not key.endswith(".scores")
metadata[key] = _read_array(handle, keep)
else:
metadata[key] = _read_scalar(handle, value_type)
for _ in range(tensor_count):
name = _read_string(handle).decode("utf-8", "replace")
n_dims = struct.unpack("<I", _read(handle, 4))[0]
dims = list(struct.unpack("<" + "Q" * n_dims, _read(handle, 8 * n_dims))) if n_dims else []
ggml_type = struct.unpack("<I", _read(handle, 4))[0]
offset = struct.unpack("<Q", _read(handle, 8))[0]
tensors.append({"name": name, "dims": dims, "type": ggml_type, "offset": offset})
return metadata, tensors, version
def _meta(metadata: dict, suffix: str):
if suffix in metadata:
return metadata[suffix]
for key, value in metadata.items():
if key.endswith("." + suffix) and ".rope." not in key:
return value
return None
def _context(metadata: dict) -> str:
for key, value in metadata.items():
if key.endswith(".context_length") and ".rope." not in key and isinstance(value, int) and value > 0:
return str(value)
return ""
def _tensor_bytes(tensor: dict) -> int | None:
spec = GGML_TYPES.get(tensor["type"])
if spec is None or not tensor["dims"]:
return None
_name, block, type_size = spec
elements = 1
for dim in tensor["dims"]:
elements *= dim
if elements % block != 0:
return None
return elements // block * type_size
def _preview(value, limit: int = 160) -> str:
if isinstance(value, str):
flat = value.replace("\n", "\\n")
if len(flat) > limit:
return f"{flat[:limit]}... ({len(value)} chars)"
return flat
if isinstance(value, list):
text = ", ".join(_preview(item, 40) for item in value[:12])
if len(value) > 12:
text += f", ... ({len(value)})"
return "[" + text + "]"
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, float):
return f"{value:.6g}"
return str(value)
def _filetype(value) -> str:
if not isinstance(value, int):
return _preview(value)
name = FILE_TYPES.get(value)
return f"{name} ({value})" if name else str(value)
def print_summary(path: Path, metadata: dict, tensors: list[dict], version: int) -> None:
arch = str(metadata.get("general.architecture") or "")
print(f"file {path}")
print(f"size {format_size(path.stat().st_size)}")
print(f"gguf version {version}")
print(f"architecture {arch or '-'}")
for label, key in (
("name", "general.name"),
("basename", "general.basename"),
("size label", "general.size_label"),
("finetune", "general.finetune"),
("type", "general.type"),
):
if key in metadata and metadata[key] not in ("", None):
print(f"{label:<14}{_preview(metadata[key])}")
if "general.file_type" in metadata:
print(f"filetype {_filetype(metadata['general.file_type'])}")
if "general.quantization_version" in metadata:
print(f"quant version {metadata['general.quantization_version']}")
context = _context(metadata)
if context:
print(f"context {context}")
fields = (
("blocks", "block_count"),
("embedding", "embedding_length"),
("feed forward", "feed_forward_length"),
("heads", "attention.head_count"),
("kv heads", "attention.head_count_kv"),
("head dim", "attention.key_length"),
("experts", "expert_count"),
("vocab", "vocab_size"),
("sliding window", "attention.sliding_window"),
)
for label, suffix in fields:
value = _meta(metadata, suffix)
if isinstance(value, int) and value > 0:
print(f"{label:<14}{value}")
swa = _meta(metadata, "attention.sliding_window_pattern")
if isinstance(swa, list) and swa and all(isinstance(item, (bool, int)) for item in swa):
flags = [bool(item) for item in swa]
print(f"swa layers {sum(flags)} sliding, {len(flags) - sum(flags)} full")
tok_model = metadata.get("tokenizer.ggml.model")
if tok_model:
print(f"tokenizer {tok_model}")
for label, key in (
("bos", "tokenizer.ggml.bos_token_id"),
("eos", "tokenizer.ggml.eos_token_id"),
):
if key in metadata:
print(f"{label:<14}{metadata[key]}")
template = metadata.get("tokenizer.chat_template")
if isinstance(template, str) and template:
print(f"chat template yes ({len(template)} chars)")
elif isinstance(template, list) and template:
print(f"chat template {len(template)} templates")
else:
print("chat template no")
if isinstance(metadata.get("general.description"), str):
print(f"description {_preview(metadata['general.description'], 200)}")
counts: Counter[str] = Counter()
known_bytes = 0
unknown = 0
for tensor in tensors:
spec = GGML_TYPES.get(tensor["type"])
counts[spec[0] if spec else f"type {tensor['type']}"] += 1
nbytes = _tensor_bytes(tensor)
if nbytes is None:
unknown += 1
else:
known_bytes += nbytes
print(f"tensors {len(tensors)}")
for name, count in counts.most_common():
print(f" {name:<10} {count}")
print(f"tensor bytes {format_size(known_bytes)}")
if unknown:
print(f"unmeasured {unknown} tensors have an unknown type or shape")
def print_metadata(metadata: dict) -> None:
print()
print("metadata")
width = max(len(key) for key in metadata) if metadata else 8
for key in metadata:
print(f" {key:<{width}} {_preview(metadata[key])}")
def print_tensors(tensors: list[dict]) -> None:
print()
print(f"{'NAME':<48} {'TYPE':<8} {'SHAPE'}")
for tensor in tensors:
spec = GGML_TYPES.get(tensor["type"])
kind = spec[0] if spec else str(tensor["type"])
# File order: the first dimension is the contiguous one.
shape = " x ".join(str(dim) for dim in tensor["dims"]) or "-"
print(f" {tensor['name']:<46} {kind:<8} {shape}")
def inspect(path: Path, metadata_dump: bool, tensor_dump: bool) -> None:
if not path.is_file():
raise SystemExit(f"Not a file: {path}")
try:
metadata, tensors, version = load_gguf(path)
except (OSError, ValueError, struct.error, UnicodeError) as exc:
raise SystemExit(f"{path}: {exc}") from exc
print_summary(path, metadata, tensors, version)
if metadata_dump:
print_metadata(metadata)
if tensor_dump:
print_tensors(tensors)
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Print architecture, quantization, and tensor info from a GGUF header.")
parser.add_argument("gguf", nargs="+", type=Path, help="One or more .gguf files")
parser.add_argument("--metadata", action="store_true", help="Print every metadata key. Long strings and the vocabulary are shortened.")
parser.add_argument("--tensors", action="store_true", help="Print every tensor name, type, and shape.")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> None:
args = parse_args(argv)
for index, path in enumerate(args.gguf):
if index:
print()
inspect(path, args.metadata, args.tensors)
if __name__ == "__main__":
main()