ik_llama.cpp 中 Gemma3(纯文本)推理支持:图构建实现与注意力机制深度解析
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
导读
本文基于 ik_llama.cpp 仓库中"Add Gemma3 support (text only)"(PR #276)这一合并记录,深入解析 Gemma3 纯文本推理在图构建层面的完整实现。文章以 PR 描述 为核心骨架,结合仓库源码(build_gemma3.cpp、llama-arch.cpp、llama-hparams.cpp 等)展开,重点讲解 Gemma3 架构特有的"5-to-1 交错注意力"(Sliding Window Attention 与全局注意力交替)、滑动窗口层的独立 RoPE 频率基准、以及注意力缩放系数差异等关键实现。读者读完可掌握 Gemma3 文本模型在 ik_llama.cpp 中从 GGUF 加载、超参解析到计算图构建的完整链路,理解其与 Gemma2 的核心差异。
一、PR 背景与工作范围
1.1 PR 基本信息
| 项目 | 内容 |
|---|---|
| PR 编号 | #276 |
| 作者 | ikawrakow(项目维护者) |
| 状态 | ❌ Closed(已关闭) |
| 创建时间 | 2025-03-21 |
| 更新时间 | 2025-03-22 |
PR 描述极其精简:"Basically just the graph building. Conversion from safetensors needs to be done with upstream."(基本只是图构建部分。从 safetensors 的转换需要借助上游完成。)
这句话界定了本次工作的明确边界:
- 本仓库负责的部分:Gemma3 架构在 llama.cpp 推理侧的计算图(graph)构建,即把 Gemma3 的权重张量组织成可执行的推理流程;
- 不负责的部分:safetensors → GGUF 的模型转换脚本。转换需要借助上游 llama.cpp 的
convert_hf_to_gguf.py完成(该脚本在本仓库根目录下同样存在:convert_hf_to_gguf.py,但 Gemma3 的转换支持以上游版本为准)。
也就是说,这是一次"推理侧先行"的架构适配:先在本地图构建层吃下 Gemma3 权重,转换管线随后补齐。
1.2 从源码结构确认的 Gemma3 支持全貌
虽然 PR 描述简短,但当前仓库源码已经完整保留了 Gemma3 支持的全部痕迹,可以从以下维度确认实现事实:
| 维度 | 源码位置 | 内容 |
|---|---|---|
| 架构注册 | src/llama-arch.cpp | { LLM_ARCH_GEMMA3, "gemma3" },将gemma3字符串映射到架构枚举 |
| 架构枚举 | src/llama-arch.h | LLM_ARCH_GEMMA3,紧随LLM_ARCH_GEMMA、LLM_ARCH_GEMMA2之后 |
| 超参解析 | src/llama-hparams.cpp | case LLM_ARCH_GEMMA3:分支,读取滑动窗口、RMS eps,并设置注意力缩放 |
| 张量加载 | src/llama-load-tensors.cpp | case LLM_ARCH_GEMMA3: use_mmap_buffer = create_gemma_tensors(tn, 3);复用 Gemma 系列张量创建逻辑 |
| 图构建 | src/graphs/build_gemma3.cpp | llm_build_context::build_gemma3(),完整的前向计算图 |
| 图调度 | src/llama-build-context.cpp | case LLM_ARCH_GEMMA3: result = llm.build_gemma3(); |
可以推断:本 PR 的核心改动即build_gemma3()图构建函数,而架构注册、超参解析等配套代码在后续迭代中持续完善(当前仓库的 Gemma3 分支比 PR 提交时更完整,例如n_swa_pattern的硬编码已抽象为超参)。
二、Gemma3 架构要点:5-to-1 交错注意力
2.1 与 Gemma2 的对比
Gemma2 使用统一滑动窗口(hparams.n_swa = 4096为默认值,见 llama-hparams.cpp),所有层都受滑动窗口约束,且采用 logit soft-capping。
Gemma3 则引入了**"5-to-1 interleaved attention"(5:1 交错注意力)模式**:每 5 层局部注意力(sliding window attention, SWA)后跟 1 层全局注意力。这在 build_gemma3.cpp 中有直接注释与实现:
// "5-to-1 interleaved attention" // 5 layers of local attention followed by 1 layer of global attention static const int sliding_window_pattern = 6;判定逻辑为:
for (int il = 0; il < n_layer; ++il) { const bool is_sliding = (il + 1) % sliding_window_pattern; ... }即第il层(从 0 计数)当(il + 1) % 6 != 0时是滑动窗口层,(il + 1) % 6 == 0时是全局层。注意:在 llama-hparams.cpp 中,hparams.n_swa_pattern = 6被写入超参,后续架构可通过 GGUF 元数据覆盖该模式。
2.2 滑动窗口层的独立 RoPE 配置
这是 Gemma3 实现的另一关键点。滑动窗口层与全局层在位置编码上使用不同的 RoPE 频率基准:
const float freq_base_l = is_sliding ? 10000.0f : freq_base; const float freq_scale_l = is_sliding ? 1.0f : freq_scale;对应 llama-hparams.cpp 中:
hparams.rope_freq_base_train_swa = 10000.0f; hparams.rope_freq_scale_train_swa = 1.0f;含义:滑动窗口层在训练时使用freq_base = 10000的 RoPE 配置,而全局层沿用模型整体(通常更长上下文优化的)freq_base。这样局部层不需要为长上下文外推而修改频率——因为它本来只"看"窗口内的有限 token。
同时,build_gemma3()利用了本仓库的 RoPE 缓存机制(cparams.rope_cache),对滑动层与全局层分别构建缓存:
if (cparams.rope_cache && (rope_type == LLAMA_ROPE_TYPE_NEOX || rope_type == LLAMA_ROPE_TYPE_NORM)) { rope_cache = ggml_rope_cache(ctx0, inp_pos, nullptr, n_rot, n_rot, rope_type, n_ctx_orig, freq_base, freq_scale, ...); rope_cache_l = ggml_rope_cache(ctx0, inp_pos, nullptr, n_rot, n_rot, rope_type, n_ctx_orig, 10000.0f, 1.0f, ...); }推理时按层选用:
auto rcache = is_sliding ? rope_cache_l : rope_cache; Qcur = ggml_rope_fast(ctx0, Qcur, rcache); Kcur = ggml_rope_fast(ctx0, Kcur, rcache);非缓存路径则用ggml_rope_ext并传入freq_base_l / freq_scale_l。两种路径殊途同归。
2.3 双 KQ Mask:全局与滑动窗口
由于存在两种注意力层,计算图需要两个不同的 attention mask:
struct ggml_tensor * KQ_mask = build_inp_KQ_mask(true); struct ggml_tensor * KQ_mask_swa = build_inp_KQ_mask_swa(true);每层根据is_sliding选用对应 mask(build_gemma3.cpp):
struct ggml_tensor * KQ_mask_l = is_sliding ? KQ_mask_swa : KQ_mask;并在 KV 注意力计算中传入滑动窗口大小:
cur = llm_build_kv(ctx0, lctx, kv_self, gf, model.layers[il].wo, NULL, Kcur, Vcur, Qcur, KQ_mask_l, n_tokens, kv_head, n_kv, hparams.f_attention_scale, cb, il, nullptr, KQ_mask_l == KQ_mask_swa ? hparams.n_swa : 0);build_inp_KQ_mask_swa的声明位于 src/llama-build-context.h,它构造只允许当前 token 回溯n_swa个位置的因果 mask。
三、build_gemma3() 计算图逐步拆解
下面按 build_gemma3.cpp 的实际代码顺序,逐段拆解图构建逻辑。
3.1 输入嵌入与缩放
inpL = llm_build_inp_embd(ctx0, lctx, hparams, batch, model.tok_embd, cb); // important: do not normalize weights for raw embeddings input (i.e. encoded image embeddings) if (batch.token) { inpL = ggml_scale(ctx0, inpL, sqrtf(n_embd)); cb(inpL, "inp_scaled", -1); }关键注释点明了 Gemma 系列的设计:词嵌入需要乘以sqrt(n_embd)缩放,但若输入来自编码器(如图像嵌入),则不能缩放。本 PR 是"text only"(纯文本)支持,batch.token路径即文本输入,因此会执行缩放。这一行为与 src/graphs/build_gemma.cpp 中 Gemma/Gemma2 的处理一致——Gemma3 图构建直接复用了家族惯例。
3.2 单层 Transformer 结构
对每一层il,计算图依次完成:
RMS 归一化 + QKV 投影:
cur = llm_build_norm(ctx0, inpL, hparams, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, cb, il); auto [Qcur, Kcur, Vcur] = llm_build_mul_mat_qkv(gf, cur, model.layers[il].wqkv, nullptr, model.layers[il].wqk, nullptr, model.layers[il].wq, nullptr, model.layers[il].wk, nullptr, model.layers[il].wv, nullptr, model.layers[il].attn_q_norm, model.layers[il].attn_k_norm, 0, il);llm_build_mul_mat_qkv是一个通用辅助函数,会根据模型实际存在的权重(融合的wqkv,或分离的wq/wk/wv)自动选择投影方式,并处理 Q/K 的 RMS 归一化(attn_q_norm、attn_k_norm,Gemma 系列在注意力前对 Q、K 做 RMS norm,这是其区别于 LLaMA 的显著特征之一)。RoPE 位置编码:按层选择 RoPE 缓存/频率(见 2.2 节)。
KV 注意力:选用全局或滑动窗口 mask 进入
llm_build_kv。注意力后归一化 + 残差:
cur = llm_build_norm(ctx0, cur, hparams, model.layers[il].attn_post_norm, NULL, LLM_NORM_RMS, cb, il); struct ggml_tensor * sa_out = ggml_add(ctx0, cur, inpL);Gemma3 在注意力输出之后还有一个
attn_post_norm,这与 Gemma2 类似(pre-norm 与 post-norm 双重归一化的"sandwich"结构)。FFN(含 post-norm):
cur = llm_build_ffn(ctx0, lctx, model.layers[il].ffn_norm, sa_out, model.layers[il].ffn_up, NULL, NULL, model.layers[il].ffn_gate, NULL, NULL, model.layers[il].ffn_down, NULL, NULL, NULL, LLM_FFN_GELU, LLM_FFN_PAR, cb, il); cur = llm_build_norm(ctx0, cur, hparams, model.layers[il].ffn_post_norm, NULL, LLM_NORM_RMS, cb, -1); cur = ggml_add(ctx0, cur, sa_out);激活函数为
LLM_FFN_GELU,并使用LLM_FFN_PAR(parallel,FFN 与注意力并行共享输入)布局,同样带ffn_post_norm。随后lctx.cvec.apply_to应用上下文向量(contrastive 控制向量)。末层优化:在最后一层(
il == n_layer - 1)用inp_out_ids提前裁剪掉未使用 token 的输出,减少后续计算量(build_gemma3.cpp)。
3.3 输出头
cur = llm_build_norm(ctx0, cur, hparams, model.output_norm, NULL, LLM_NORM_RMS, cb, -1); cur = llm_build_lora_mm(lctx, ctx0, model.output, cur); ggml_build_forward_expand(gf, cur);最终经output_norm(RMS norm)后,用llm_build_lora_mm做 lm_head 投影(该辅助函数支持 LoRA 适配器,无 LoRA 时即普通矩阵乘),最后ggml_build_forward_expand展开为完整前向图返回。
3.4 图构建的注册入口
在 src/llama-build-context.cpp:
case LLM_ARCH_GEMMA3: result = llm.build_gemma3(); break;而 src/llama-build-context.h 声明了ggml_cgraph * build_gemma3();。整条链路(架构枚举 → 图分发 → 图实现)闭合。
四、超参解析:llama-hparams.cpp 中的 Gemma3 分支
在 src/llama-hparams.cpp,LLM_ARCH_GEMMA3分支完成了以下工作:
case LLM_ARCH_GEMMA3: { hparams.n_swa_pattern = 6; hparams.rope_freq_base_train_swa = 10000.0f; hparams.rope_freq_scale_train_swa = 1.0f; ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa); ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); switch (hparams.n_layer) { case 26: model.type = e_model::MODEL_2B; break; case 34: model.type = e_model::MODEL_4B; break; case 48: model.type = e_model::MODEL_12B; break; case 62: model.type = e_model::MODEL_27B; break; default: model.type = e_model::MODEL_UNKNOWN; } hparams.f_attention_scale = model.type == e_model::MODEL_27B ? 1.0f / std::sqrt(float(hparams.n_embd / hparams.n_head(0))) : 1.0f / std::sqrt(float(hparams.n_embd_head_k_full)); } break;各要点说明:
| 配置项 | 说明 |
|---|---|
n_swa_pattern = 6 | 交错注意力周期:5 层局部 + 1 层全局 |
rope_freq_base_train_swa = 10000.0f | 滑动层训练期 RoPE 基准频率 |
rope_freq_scale_train_swa = 1.0f | 滑动层训练期频率缩放 |
LLM_KV_ATTENTION_SLIDING_WINDOW | 从 GGUF 读取滑动窗口大小(如 1024) |
LLM_KV_ATTENTION_LAYERNORM_RMS_EPS | 读取 RMS 归一化 epsilon |
| 模型规格识别 | 26 层 → 2B、34 层 → 4B、48 层 → 12B、62 层 → 27B |
f_attention_scale | 注意力缩放系数,27B 与其他尺寸不同 |
4.1 注意力缩放系数的尺寸差异
值得单独强调:Gemma3 的注意力缩放系数不是统一公式:
- 27B 模型:
1 / sqrt(n_embd / n_head(0)),即按每个头的维度n_embd / n_head缩放(经典1/sqrt(d_k)风格); - 其余尺寸(2B/4B/12B):
1 / sqrt(n_embd_head_k_full),按完整 K 头维度缩放。
这直接影响了llm_build_kv中hparams.f_attention_scale的取值,是从 llama-hparams.cpp 源码可确认的实现事实。
4.2 张量加载:复用 create_gemma_tensors
在 src/llama-load-tensors.cpp:
case LLM_ARCH_GEMMA3: use_mmap_buffer = create_gemma_tensors(tn, 3); break;create_gemma_tensors(tn, 3)复用了 Gemma 系列的张量创建函数,第三参数为版本号 3。Gemma2(create_gemma_tensors(tn, 2))与 Gemma(create_gemma_tensors(tn, 1))同样走此路径。这印证了 PR 描述中"Basically just the graph building"的边界:张量加载层几乎零改动,差异集中在图构建。
五、从 PR 到当前仓库的演进(附实践指引)
5.1 可以观察到的演进痕迹
对比 PR 提交(2025-03-21/22)与当前仓库代码,可以推断以下演进:
- 滑动窗口模式超参化:
sliding_window_pattern从图构建函数的局部静态常量(static const int sliding_window_pattern = 6;)演进为hparams.n_swa_pattern,使模式可通过超参读取; - RoPE 缓存路径:
cparams.rope_cache分支(ggml_rope_cache+ggml_rope_fast)是仓库为提升长上下文性能引入的机制,Gemma3 图构建同步适配; - 多模态支持:当前仓库 examples/mtmd(多模态)与 gguf-py/gguf/constants.py 中均出现 gemma3 相关条目,说明纯文本支持之后,视觉模态也已跟进(本 PR 仅覆盖文本)。
5.2 如何使用 Gemma3 文本模型
结合仓库结构与文档,运行 Gemma3 文本模型的标准路径为:
- 获取 GGUF:使用上游 llama.cpp 的
convert_hf_to_gguf.py将 Hugging Face 上的 Gemma3 safetensors 权重转换为 GGUF(本仓库也提供 convert_hf_to_gguf.py,但 PR 明确说明转换需以上游为准); - 构建:按 docs/install.md 配置并编译(如
cmake -B build && cmake --build build --config Release -j); - 运行:使用 examples/main/main.cpp 或 examples/server 加载 GGUF 推理:
./build/bin/llama-cli -m path/to/gemma3-4b-it.gguf -p "Hello, Gemma3!" -n 256 - 验证:启动时日志会输出架构名
gemma3;如需对比量化效果,可参考仓库讨论 334 -iq4_ksperforms great on gemma-3-27b-it-qat-q4_0-unquantized 中的社区实践(注意该讨论为社区观测,非官方承诺)。
5.3 关键源码速查表
| 关注点 | 文件 |
|---|---|
| Gemma3 图构建实现 | src/graphs/build_gemma3.cpp |
| 图构建分发入口 | src/llama-build-context.cpp |
| 图构建类声明 | src/llama-build-context.h |
| 架构字符串注册 | src/llama-arch.cpp |
| 超参解析(SWA/RoPE/缩放) | src/llama-hparams.cpp |
| 张量加载(复用 Gemma 家族) | src/llama-load-tensors.cpp |
六、总结
PR #276 以极简的改动面("Basically just the graph building")为 Gemma3 纯文本推理铺平了道路。当前仓库的 build_gemma3.cpp 完整呈现了这一实现,其技术要点可归纳为:
- 5-to-1 交错注意力:5 层滑动窗口注意力 + 1 层全局注意力循环,窗口层与全局层使用不同 KQ mask 和不同的 RoPE 频率基准(滑动层固定
freq_base=10000,全局层沿用长上下文配置); - 双重 post-norm:注意力与 FFN 输出后均附加 RMS post-norm,延续 Gemma2 的"pre+post"归一化风格;
- 家族复用:张量加载复用
create_gemma_tensors(tn, 3),超参解析与图构建为 Gemma3 单独分支; - 尺寸差异化缩放:27B 模型与其他尺寸采用不同的注意力缩放系数公式,由超参解析阶段按层数判定。
对希望移植或对比 Gemma 系列架构的开发者而言,build_gemma3.cpp 与 llama-hparams.cpp 是理解"交错注意力如何在 C++ 推理引擎中落地"的最佳范本。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考