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

bonsai三元GGUF模型转ninfer模型02

2打包流程:打包流程、groupwise-int 模板要求、MAPPING.json、产出 vs 借用、3 个 fork 补丁、check

person作者: montherlandhubModelScope

三元打包:GGUF → .ninfer

只改 src\,不要改 dist\。 dist\ 是 install.cmd 从 src\ 生成的运行副本。

.\install.cmd                                             # 装依赖、建 dist\.venv(不转模型)
.\dist\pack-ternary.cmd                                   # 文件选择器 GUI
.\dist\pack-ternary.cmd check                             # 只读自检
.\dist\pack-ternary.cmd layer3 out\layer3.ninfer          # 最小制品 → 自动验收 → 弹通知
.\dist\pack-ternary.cmd build  out\bonsai.ninfer          # 完整制品 → 自动验收 → 弹通知
.\dist\pack-ternary.cmd verify out\bonsai.ninfer --sweep  # 只验收,64 层全覆盖

任何模式都接受 --gguf <in.gguf> --template <tpl.ninfer>; 也可用环境变量 NINFER_TERNARY_GGUF / NINFER_TERNARY_TEMPLATE。 NINFER_ACCEPT_QUIET 抑制验收横幅。

build / layer3 写完产物会自动跑验收(pack-ternary.cmd:96)。写出来的产物不等于 被接受的产物。


1. 模板要求:weights_id == "groupwise-int"

模板不是单纯的 vision/MTP 负载捐赠者,而是打包器要遍历的骨架与清单——它自己的 对象名必须能逐个映射进封闭表。所以:

  • ✅ 任何 identity.weights_id == "groupwise-int" 的 qwen3.8-27b 制品。
  • ❌ nvfp4 打包的制品。它的 GDN/attention 是融合命名: gdn/a_b_projection、gdn/query_key_value_z、attention/query_key_gate_value —— 不在映射表里,必然以 unmapped gdn object ... 中止。

两种打包由同一模型的不同转换器产出,都在 tools/convert/qwen3_8_27b/ 下。 自检:① identity.weights_id == 'groupwise-int';② text/layers/3/ 下是 attention/query_key 与 attention/gate_value 两个对象(只有单个 attention/query_key_gate_value 就是 nvfp4 版,用不了)。

模板不存在时报的错里带 NINFER_TERNARY_TEMPLATE 与 README FAQ 指引。


2. 映射规则不在代码里

pack.py 不推导任何映射规则,全部来自 MAPPING.json(那里记录了每条的证据)。

顶层键:_doc、_verified_by、_layers、_constants、_rules(8 条)、 text_globals(5 条)、per_layer_gdn(13 条)、per_layer_attention(9 条)、 _counts、_unresolved(4 条)。

每条映射的字段:src(源 GGUF 张量名,{l} 是层号占位)、shape、fmt、transform、 可选 note。fmt 可以是真实格式名,也可以是标记 "TERNARY"(= 跟随源张量的三元类型)。

8 条 _rules 里有两条是 INFERRED 而非实测,且都是整行置换: gdn_value_z 与 attn_q_per_head_interleave。详见 ninfer-ternary-format 第 5 节。 另外 norm_shift 有例外:text/layers/{l}/gdn/norm <- blk.{l}.ssm_norm.weight 是 RAW 拷贝,不减 1。

层分类(--sweep 覆盖的就是这两组)

_layers.gdn_linear_attention : 48 层 = 0,1,2,4,5,6,...,60,61,62   (≡ 0,1,2 mod 4)
_layers.full_attention        : 16 层 = 3,7,11,...,63             (≡ 3 mod 4)

note:分类是推导的——某层是全注意力层 iff GGUF 里存在 blk.{l}.attn_q.weight。 48×13 + 16×9 + 5 = 773 个 text 对象;源侧 48×14 + 16×11 + 3 = 851 个 GGUF 张量。


3. 产出 vs 借用

清单由模板自己的对象列表构建(_counts.artifact_objects_total = 1124, 加打包器补的 2 个符号表对象 = 1126)。

Packer.produce(name) 对 text/*(除 text/vision* 与 text/draft_head*)返回字节, 返回 None 表示"借用模板的"。mode_build 里逐对象:产出就写,否则从模板 mmap 借用。

借用是有意的,逐类理由:

| 借用 | 数量 | 理由 | |---|---|---| | vision/* | 333 | Bonsai 把视觉塔作为单独的 mmproj GGUF 发布,本 GGUF 里没有。模板那份是同一个官方塔,文本解码期间不读。 | | mtp/* | 12 | 本 GGUF 没有 MTP 头(851 张量,零个 blk.64.*)。模板那份是同一套官方 Qwen3.8-27B 头:12 个张量全部 cos ≥ 0.99966,7 个 norm 逐位相同。 | | frontend/* | 6 | tokenizer 等。 | | text/draft_head + _token_ids | 2 | 频率短名单。同一 tokenizer ⇒ 同一短名单。要改就重新生成,别拷贝。 |

实测(2026-09-29,PTQ1_0 源 5.538 GiB):

总计           7,047,407,628 B = 6.563 GiB   objects 1126
  text 产出     5,930,360,844 B = 5.523 GiB
  借用负载      1,116,856,537 B = 1.040 GiB   {frontend 6, text 2, mtp 12, vision 333}

借用的 1.040 GiB 不是你的权重——MAPPING 把 vision 标为 _unresolved。

非位精确的部分(已知边界,别当 bug 修)

  • norm:gguf - 1.0 之后截断到 BF16(cos 0.99991–0.99997)。
  • a_log = log(-a) 用 float64 算完存 FP32。
  • 这些不是位精确。位精确的是三元部分。

产物不膨胀

CHECK 1 八种形状 pad 全 0,即 GGML 连续块流与 ninfer row-split 平面占字节完全相等。 相对源 GGUF 的增量全部是"GGUF 本来就没有、只能从模板借"的内容。格式开销为 0。


4. 三个 fork 补丁(只有 pack.py 里的这三处)

基线 = ninfer fork 的 tools/pack.py @ pascal-sm61 679f005d,sha256 bb666e19…5fddf。 纯净副本保留为 src\pack.ninfer-fork.py,三处补丁就是相对它的 diff。 完整溯源(含"为什么用 fork 而不是 ModelScope 那份")在 src\pack.base.provenance.txt。 本机没装 git,无法核对 fork 工作副本与该 commit 是否一致——以 pack.ninfer-fork.py 的 sha256 为准,commit 号仅作溯源。

选 fork 而不是 ModelScope 发布版,是因为它多带两个本部署需要的修正: CHECK 2 的 scale 探针格式感知(发布版硬编码 34/0:2,在 PTQ1_0 源上直接 ValueError: cannot reshape array of size 13762560 into shape (34)); 以及 dq_ptq1_0 分块解码(发布版整数组形式要 ~14 GiB float32 临时量,24 GiB 机器上 OOM)。 两者都不改任何输出字节。

| # | 补丁 | 为什么必须 | 位置 / 影响面 | |---|---|---|---| | 1 | dq_pq2_0 改 float32 + 分块 + 预分配 | 上游 (q.astype(np.int32) - 1) * d 里 int32 × float32 被 NumPy 提升成 float64,词表张量单份 9.47 GiB,而 mode_check 同时持有 a、b 两份 | pack.py:261。dq_pq2_0 全文件只被引用一次且在 mode_check 里,build/layer3 从不调用 ⇒ 不改判定、不改产物一个字节。等价性由 verify_decoder_patch.py 实测:4 个真实张量 × 4 种分块大小(含 chunk=7 压边界)全部 ndiff=0 | | 2 | mode_build 只在模板缺符号表时才追加 | 新版 groupwise-int 模板自带两个 Hadamard 符号表对象,且作为生成物排在最后两个位置(1126 里的 idx 1124/1125)——正是追加要放的地方。无条件追加会让 plan_objects() 抛 duplicate object name: text/hadamard_signs,一个字节都没写就退出,build/layer3 全失败 | pack.py:922。存在性由模板决定,不由硬编码假设决定;shape/format 仍取 SIGN_OBJECTS,所以两种写法不会漂移。模板自带的那份无论哪种情况都留在原位,且 produce() 会用本次 GGUF 的 KV 块覆盖两个对象,所以制品的符号表永远是源模型的,不是捐赠者的 | | 3 | GGML_GROUP / GGML_BLOCK / BLOCK_PLANE_SLICES 命名一次 | 让验收工具从 row_split_geometry() 推导平面偏移,而不是再硬编码一套可能与打包器不一致的常数 | pack.py:354 |

SIGN_OBJECTS = (("text/hadamard_signs", (28672,), "FP32"), ("text/hadamard_widths", (3,), "I32"))


5. check:无损性自检(只读,不写任何文件)

mode_check() 构造 Packer(所以模板 schema 错也会在这里快速失败),跑三组:

CHECK 1 — 几何

扫 GGUF 里所有 tt in FMT 的张量,按 (fmt, n, k) 归组,每组打一行 groups/row / src / payload / pad。健康值:pad = 0(无格式开销)。实测 8 种形状。

CHECK 2 — 字节往返 + 解码相等 + 偏移自检(决定性的一环)

6 个固定采样:blk.3.attn_q.weight、blk.3.attn_output.weight、blk.3.ffn_down.weight、 blk.0.attn_qkv.weight、blk.0.ssm_out.weight、token_embd.weight。 对每个算 bytes_equal(disassemble(assemble(src)) == src)与 decode_equal。

但 bytes_equal 只证明装配件自洽——读错文件偏移同样会给 bytes_equal,因为错的 字节进去又错出来。所以必须验内容:真正的组 scale 是紧密的正簇(高字节种类少、全有限、 无负值),错位读出来像均匀 uint16 采样,一堆 NaN/inf 和负值。

block = 28 if tt == T_PTQ1_0 else 34
sc_cols = slice(26, 28) if tt == T_PTQ1_0 else slice(0, 2)   # ← 格式感知
plaus = (n_nonfinite == 0 and n_neg == 0 and hi_distinct <= 64 and 0.0 < med < 1.0)

判据:nonfinite=0、negative=0、hi_distinct<=64、0<median<1 ⇒ 打印 PLAUSIBLE, 否则 IMPLAUSIBLE -- offset bug?。硬编码 34 / slice(0,2) 会静默读错 PTQ1_0 源。

CHECK 3 — producer 冒烟(只比 shape 和 size)

13 个名字:layer 3 的 attention/query_key、attention/gate_value、mlp/gate_up、 mlp/down、attention/output;layer 0 的 gdn/value_z、gdn/query_key、gdn/output; token_embedding、output_head、final_norm;外加两个符号表对象。 produce() 返回 None 的打 (borrowed) SKIP。尺寸不符打 MISMATCH want N 并置 ok=False。

最后 RESULT: OK / RESULT: FAILED,返回 0/1。

🚨 check 不是验收门 3 的替代品。 CHECK 3 只比字节数,而字节数恰恰是 "顺序错了也照样精确正确"的那类量。历史 bug 在 CHECK 1/2/3 全绿下出厂。见 ninfer-ternary-traps。


6. layer3 与 build

layer3 <out> = mode_build(g, out, only_layer=3):只保留 frontend/*、 text/hadamard_* 和 text/layers/3/*,得到一个最小制品(2 行构建)。用来快速验证 管道,不用来验收——--sweep 遇到 layer3-only 制品会报 1/64 并非零退出。

build <out> = 完整 text 模型。

两者都:

  • if out_path.exists(): raise SystemExit(f"refusing to overwrite {out_path}") —— 拒绝覆盖已存在的产物。
  • 逐对象:能产出就 w.write(name, data) 并累加 produced_bytes;否则从模板 tpl.payload(obj) 借用并累加 borrowed_bytes,按 name.split("/")[0] 计数。 借用后 del mv 释放 mmap 视图,否则 Artifact.close() 抛 BufferError。
  • 模板缺对象 → template has no object <name> to borrow。
  • 结尾打印 total / produced / borrowed(按前缀分组)/ objects 数量。

ArtifactWriter 是严格目录顺序的:写错顺序 → expected payload X, got Y; 长度不符 → payload X exceeds its planned byte length 或 has N bytes; expected M。 对齐填充由 writer 自动补零。


7. MTP 头删不掉

Binder::require_tensor → find_unconsumed 缺对象即抛 required artifact object is missing;Binder::finish() 还强制双向精确匹配(多一个 对象报 was not consumed by the selected target)。要关只能在启动时把 EngineOptions.speculative 设为 SpeculativeBackend::None,那 12 个对象走 ValidateOnly、留在 host 不占显存。关掉不影响输出正确性(每个 draft token 都被目标 模型校验),只掉吞吐。


相关技能

  • ninfer-ternary-format — 容器排布、平面几何、perm48
  • ninfer-ternary-accept — 写完之后的七道门
  • ninfer-ternary-tooling — install / GUI / 诊断脚本
  • ninfer-ternary-traps — 这些约束背后的失效模式