三元打包: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— 容器排布、平面几何、perm48ninfer-ternary-accept— 写完之后的七道门ninfer-ternary-tooling— install / GUI / 诊断脚本ninfer-ternary-traps— 这些约束背后的失效模式
Scan to join WeChat group