ik_llama.cpp 运行 BitNet b1.58 2B 模型实战:从 bitnet-b1.58 架构兼容到 I2_S 量化转换
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
本文以 ik_llama.cpp 仓库中关于 BitNet 新架构的 issue #365 为核心脉络,完整梳理微软 BitNet b1.58 2B 模型从「加载报错」到「量化转换、正常运行」的全过程:包括bitnet-b1.58架构名兼容、微软私有量化类型I2_S的识别,以及IQ2_BN/IQ2_BN_R4/IQ1_BN三种转换方案的选型。读完本文,你将掌握在 ik_llama.cpp 中转换并运行 BitNet b1.58 系列模型的完整命令、量化类型取舍原则,以及背后的源码级实现原理。
一、背景:BitNet 模型与架构命名的演进
BitNet 是微软提出的 1-bit 大语言模型路线,权重被限制在{-1, 0, +1}三个值(即 1.58 bit),因此在 CPU 上也能以极低的内存占用运行。ik_llama.cpp 最初通过 PR 337 支持了 2025 年初发布的 BitNet 模型,其 GGUF 中标注的架构名为bitnet-25。
2025 年 4 月 23 日,微软在 Hugging Face 上替换了旧的 BitNet 模型,发布新的 BitNet b1.58 2B 模型(4T tokens 训练),新版本 GGUF 的general.architecture字段改成了bitnet-b1.58。这看起来只是名称变化,却导致了旧版本 ik_llama.cpp(如 issue 中提到的98d1626469879d35faba9cb7e9d0b1ddaf853eee)无法识别该模型。
在当前的仓库源码中,可以清楚看到 BitNet 家族共有三个架构条目,见 src/llama-arch.cpp:
{ LLM_ARCH_BITNET, "bitnet" }, { LLM_ARCH_BITNET_25, "bitnet-25" }, { LLM_ARCH_BITNET_B158, "bitnet-b1.58" },对应枚举定义在 src/llama-arch.h 中:LLM_ARCH_BITNET、LLM_ARCH_BITNET_25、LLM_ARCH_BITNET_B158。其中bitnet-b1.58正是 issue 中新增的架构名,而它在计算图构建上复用了与bitnet-25相同的实现——src/llama-build-context.cpp 中:
case LLM_ARCH_BITNET: result = llm.build_bitnet(); break; case LLM_ARCH_BITNET_B158: case LLM_ARCH_BITNET_25: result = llm.build_bitnet_158(); break;同时,bitnet-25与bitnet-b1.58的张量映射表(token_embd、output_norm、output、rope_freqs、attn_norm、attn_q/k/v、attn_output、attn_rot_embd、ffn_gate_inp、ffn_norm、ffn_gate/down/up、MoE 专家张量、attn_sub_norm、ffn_sub_norm 等)在 src/llama-model.cpp 中完全一致。这说明从模型结构角度,两者本质上是同一套网络布局,差异主要体现在架构标识上。
二、问题现象:新模型加载与量化直接报错
issue 中用户 jdluzen 在 Windows arm64 上尝试运行新版 BitNet 模型时,遇到两类典型错误:
- 架构无法识别:
llama-quantize报unknown model architecture: 'bitnet-b1.58',直接导致量化失败; - 张量类型无法处理:模型加载器提示
llama_model_loader: unknown type i2_s,随后在ggml.c中因GGML_TYPE_I2_S(类型号 36)对应的vec_dot为 null 而崩溃。
另一位用户 usatenko 在 macOS 上复现了同样的问题,给出了完整的量化失败日志。模型元数据非常值得关注,这里完整摘录(该模型即微软官方发布的bitnet-b1.58-2B-4T-gguf):
./bin/llama-quantize --allow-requantize models/ggml-model-i2_s.gguf ggml-model-i2_s_bn.gguf iq2_bnmain: build = 3657 (98d16264) main: built with Apple clang version 17.0.0 (clang-1700.0.13.3) for arm64-apple-darwin24.4.0 main: quantizing 'models/ggml-model-i2_s.gguf' to 'ggml-model-i2_s_bn.gguf' as IQ2_BN llama_model_loader: loaded meta data with 24 key-value pairs and 332 tensors from models/ggml-model-i2_s.gguf (version GGUF V3 (latest)) llama_model_loader: unknown type i2_s llama_model_loader: Dumping metadata keys/values. Note: KV overrides do not apply in this output. llama_model_loader: - kv 0: general.architecture str = bitnet-b1.58 llama_model_loader: - kv 1: general.name str = bitnet2b llama_model_loader: - kv 2: bitnet-b1.58.vocab_size u32 = 128256 llama_model_loader: - kv 3: bitnet-b1.58.context_length u32 = 4096 llama_model_loader: - kv 4: bitnet-b1.58.embedding_length u32 = 2560 llama_model_loader: - kv 5: bitnet-b1.58.block_count u32 = 30 llama_model_loader: - kv 6: bitnet-b1.58.feed_forward_length u32 = 6912 llama_model_loader: - kv 7: bitnet-b1.58.rope.dimension_count u32 = 128 llama_model_loader: - kv 8: bitnet-b1.58.attention.head_count u32 = 20 llama_model_loader: - kv 9: bitnet-b1.58.attention.head_count_kv u32 = 5 llama_model_loader: - kv 10: tokenizer.ggml.add_bos_token bool = true llama_model_loader: - kv 11: bitnet-b1.58.attention.layer_norm_rms_epsilon f32 = 0.000010 llama_model_loader: - kv 12: bitnet-b1.58.rope.freq_base f32 = 500000.000000 llama_model_loader: - kv 13: general.file_type u32 = 40 llama_model_loader: - kv 14: tokenizer.ggml.model str = gpt2 llama_model_loader: - kv 15: tokenizer.ggml.tokens arr[str,128256] = ["!", "\"", "#", "$", "%", "&", "'", ... llama_model_loader: - kv 16: tokenizer.ggml.scores arr[f32,128256] = [0.000000, 0.000000, 0.000000, 0.0000... llama_model_loader: - kv 17: tokenizer.ggml.token_type arr[i32,128256] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ... llama_model_loader: - kv 18: tokenizer.ggml.merges arr[str,280147] = ["Ġ Ġ", "Ġ ĠĠĠ", "ĠĠ ĠĠ", "... llama_model_loader: - kv 19: tokenizer.ggml.bos_token_id u32 = 128000 llama_model_loader: - kv 20: tokenizer.ggml.eos_token_id u32 = 128001 llama_model_loader: - kv 21: tokenizer.ggml.padding_token_id u32 = 128001 llama_model_loader: - kv 22: tokenizer.chat_template str = {% for message in messages %}{% if lo... llama_model_loader: - kv 23: general.quantization_version u32 = 2 llama_model_loader: - type f32: 121 tensors llama_model_loader: - type f16: 1 tensors llama_model_loader: - type i2_s: 210 tensors llama_model_quantize: failed to quantize: unknown model architecture: 'bitnet-b1.58' main: failed to quantize model from 'models/ggml-model-i2_s.gguf'这段日志揭示了两个关键事实:
- 模型结构:128256 词表(gpt2 BPE 分词器)、4096 上下文、2560 隐藏维度、30 层、6912 FFN 维度、20 个注意力头(5 个 KV 头)、RoPE 维度 128、freq_base 500000、RMS Norm epsilon 1e-5;
- 张量分布:332 个张量中,121 个 f32、1 个 f16、210 个 i2_s。i2_s 就是微软私有的
I2_S量化类型,占据了绝大多数权重张量。
三、根因分析:I2_S 是微软私有量化格式
为什么老版本无法加载?问题不在架构本身,而在量化类型。微软发布的 BitNet GGUF 使用了自己的量化类型I2_S,这是一种 2-bit 符号量化格式,权重量化为{-1, 0, +1}三元值(1.58 bit 的含义即来源于此)。
在 ik_llama.cpp 的 ggml/include/ggml.h 中,GGML_TYPE_I2_S = 36的注释写得很明确:
GGML_TYPE_I2_S = 36, // So we are able to consume MS BitNet I2_S quants即该类型是为了「能够消费微软 BitNet 的 I2_S 量化权重」而加入的。但仅有类型定义还不够——量化权重要真正参与计算,必须要有对应的vec_dot(向量点积)内核。issue 中的崩溃点vec_dot为 null 正是发生在GGML_TYPE_I2_S类型上:旧版本代码里 I2_S 只有类型定义(用于读取模型),却没有可直接执行的计算内核,因此直接运行原始 GGUF 会在矩阵乘环节崩溃。
issue 中 jdluzen 用 debug 版llama-cli.exe -m ggml-model-i2_s.gguf直接运行原始模型时,还遇到了另一个崩溃:
Assertion failed: ldb >= k, file A:\src\ik_llama.cpp\ggml\src\llamafile\sgemm.cpp, line 856这正是「拿一个只有存储格式、没有完整计算链的量化类型直接跑推理」的典型症状。
四、解决方案一:补充 bitnet-b1.58 架构名
社区成员 saood06 很快定位到问题:新版模型只是架构名从bitnet-25改成了bitnet-b1.58,模型结构本身没有变化。验证方法很有说服力——对基于新旧模型分别转换出的 GGUF 运行gguf-hash.py,得到完全一致的哈希值,证明两个 GGUF 在内容层面(除架构名外)是等价的。
随后提交的 PR 366 在架构名映射表中新增了bitnet-b1.58条目(即当前仓库 src/llama-arch.cpp 中{ LLM_ARCH_BITNET_B158, "bitnet-b1.58" }这行),并使其与bitnet-25共用build_bitnet_158()计算图构建逻辑。用户 usatenko 确认「thank you, it works now」,问题得以解决。
这也解释了为什么说「这只是个名称变化」:架构枚举、张量映射、计算图构建都是现成的,缺的只是把字符串bitnet-b1.58解析到已有枚举LLM_ARCH_BITNET_B158的那一行映射。
五、解决方案二:将 I2_S 转换为 IQ2_BN / IQ1_BN(推荐)
架构名修复只是让模型「能被识别」,但要获得可用的推理性能,还需要把微软的I2_S权重转换为 ik_llama.cpp 原生支持且带有完整计算内核的量化格式。项目作者 ikawrakow 在 issue 中给出了官方标准做法:
./bin/llama-quantize --allow-requantize $microsoft_model $converted_model iq2_bn即对微软原始 GGUF 执行**允许重新量化(requantize)**的转换,目标格式为IQ2_BN。
5.1 为什么需要 --allow-requantize
常规量化是从 fp16/fp32 等未量化权重出发;而微软模型本身已经是量化过的I2_S。要把I2_S再转成IQ2_BN,属于「量化到量化」的转换,必须显式传入--allow-requantize才能放行。
在 src/llama-quantize.cpp 中可以看到,对GGML_TYPE_I2_S张量有专门的处理分支——先通过qtype.to_float把整个张量反量化回 f32,再做后续处理:
if (tensor->type == GGML_TYPE_I2_S) { // we need to dequantize the entire tensor for I2_S qtype.to_float(tensor->data, f32_output, nelements); return; }也就是说,转换链路是I2_S → f32 → IQ2_BN,中间必须经过全精度反量化。这也意味着转换过程需要一定的内存与时间开销。
5.2 三种目标格式的取舍
ikawrakow 在 issue 回复中给出了完整的选型建议,整理如下:
| 目标格式 | 每权重比特数 | 适用场景 | 说明 |
|---|---|---|---|
iq2_bn | 约 2 bit | 通用默认 | 官方推荐的标准转换目标,CPU / GPU 均可 |
iq2_bn_r4 | 约 2 bit | 仅 CPU且追求 prompt 处理速度 | 采用行交错(row-interleaved)打包,prompt processing 性能更好 |
iq1_bn | 1.625 bit | 追求更小模型体积 | 模型更小,PP 性能低于 iq2_bn/iq2_bn_r4;但在部分 CPU 上可能获得略高的 token 生成速度 |
具体命令(以iq2_bn_r4和iq1_bn为例):
# CPU-only 场景,追求更好的 prompt processing 性能 ./bin/llama-quantize --allow-requantize $microsoft_model $converted_model iq2_bn_r4 # 追求更小体积(1.625 bits/weight) ./bin/llama-quantize --allow-requantize $microsoft_model $converted_model iq1_bn从源码看,iq2_bn_r4是iq2_bn的 4 行交错变体,映射关系定义在 src/llama-quantize.cpp:{ GGML_TYPE_IQ2_BN_R4, { GGML_TYPE_IQ2_BN, 4} },即IQ2_BN_R4以 4 行为一组打包,底层量化结构与IQ2_BN相同。这就是为什么两种格式可以自由互转、且 r4 变体在行连续的内存布局上对 CPU 更友好的原因。
5.3 转换时的张量处理细节
转换过程还有一些值得注意的细节:
- 对齐要求:
IQ1_BN/IQ2_BN/IQ2_BN_R4要求张量列数能被块大小整除(nx % QK_IQ1BN == 0,其中QK_IQ1BN定义为 64,见 ggml/src/ggml-common.h)。若张量形状不满足,量化器会自动回退为兼容类型,见 src/llama-quantize.cpp; - 特殊张量类型:对于
output_norm等归一化张量和token_embd.weight嵌入张量,当目标为IQ1_BN/IQ2_BN/IQ2_BN_R4时,默认类型会被调整为IQ4_NL(见 src/llama-quantize.cpp 与 src/llama-quantize.cpp),因为这些张量对精度更敏感,不适合 1-2 bit 量化; - 文件类型标识:转换产物对应 GGUF 文件类型
LLAMA_FTYPE_MOSTLY_IQ1_BN = 136、LLAMA_FTYPE_MOSTLY_IQ2_BN = 137、LLAMA_FTYPE_MOSTLY_IQ2_BN_R4 = 337(见 include/llama.h),对应的底层张量类型为GGML_TYPE_IQ1_BN = 134、GGML_TYPE_IQ2_BN = 135、GGML_TYPE_IQ2_BN_R4 = 335(见 ggml/include/ggml.h)。
六、IQ1_BN / IQ2_BN 量化格式的源码级原理
转换后的IQ1_BN/IQ2_BN是 ik_llama.cpp 为 BitNet 这类 1.58-bit 模型引入的特化格式。从 ggml/src/iqk/iqk_quantize.cpp 可以看到其核心实现:
void quantize_row_iq1_bn(const float * x, void * y, int64_t k) { quantize_iq1_bn(x, y, 1, k, nullptr, nullptr); } void dequantize_row_iq1_bn(const block_iq1_bn * x, float * y, int64_t k) { assert(k%QK_IQ1BN == 0); int nblock = k / QK_IQ1BN; ... } size_t quantize_iq2_bn(const float * src, void * dst, int64_t nrows, int64_t n_per_row, const float * imatrix, ...) { IQ1BNQuantizer iq1bn; ... iq1bn.quantize_one_row_2bn(src + row*n_per_row, (block_iq2_bn *)qrow, n_per_row, imatrix); ... }反量化逻辑显示,IQ1_BN以 64 个元素为一个 block(QK_IQ1BN = 64),每个 block 用极紧凑的位打包表示{-1, 0, +1}三元权重,反量化时通过乘加移位还原出整数值。IQ2_BN则基于同一套IQ1BNQuantizer实现(quantize_one_row_2bn),在 1.58-bit 的基础上提供约 2 bit 的更高精度表示——这也解释了为什么iq2_bn是官方首推的转换目标:它在模型质量与体积之间取得了更平衡的取舍。
从仓库的模型支持公告看(README.md),bitnet-b1.58-2B-4T的支持正是通过 PR 337 引入的,与本次 issue 修复(PR 366)共同构成了该模型在 ik_llama.cpp 中的完整支持链路:PR 337 加架构与 I2_S 读取支持 → PR 366 补架构名映射 → llama-quantize 提供 I2_S → IQ1_BN/IQ2_BN 的转换通道。
七、完整落地步骤与验证
综合以上分析,在 ik_llama.cpp 中运行 BitNet b1.58 2B 的推荐流程如下:
- 更新代码:确保使用包含 PR 366(
bitnet-b1.58架构名)的版本,即当前仓库源码可直接识别该架构; - 准备模型:获取微软官方
bitnet-b1.58-2B-4TGGUF(其中权重为I2_S类型); - 转换量化(关键步骤,二选一):
# 标准转换,通用推荐 ./bin/llama-quantize --allow-requantize $microsoft_model $converted_model iq2_bn # 仅 CPU 运行,追求 prompt processing 性能 ./bin/llama-quantize --allow-requantize $microsoft_model $converted_model iq2_bn_r4 # 追求最小体积(1.625 bits/weight) ./bin/llama-quantize --allow-requantize $microsoft_model $converted_model iq1_bn- 验证转换结果:转换成功后,加载日志中应不再出现
unknown type i2_s,张量类型应显示为iq1_bn/iq2_bn/iq2_bn_r4,架构显示为bitnet-b1.58; - 运行推理:
./bin/llama-cli -m $converted_model -p "hi what are you"八、注意事项与已知限制
- 不要直接跑原始 I2_S 模型:微软原始 GGUF 中的
I2_S在 ik_llama.cpp 中主要用于「读取和转换」,直接用于推理可能因缺少对应计算内核而崩溃(如vec_dot为 null 断言)。务必先转换为IQ2_BN系列; - 必须加
--allow-requantize:从已量化的I2_S再量化必须显式允许 requantize,否则转换会被拒绝; iq2_bn_r4的适用前提:r4 行交错变体主要面向 CPU 优化;若涉及 GPU 卸载,建议优先使用标准iq2_bn;- 体积与性能的权衡:
iq1_bn(1.625 bit)体积最小,但 prompt processing 性能下降;是否换来更好的生成速度取决于具体 CPU 平台; - 架构名敏感性:
bitnet-b1.58、bitnet-25、bitnet三者是独立的架构条目,加载模型时general.architecture必须精确匹配其一,否则报unknown model architecture。
九、小结
BitNet b1.58 2B 在 ik_llama.cpp 中的接入过程,是一个典型的「模型更新 → 架构名兼容 → 量化格式适配」三重问题。架构层面由 PR 366 补齐bitnet-b1.58名称映射(与bitnet-25共用build_bitnet_158计算图)解决;性能层面则由llama-quantize的I2_S → IQ2_BN / IQ2_BN_R4 / IQ1_BN转换通道解决。整个链路在 src/llama-arch.cpp、src/llama-build-context.cpp、src/llama-quantize.cpp 与 ggml/src/iqk/iqk_quantize.cpp 中均有清晰的源码佐证。对希望以约 2 bit 甚至 1.625 bit 的超低精度在 CPU 上运行 BitNet 类模型的开发者而言,本文给出的转换命令与选型建议可以直接复用。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考