← 返回 Skill 列表
extension
分类: 开发与工程API Key 暂未确认

bonsai三元GGUF模型转ninfer模型01

1格式说明: 容器排布、PQ2_0_G128/PTQ1_0_G128 几何、34/28 字节块、row-split 平面、perm48/perm_row等内容

person作者: montherlandhubModelScope

ninfer 三元制品:容器格式与几何

本技能是格式层的权威速查:文件怎么排、平面在哪、置换怎么算。改打包器或写验收脚本 前先读这里,不要凭直觉推偏移。

源码位置:src\pack.py、src\tools\artifact\、src\MAPPING.json。 (dist\ 是 src\ 的运行副本,由 install.cmd 生成,改代码只改 src\。)


1. .ninfer 容器(tools/artifact/container.py)

offset 0            : MAGIC = b"NINFER\x00\x02"        8 字节
offset 8            : json_bytes  (uint64 LE)          8 字节
offset 16           : JSON 目录,json_bytes 字节,零空白紧凑 UTF-8
                     payload_offset = align_up(16 + json_bytes, 4096)
payload_offset + obj.offset : 对象负载字节

关键点:目录里的 offset 是 payload 相对的,不是文件相对的。Artifact.payload() 内部 才加上 payload_offset。

对齐常数:PREFIX_BYTES = 16、PAYLOAD_ALIGNMENT = 4096、 PLANE_ALIGNMENT = 256、K_ALIGNMENT = 128。四种 layout 的对象对齐都是 256; resource(raw-bytes-v1)对齐是 1。

目录 JSON 的成员集合是精确相等,多一个少一个都拒绝

| 位置 | 必须恰好是 | |---|---| | 根 | identity, objects | | identity | model_id, weights_id(都非空) | | tensor 条目 | name, kind, shape, format, layout, offset, bytes | | resource 条目 | name, kind, encoding, offset, bytes |

  • kind 只能是 "tensor" 或 "resource";resource 的 encoding 只能是 raw-bytes-v1。
  • bytes 必须恰好等于 encoded_size(layout, format, shape),否则报 tensor <name> stores N bytes; layout requires M。
  • _validate_ranges() 按顺序逐个强制:offset >= cursor(不许重叠/乱序)、 offset % alignment == 0、offset + bytes <= payload_bytes。
  • plan_objects() 在计划阶段就拒绝重名:duplicate object name: <name>。

2. 三元数字格式(tools/artifact/numeric.py)

PQ2_0_G128  = TernaryFormat("PQ2_0_G128",  128, 32, 0)   # (name, group_size, base_bytes, high_bytes)
PTQ1_0_G128 = TernaryFormat("PTQ1_0_G128", 128, 24, 2)

⚠️ 这两个注册是本地重建的,不在上游 ninfer 里。 发布包只发了 pack.py(它会用到这两个 名字)和 artifact 层的 C++ 实现,没发 Python 侧的注册。所以在干净的 ninfer 树上 get_format("PQ2_0_G128") 会抛 unknown numeric format。 必须用 ninfer-ternary-bonsai-ada 那份 tools\artifact,不能用 ninfer-4090-windows 那棵树的 numeric.py——它的副本会在 row_split_geometry 处抛 ValueError: unknown numeric format: 'PQ2_0_G128'。

为什么 QuantFormat 表达不了:row_split_geometry 对 bits != 8 一律推 base_bytes_per_group = group_size // 2 = 64,而 PQ2_0 实际每组 32 字节 base、 PTQ1_0 每组 24 字节 base + 2 字节 high。所以走独立分支。

NUMERIC_FORMATS 共 12 项(3 direct + 4 quant + 1 nvfp4 + 1 fp8-row + 2 ternary)。


3. row-split 平面几何

row_split_geometry(format, shape) -> RowSplitGeometry   # 16 个字段

shape 必须 rank 2 且维度为正整数。推导:

k_pad              = align_up(k, 128)
groups_per_row     = k_pad // 128
base_row_bytes     = groups_per_row * base_bytes_per_group
high_row_bytes     = groups_per_row * high_bytes_per_group
scale_row_bytes    = groups_per_row * 2          # 每组一个 binary16,恒定
base_offset        = 0
high_offset        = align_up(base_bytes, 256)
scale_offset       = high_offset + align_up(high_bytes, 256)
payload_bytes      = scale_offset + scale_bytes

RowSplitGeometry 字段顺序: n, k, k_pad, groups_per_row, base_bytes_per_group, high_bytes_per_group, base_row_bytes, high_row_bytes, scale_row_bytes, base_offset, base_bytes, high_offset, high_bytes, scale_offset, scale_bytes, payload_bytes

high_offset 即使 high_bytes == 0 也会算,所以 PQ2_0 仍有一个(空的)high 平面位置, scale_offset == high_offset。

每格式的 scale 平面位置 —— 这是最常被硬编码搞错的地方

| | 每组 base | 每组 high | 每组 scale | 块大小 | scale 起点 | |---|---|---|---|---|---| | PQ2_0_G128 | 32 | 0 | 2 | 34 | rows * groups * 32 | | PTQ1_0_G128 | 24 | 2 | 2 | 28 | rows * groups * 26 |

实例子尺寸(与 docs/03-三元模型转-NInfer.md §1.1 记录的一致):

| shape | 格式 | payload_bytes | |---|---|---| | (248320, 5120) 词表 | PQ2_0 | 337,715,200 | | (248320, 5120) 词表 | PTQ1_0 | 278,118,400 | | (12288, 5120) value_z | PQ2_0 / PTQ1_0 | n*1360 / n*1088 | | (7168, 5120) query_key | PQ2_0 / PTQ1_0 | n*1360 / n*1088 |

PQ2_0 是 34 B / 128 权重 = 2.125 bit/权重。

平面顺序 ≠ 块内字节顺序

这是最容易反直觉的一点:artifact 侧的平面顺序和 ggml 侧的块内顺序不是一回事。

PQ2_0  34 字节块 : [ scale 2 ][ base/qs 32 ]          scale 在【前】
PTQ1_0 28 字节块 : [ qs 24 ][ qh 2 ][ scale 2 ]        scale 在【后】

推论:验收工具绝不能硬编码 scale 偏移。 必须从 row_split_geometry() 取。 把 PTQ1_0 的 scale 当成在 rows*groups*base_bytes(即落在 high 平面里)读,实测在一份 健康产物上给出 neg=118596 nan=8552 median=1.3496——数字看着像那么回事,其实读的是 错地方。正确的 PTQ1_0 偏移读出来是 neg=0 nan=0 median=0.0168。

PQ2_0 上 groups_per_row = 40(k=5120),块流是 [scale][base]... 逐组交替; PTQ1_0 上同样是 40 组,块流是 [base][high][scale]...。k=6144 时 groups_per_row = 48。


4. 块 ↔ 平面 互转(pack.py 补丁 3 引入的唯一命名处)

GGML_GROUP = 128
GGML_BLOCK = {"PTQ1_0_G128": 28, "PQ2_0_G128": 34}
BLOCK_PLANE_SLICES = {
    "PTQ1_0_G128": ((0, 24), (24, 26), (26, 28)),   # base, high, scale
    "PQ2_0_G128":  ((2, 34), (None, None), (0, 2)),  # base, 无 high, scale
}
  • GGML_GROUP = 128,每组权重数。消费者用它算 gpr = cols // 128。
  • GGML_BLOCK = 含 scale 的单组块字节数。Gguf.blocks() 独立地推 28 if tt == T_PTQ1_0 else 34,_check_geometry_constants.py 断言两者一致。
  • BLOCK_PLANE_SLICES = 每格式三个 (start, stop) 字节区间,顺序是平面顺序 (base, high, scale),不是字节顺序——PQ2_0 的 scale 区间是 (0,2) 但排在第三位。 None 表示该平面不存在。

verify\_check_geometry_constants.py 就是对着这三处做断言:切片能复现 block_to_planes、 长度之和等于块大小、planes_to_block 是精确逆、以及 k ∈ {5120, 6144, 17408} 时 base_bytes_per_group/high_bytes_per_group 与切片长度一致。它没有 main(), 断言在 import 时执行——想手动跑就直接 python 这个文件,别从别的模块 import 它。


5. 行置换:perm48 / perm_row

def perm48(i):
    # 48 条 HEAD 轴上 tiled (3,16) -> grouped (16,3)
    # 目标 head g*3+t 来自源 head t*16+g
    return (i % 3) * 16 + (i // 3)

def perm_row(i):
    head, inner = divmod(i, 128)     # V_HEAD_DIM = 128
    return perm48(head) * 128 + inner

value_z 的行轴是 48 head × 128 行,所以置换必须在 head 粒度上做:

src_row = perm48(row // 128) * 128 + row % 128

🚨 把 perm48 直接作用在 6144 行索引上不是双射:perm48(1) == perm48(48) == 16。 详见 ninfer-ternary-traps 技能,这是整个验收链存在的理由。

attn_q 交错

attn_q.weight 是 N=12288,视为 48 个 256 行的块:偶数块 0,2,…,46 是 24 个 query head (顺序不变),奇数块 1,3,…,47 是 24 个 output-gate head。取法:

ent = [(name, (2*h + parity)*256 + r) for h in range(heads) for r in range(256)]
# parity=0 -> query, parity=1 -> gate

pack.py 的 deinterleave() 会强制 n % 512 == 0,否则 f"{name}: {n} rows is not a multiple of 512"。


6. 常量

| 名字 | 值 | |---|---| | HIDDEN | 5120 | | V_HEADS | 48 | | V_HEAD_DIM | 128 | | QK_ROWS | 4096 | | V_ROWS | 6144 | | SIGN_WIDTHS | [5120, 6144, 17408],和 28672 | | T_PQ2_0 / T_PTQ1_0 | 142 / 143 | | block_size | 1024 | | sign_mode | explicit | | transform | normalized-sylvester-walsh-hadamard | | prism.hadamard.gdn_v_grouped | 1 |


7. 三元编解码器在 pack.py,不在 tools/artifact

tools/artifact 里没有三元编解码器。只有 encode_row_split / assemble_row_planes / decode_row_split_codes / dequantize_row_split 这四个,它们都显式拒绝 TernaryFormat:

ValueError: row-split encoding requires a grouped quantized format

真正的编解码在 pack.py(numpy):

| 函数 | 作用 | |---|---| | dq_pq2_0(raw, chunk_groups=1<<18) | 34 字节 PQ2_0 组 → ndarray(fork 补丁 1:float32 + 分块 + 预分配) | | dq_ptq1_0(raw, chunk_groups=1<<18) | 28 字节 PTQ1_0 组 → ndarray(fork 基线已修) | | assemble_ternary(fmt, shape, row_fn) | 逐行 ggml 块 → row-split payload 平面 | | disassemble_ternary(fmt, shape, payload) | 精确逆:row-split 平面 → 连续 ggml 块流 |

assemble_ternary / disassemble_ternary 互为精确逆,这就是无损性的证明。

verify/_ternary_ref.py 的 Gguf.raw() 硬编码 PQ2_0_BLOCK_BYTES = 34,所以它作为 reader 是格式通用的,但 raw() 是 PQ2_0 专属。PTQ1_0 必须走 ternary_rows.gguf_row_keys()(用 pack.BLOCK_PLANE_SLICES / GGML_BLOCK)。


相关技能

  • ninfer-ternary-pack — 打包流程、模板要求、fork 补丁
  • ninfer-ternary-accept — 七道验收门怎么用
  • ninfer-ternary-traps — 这些几何背后的失效模式