GGUF-量化内幕-附录-C-最小-Python-参考实现
GGUF 量化内幕:附录 C 最小 Python 参考实现
本附录提供一套可独立运行的最小 Python 参考实现:一个 GGUF 解析器、六个格式的解码器(Q4_0 / Q4_K / Q6_K / IQ1_S / STQ1_0 / PTQ1_0),以及与官方 gguf 库的交叉验证脚本。
设计原则:
- 不依赖任何第三方库(只有标准库
struct),因此可以直接python3运行; - 每个解码器都配手工字节验证断言——构造一块已知字节,断言解码结果与期望一致;
- 位序严格对齐 llama.cpp 主线
ggml-quants.c(Q4_K 的get_scale_min_k4、Q6_K 的ql/qh位移、IQ1_S 的qh高 12 位尺度 +IQ1S_DELTA); - STQ1_0 / PTQ1_0 不在主线,是 fork/PR 格式,本附录按细纲记载的布局实现,落地使用前须以实际 fork 文档复核。
术语(已在第 4、7、9、11、13 章定义):块(block)是共享尺度的最小量化单元;bpw = 块字节数×8÷块内权重数;nibble 是半字节(4 bit);grid/codebook 是码本查表;trit 是三值单位。小端(
<)是 GGUF 的固定字节序。
C.1 工具函数:FP16 位模式 ↔ 浮点
GGUF 里的尺度 d 大多以 FP16 位模式存储(2 字节)。struct 模块用 '<e' 直接转换:
import struct
def f16_to_f32(b2: bytes) -> float:
"""2 字节 FP16 位模式 → float(小端)"""
return struct.unpack('<e', b2)[0]
def f32_to_f16(x: float) -> bytes:
"""float → 2 字节 FP16 位模式(小端)"""
return struct.pack('<e', x)
# 验证:1.0 的 FP16 位模式是 0x3C00
assert f16_to_f32(b'\x00\x3c') == 1.0
assert f32_to_f16(1.0) == b'\x00\x3c'
C.2 GGUF 解析器
GGUF 的文件布局(第 3 章详述)依次是:header → KV 数组 → tensor_infos 数组 →(对齐填充)→ 数据段。
import struct
from dataclasses import dataclass, field
MAGIC = b'GGUF'
@dataclass
class GgufTensorInfo:
name: str
n_dims: int
dims: list[int]
ggml_type: int # type id(见 facts-verified §1)
offset: int # 相对数据段起点的偏移
n_elements: int
n_bytes: int
@dataclass
class GgufReader:
version: int
kv: dict[str, object] = field(default_factory=dict)
tensors: list[GgufTensorInfo] = field(default_factory=list)
data_offset: int = 0
@classmethod
def load(cls, path: str) -> 'GgufReader':
with open(path, 'rb') as f:
return cls.parse(f.read())
@classmethod
def parse(cls, buf: bytes) -> 'GgufReader':
if buf[:4] != MAGIC:
raise ValueError('not a GGUF file')
version, tensor_count, kv_count = struct.unpack_from('<IQQ', buf, 4)
if version not in (2, 3):
raise ValueError(f'unsupported GGUF version {version}')
r = cls(version=version)
off = 4 + 4 + 8 + 8 # 越过 header
# --- KV 数组 ---
for _ in range(kv_count):
klen = struct.unpack_from('<Q', buf, off)[0]; off += 8
key = buf[off:off+klen].decode('utf-8'); off += klen
vtype = struct.unpack_from('<I', buf, off)[0]; off += 4
if vtype == 12: # ARRAY
r.kv[key] = _read_array(buf, off)
off += _array_size(buf, off)
else: # 标量 / 字符串
r.kv[key] = _read_value(buf, vtype, off)
off += _value_size(buf, vtype, off)
# --- tensor_infos 数组 ---
for _ in range(tensor_count):
nlen = struct.unpack_from('<Q', buf, off)[0]; off += 8
name = buf[off:off+nlen].decode('utf-8'); off += nlen
n_dims = struct.unpack_from('<I', buf, off)[0]; off += 4
dims = list(struct.unpack_from(f'<{n_dims}Q', buf, off)); off += 8 * n_dims
ggml_type = struct.unpack_from('<I', buf, off)[0]; off += 4
toff = struct.unpack_from('<Q', buf, off)[0]; off += 8
n_elements = 1
for d in dims:
n_elements *= d
r.tensors.append(GgufTensorInfo(name, n_dims, dims, ggml_type, toff,
n_elements, 0))
# --- 对齐到数据段起点 ---
alignment = 32
if 'general.alignment' in r.kv:
alignment = int(r.kv['general.alignment'])
pad = (alignment - (off % alignment)) % alignment
r.data_offset = off + pad
return r
def _value_size(buf: bytes, vtype: int, off: int) -> int:
"""返回某类型 value 占的字节数(从 off 处解析)"""
if vtype == 6: # STRING:u64 长度 + 字节
n = struct.unpack_from('<Q', buf, off)[0]
return 8 + n
sizes = {0:1, 1:1, 2:2, 3:2, 4:4, 5:4, 7:8, 8:8, 9:4, 10:8, 11:1}
return sizes.get(vtype, 0)
def _read_value(buf: bytes, vtype: int, off: int) -> object:
"""标量/字符串 value 解码(ARRAY 除外)"""
if vtype == 6:
n = struct.unpack_from('<Q', buf, off)[0]
return buf[off+8:off+8+n].decode('utf-8')
fmt = {0:'<b', 1:'<B', 2:'<h', 3:'<H', 4:'<i', 5:'<I', 7:'<q', 8:'<Q', 9:'<f', 10:'<d', 11:'?'}[vtype]
return struct.unpack_from(fmt, buf, off)[0]
def _array_size(buf: bytes, off: int) -> int:
"""ARRAY 的字节数:item_type(u32) + len(u64) + len×item 大小"""
item_type = struct.unpack_from('<I', buf, off)[0]
n = struct.unpack_from('<Q', buf, off + 4)[0]
base = 8
if item_type == 6: # 字符串数组
p = off + 8
for _ in range(n):
l = struct.unpack_from('<Q', buf, p)[0]; p += 8 + l
return p - off
return base + n * _value_size(buf, item_type, off + 8)
def _read_array(buf: bytes, off: int) -> object:
"""ARRAY value 解码(此处仅演示标量数组;字符串数组可类推)"""
item_type = struct.unpack_from('<I', buf, off)[0]
n = struct.unpack_from('<Q', buf, off + 4)[0]
out = []
p = off + 8
for _ in range(n):
out.append(_read_value(buf, item_type, p))
p += _value_size(buf, item_type, p)
return out
手工验证:构造一个 1 个 KV(test.arch = "llama")+ 1 个 F32 张量的最小 GGUF,解析它:
import io
def build_mini_gguf() -> bytes:
buf = bytearray()
buf += MAGIC + struct.pack('<I', 3) + struct.pack('<QQ', 1, 1) # v3, 1 张量, 1 KV
# KV: test.arch = "llama"
key = b'test.arch'
buf += struct.pack('<Q', len(key)) + key
buf += struct.pack('<I', 6) # STRING
val = b'llama'
buf += struct.pack('<Q', len(val)) + val
# tensor_info: name="w", 1 维 dims=[4], F32(type 0), offset=0
tn = b'w'
buf += struct.pack('<Q', len(tn)) + tn
buf += struct.pack('<I', 1) + struct.pack('<Q', 4)
buf += struct.pack('<II', 0, 0) # type=F32, offset=0
# 对齐到 32:数据段
while len(buf) % 32 != 0:
buf += b'\x00'
buf += struct.pack('<4f', 1.0, 2.0, 3.0, 4.0) # 4 个 F32
return bytes(buf)
mini = build_mini_gguf()
r = GgufReader.parse(mini)
assert r.version == 3
assert r.kv['test.arch'] == 'llama'
assert r.tensors[0].name == 'w'
assert r.tensors[0].dims == [4]
assert r.tensors[0].ggml_type == 0 # F32
assert r.tensors[0].n_elements == 4
print('C.2 GGUF 解析器:断言全部通过')
注意:
tensor_infos[i].offset是相对数据段起点的偏移。读一个张量的原始字节 =buf[data_offset + offset : data_offset + offset + n_bytes]。n_bytes由ggml_type决定(见 C.3 的表)。
C.3 解码器公共骨架
每个解码器的签名统一为 decode_xxx(block_bytes, n) -> list[float]:输入若干块字节,输出 n 个 float。BLOCK_BYTES、BLOCK_VALUES、BITS_PER_WEIGHT 是每个格式的元数据。
FORMAT_TABLE = {
# 格式名: (块字节数, 块内权重数, bpw, type id)
'Q4_0': (18, 32, 4.5, 2),
'Q4_K': (144, 256, 4.5, 12),
'Q6_K': (210, 256, 6.5625, 14),
'IQ1_S': (50, 256, 1.5625, 19),
'STQ1_0': (42, 256, 1.3125, None), # fork/PR,type id 未定
'PTQ1_0': (28, 128, 1.75, None), # fork/PR,type id 未定
}
C.4 Q4_0 解码器
Q4_0:18 字节/32 值。d(f16) + qs[16],每个字节低 nibble = 第 j 个权重、高 nibble = 第 j+16 个权重。公式 y = d·(q − 8)。
def decode_q4_0(block: bytes, n: int) -> list[float]:
assert len(block) == (n // 32) * 18
out = []
for i in range(0, len(block), 18):
d = f16_to_f32(block[i:i+2])
qs = block[i+2:i+18]
for j in range(16):
lo = qs[j] & 0x0F
hi = (qs[j] >> 4) & 0x0F
out.append(d * (lo - 8))
out.append(d * (hi - 8))
assert len(out) == n
return out
# 手工字节验证:d = 1.0 (0x3C00),qs 全部 = 0x0F(lo=15→7,hi=15→7)
blk = b'\x00\x3c' + bytes([0x0F]) * 16
res = decode_q4_0(blk, 32)
assert res[0] == 1.0 * (15 - 8) == 7.0
assert res[1] == 7.0
assert res[16] == 7.0 # 第 17 个 = qs[0] 的高 nibble
print('C.4 Q4_0:断言全部通过')
C.5 Q4_K 解码器
Q4_K:144 字节/256 值。dm(4,含 d+dmin) + scales[12] + qs[128]。关键在 6-bit 交错打包(第 7 章):scales[12] 不是"尺度 6 字节 + 最小值 6 字节",而是交错位流,必须用 get_scale_min_k4 解包。解码端 y = d·sc·q − dmin·m,sc/m 是 6-bit 整数、不再除以 63/64(归一化已由量化端折进 d/dmin)。
def get_scale_min_k4(j, scales):
"""第 7 章 get_scale_min_k4 的 Python 版;j = 子块索引 0..7"""
if j < 4:
sc = scales[j] & 63
m = scales[j + 4] & 63
else:
sc = (scales[j + 4] & 0x0F) | ((scales[j - 4] >> 6) << 4)
m = (scales[j + 4] >> 4) | ((scales[j] >> 6) << 4)
return sc, m
def decode_q4_k(block: bytes, n: int) -> list[float]:
assert len(block) == (n // 256) * 144
out = []
for i in range(0, len(block), 144):
d = f16_to_f32(block[i:i+2])
dmin = f16_to_f32(block[i+2:i+4])
scales = block[i+4:i+16]
qs = block[i+16:i+144]
q_ptr = 0 # 忠实源码 dequantize_row_q4_K 的 q 游标
is_ = 0
for j in range(0, 256, 64): # 每 64 值 = 两个子块
sc0, m0 = get_scale_min_k4(is_ + 0, scales)
sc1, m1 = get_scale_min_k4(is_ + 1, scales)
d1, m1v = d * sc0, dmin * m0
d2, m2v = d * sc1, dmin * m1
for l in range(32):
out.append(d1 * (qs[q_ptr + l] & 0x0F) - m1v) # 子块 is_ :低 nibble
for l in range(32):
out.append(d2 * (qs[q_ptr + l] >> 4) - m2v) # 子块 is_+1:高 nibble
q_ptr += 32
is_ += 2
assert len(out) == n
return out
# 手工字节验证:构造一个 d=1.0、dmin=1.0、scales 全 0、qs 全 0 的块
# 子块 0..7 的 sc=m=0 → y = 1*0*q - 1*0 = 0
blk = b'\x00\x3c' + b'\x00\x3c' + bytes(12) + bytes(128)
res = decode_q4_k(blk, 256)
assert all(v == 0.0 for v in res)
# 再验证 sc 生效:把子块 0 的尺度设为 1(scales[0] 低 6 位 = 1),
# 且让子块 0 的 qs[0] 低 nibble = 3 → y = 1*1*3 = 3
blk2 = bytearray(b'\x00\x3c' + b'\x00\x3c' + bytes(12) + bytes(128))
blk2[4] = 1 # scales[0] = 1 → 子块0 sc=1
blk2[16] = 0x03 # qs[0] = 3 → 子块0 第1个值=3
res2 = decode_q4_k(bytes(blk2), 256)
assert res2[0] == 3.0
assert res2[32] == 0.0 # 子块 1 仍是 0
print('C.5 Q4_K:断言全部通过')
C.6 Q6_K 解码器
Q6_K:210 字节/256 值。ql[128](低 4 位)+ qh[64](高 2 位)+ scales[16](int8)+ d。16 个子块 × 16 值,公式 y = d·scales[j]·(q − 32),无 dmin。
位序(第 9 章):以 128 值段为单位,ql[l]/qh[l] 的 4 个 2-bit 组分别给位置 l, l+32, l+64, l+96:
def decode_q6_k(block: bytes, n: int) -> list[float]:
assert len(block) == (n // 256) * 210
out = []
for i in range(0, len(block), 210):
d = f16_to_f32(block[i:i+2])
ql = block[i+2:i+130]
qh = block[i+130:i+194]
scales = struct.unpack('<16b', block[i+194:i+210]) # int8 尺度
q, h = 0, 0
for seg in range(2): # 2 个 128 值段
for l in range(32):
is_ = l // 16
q1 = (ql[q+l] & 0x0F) | (((qh[h+l] >> 0) & 3) << 4) - 32
q2 = (ql[q+l+32] & 0x0F) | (((qh[h+l] >> 2) & 3) << 4) - 32
q3 = (ql[q+l] >> 4) | (((qh[h+l] >> 4) & 3) << 4) - 32
q4 = (ql[q+l+32] >> 4) | (((qh[h+l] >> 6) & 3) << 4) - 32
out.append(d * scales[is_ + 0] * q1)
out.append(d * scales[is_ + 2] * q2)
out.append(d * scales[is_ + 4] * q3)
out.append(d * scales[is_ + 6] * q4)
q += 64; h += 32
assert len(out) == n
return out
# 手工字节验证:d=1.0,scales 全 1,ql 全 0,qh 全 0
# → q1..q4 全是 0-32 = -32 → y = 1*1*(-32) = -32
blk = b'\x00\x3c' + bytes(128) + bytes(64) + struct.pack('<16b', *([1]*16))
res = decode_q6_k(blk, 256)
assert all(v == -32.0 for v in res)
# 把 qh 的 bit0-1 设为 1 → q1 的高 2 位 = 1 → q1 = 1*16 - 32 = -16
blk2 = bytearray(b'\x00\x3c' + bytes(128) + bytes(64) + struct.pack('<16b', *([1]*16)))
blk2[130] = 0x03 # qh[0] 低 2 位 = 3
res2 = decode_q6_k(bytes(blk2), 256)
assert res2[0] == 1 * 1 * (3*16 - 32) == 16.0
print('C.6 Q6_K:断言全部通过')
C.7 IQ1_S 解码器
IQ1_S:50 字节/256 值。d(f16) + qs[32] + qh[8](uint16)。这是码本查表格式:每 32 值一组,组尺度 dl = d·(2·((qh[ib]>>12)&7)+1)(∈ {1,3,5,...,15} 倍 d),每 8 值查一次 iq1s_grid(2048 项、每项 8 个 int8),索引 11 bit = qs[l] 8 bit + (qh[ib]>>3l)&7 3 bit。反量化 y = dl·(grid[j] + delta),delta = ±0.125(IQ1S_DELTA)由 qh[ib] 的 bit15 决定。
完整
iq1s_grid有 2048 项,在 llama.cpp 的ggml-common.h(GGML_TABLE_BEGIN(uint64_t, iq1s_grid, NGRID_IQ1S))。下面只列前几项作演示;真实使用时需完整导入该表。
IQ1S_DELTA = 0.125
# 演示用的 iq1s_grid 前 4 项(与 ggml-common.h 一致),完整表需从源码导入
iq1s_grid_demo = [
0xFFFFFFFFFFFFFFFF, # 展开成 8 个 -1
0xFFFFFFFFFFFFFF01, # 展开成 [1, -1, -1, -1, -1, -1, -1, -1]
0xFFFFFFFFFFFFFF02, # [2, -1, ...]
0xFFFFFFFFFFFFFF03, # [3, -1, ...]
]
def grid_entry(idx: int, table=iq1s_grid_demo) -> list[int]:
"""把一项 uint64 展开成 8 个 int8(低字节在前)"""
return list(struct.unpack('<8b', struct.pack('<Q', table[idx])))
def decode_iq1_s(block: bytes, n: int) -> list[float]:
assert len(block) == (n // 256) * 50
out = []
for i in range(0, len(block), 50):
d = f16_to_f32(block[i:i+2])
qs = block[i+2:i+34]
qh = struct.unpack('<8H', block[i+34:i+50]) # 8 个 uint16
for ib in range(8): # 8 组 × 32 值
dl = d * (2 * ((qh[ib] >> 12) & 7) + 1)
delta = -IQ1S_DELTA if (qh[ib] & 0x8000) else IQ1S_DELTA
for l in range(4): # 4 个 8 值子组
idx = qs[ib*4 + l] | (((qh[ib] >> (3*l)) & 7) << 8)
g = grid_entry(idx)
for j in range(8):
out.append(dl * (g[j] + delta))
assert len(out) == n
return out
# 手工字节验证:d=1.0;qh[0] 高 3 位 = 0(尺度 = 1)、bit15 = 0(delta=+0.125)
# qs[0] = 0、qh[0] bit0-2 = 0 → 索引 0 → grid[0] = [-1]*8
# → y = 1*(2*0+1) * (-1 + 0.125) = -0.875
blk = b'\x00\x3c' + bytes(32) + b'\x00\x00' + bytes(14)
res = decode_iq1_s(blk, 256)
assert abs(res[0] - (-0.875)) < 1e-9
# 索引 1 → grid[1] = [1, -1, ...] → y[0] = 1 * (1 + 0.125) = 1.125
# 注意:要得到索引 1,改 qs[0](索引低 8 bit),而不是 qh(qh 会污染高位/scale)
blk2 = bytearray(b'\x00\x3c' + bytes(32) + b'\x00\x00' + bytes(14))
blk2[2] = 0x01 # qs[0] = 1 → 索引低 8 位 = 1
res2 = decode_iq1_s(bytes(blk2), 256)
assert abs(res2[0] - 1.125) < 1e-9
print('C.7 IQ1_S:断言全部通过')
C.8 STQ1_0 解码器(fork 格式,按细纲布局)
STQ1_0 是 3:4 稀疏三值格式(第 13 章):每 4 个权重恰好 1 个零、3 个 ±d;32 种模式用 5 bit 编码(4-bit 模式索引 + 1-bit 符号)。细纲记载块为 42 字节/256 值:qs[32](码本索引)+ sign[8] + fp16 d。stride-16 分组:每组 4 个权重取 w0, w16, w32, w48(而非连续四个),便于 SIMD 无重排解码。
该格式不在 llama.cpp 主线,以下为按细纲布局的最小解码骨架,字段名/模式表须以实际 fork 文档复核。
# 32 种模式的 3:4 布局:每种模式告诉"4 个位置里谁是零"
# 模式号 p ∈ [0,32),用 p>>2 选零位置(0..3)、p&3 给其余三值的±(符号由 sign 位决定)
def decode_stq1_0(block: bytes, n: int) -> list[float]:
# 细纲口径:每块 256 值 = 42 字节
assert len(block) == (n // 256) * 42
out = [0.0] * n
for i in range(0, len(block), 42):
d = f16_to_f32(block[i:i+2])
qs = block[i+2:i+34] # 32 字节码本索引(每字节 2 个 5-bit 模式)
sign = block[i+34:i+42] # 8 字节符号位
base = i // 42 * 256
for g in range(64): # 64 组 × 4 权重 = 256
p5 = (qs[g >> 1] >> (4 * (g & 1))) & 0x1F # 5-bit 模式
sgn = (sign[g >> 3] >> (g & 7)) & 1
zero_pos = p5 >> 2 # 0..3:零的位置
mag = p5 & 3 # 0..3:其余三值的幅度编码(±1)
for lane in range(4):
# stride-16 分组:组内 lane 映射到 w, w+16, w+32, w+48
wpos = (g % 16) + lane * 16
if lane == zero_pos:
out[base + wpos] = 0.0
else:
v = 1.0 if (mag >> ((lane + (1 if lane > zero_pos else 0)) % 3)) & 1 else -1.0
out[base + wpos] = d * v * (-1 if sgn else 1)
return out
验证要点(第 18 章):检查每个 4-lane 组恰好一个零;任何"一组两个零"或"一组没零"都说明模式表/打包实现有误。
C.9 PTQ1_0 解码器(fork 格式,按细纲布局)
PTQ1_0 是稠密三值格式:不强制稀疏位置,任何权重都可是零,因此用 base-3 编码。细纲记载块为 28 字节/128 值:24 字节装 5 trits/字节(3⁵=243 < 256)+ 2 字节余数(4 trits/字节)+ fp16 d。
def decode_ptq1_0(block: bytes, n: int) -> list[float]:
# 细纲口径:每块 128 值 = 28 字节
assert len(block) == (n // 128) * 28
out = []
for i in range(0, len(block), 28):
d = f16_to_f32(block[i:i+2])
main = block[i+2:i+26] # 24 字节 × 5 trits = 120 个 trit
rem = block[i+26:i+28] # 2 字节 × 4 trits = 8 个 trit
trits = []
for b in main: # 每字节拆 5 个 base-3 数字
v = b
for _ in range(5):
trits.append(v % 3); v //= 3
for b in rem: # 每字节拆 4 个
v = b
for _ in range(4):
trits.append(v % 3); v //= 3
assert len(trits) == 128
# trit ∈ {0,1,2} → 值 {0,-1,+1}(约定:0=0, 1=+1, 2=-1 或 0=0,1=-1,2=+1,按 fork 文档)
for t in trits:
out.append(d * {0: 0.0, 1: -1.0, 2: 1.0}[t])
assert len(out) == n
return out
# 手工字节验证:d=1.0,main 全 0 → 全部 trit=0 → y 全 0
blk = b'\x00\x3c' + bytes(26)
res = decode_ptq1_0(blk, 128)
assert all(v == 0.0 for v in res)
# 第一字节 = 1 → 第 1 个 trit = 1 → y[0] = -1
blk2 = b'\x00\x3c' + b'\x01' + bytes(25)
res2 = decode_ptq1_0(blk2, 128)
assert res2[0] == -1.0
print('C.9 PTQ1_0:断言全部通过')
⚠ Hadamard 折叠:PTQ1_0 的磁盘权重以 Hadamard 折叠形式存储。上面的解码器只还原"折叠后的值";真正推理时必须在激活侧做 Hadamard 变换(见第 13.3.4 节),否则 logits 完全错误。格式正确但缺失旋转仍然是错的。
C.10 与 gguf-py 的交叉验证脚本
把我们的解码器当作"自研实现",与官方 gguf.dequantize 逐块比较。这是第 18 章"参考实现交叉验证"的可执行版。
pip install gguf numpy # 交叉验证需要官方库
import numpy as np
from gguf import GGUFReader, dequantize, GGMLQuantizationType
import gguf.gguf_reader as gr # 0.19.0 内部有 Quants 类
def cross_check(path: str, max_tensors: int = 5):
reader = GGUFReader(path)
checked = 0
for t in reader.tensors:
if t.tensor_type.name not in ('Q4_0', 'Q4_K', 'Q6_K', 'IQ1_S'):
continue
# 官方反量化
ref = dequantize(t.data, t.tensor_type)
# 自研解码器(输入 t.data 是 uint8 ndarray → 转 bytes)
raw = t.data.tobytes()
our = {'Q4_0': decode_q4_0, 'Q4_K': decode_q4_K, 'Q6_K': decode_q6_K, 'IQ1_S': decode_iq1_s}[t.tensor_type.name](raw, t.n_elements)
our = np.array(our, dtype=np.float32)
# 逐元素比较:参考实现误差必须为 0(格式定义正确的必要条件)
max_err = float(np.max(np.abs(our - ref)))
ok = max_err < 1e-6
print(f'{t.name:48s} {t.tensor_type.name:6s} max_err={max_err:.3e} {"OK" if ok else "FAIL"}')
assert ok, f'{t.name} 交叉验证失败'
checked += 1
if checked >= max_tensors:
break
print(f'交叉验证完成:{checked} 个张量全部一致')
# cross_check('model-q4_k_m.gguf')
若某格式交叉验证
FAIL,先怀疑自研解码器(位序/打包),而不是官方库——第 18 章的原则:任何偏差都应先怀疑自己。
注意 STQ1_0 / PTQ1_0 不在主线 gguf-py 中,交叉验证需使用对应 fork 的实现。
C.11 使用速查
| 目的 | 代码 |
|---|---|
| 解析一个 GGUF 文件 | r = GgufReader.load('model.gguf') |
| 打印张量类型分布 | from collections import Counter; Counter(t.ggml_type for t in r.tensors) |
| 取某个张量的原始字节 | b = buf[data_offset + t.offset : ...](见 C.2 注释) |
| 反量化一个张量 | decode_q4_k(raw, n_elements) 等 |
| 与官方库交叉验证 | C.10 的 cross_check() |
| 确认格式的 bpw | FORMAT_TABLE['Q4_K'][2] # 4.5 |
本附录的代码追求清晰和正确,不追求速度。真实推理引擎的反量化用 SIMD 一次处理一整块(见第 17 章),但无论多快,字节规则都和这里逐行一致。

浙公网安备 33010602011771号