gemma.cpp 库集成实战:用 simplified_gemma 模板构建最小文本生成应用
【免费下载链接】gemma.cpplightweight, standalone C++ inference engine for Google's Gemma models.项目地址: https://gitcode.com/GitHub_Trending/ge/gemma.cpp
导读
simplified_gemma 是 gemma.cpp 仓库中最简洁的“把 Gemma 模型作为 C++ 库来使用”的模板项目:它不提供交互式界面,而是用几行代码完成模型加载、状态初始化,并针对一个硬编码的 prompt 生成文本。读完本文,你将掌握基于 CMake + FetchContent 的两种构建方式(拉取远程 libgemma 与使用本地源码)、--tokenizer/--weights/--model三参数运行方式,以及--reject受约束解码(constrained decoding)的用法,并能读懂SimplifiedGemma封装背后的RuntimeConfig、stream_token与accept_token回调机制,把它改造成自己的生成程序。
一、示例定位:最小化、可复制的“库用法”模板
examples/README.md对该目录的定位说明得很清楚:这里展示的是“超越交互式gemma应用(run.cc)之外的、把 gemma.cpp 作为库使用的简单示例”,其中hello_world/被描述为“minimal/template project”(最小模板项目)。simplified_gemma/README.md 进一步明确:
- 它是一个模板工程,用于演示将
gemma.cpp作为库集成; - 没有交互界面,模型状态设置好后,针对单个硬编码 prompt生成文本;
- 构建步骤与主
gemma可执行程序类似,目前仅支持cmake/make构建。
与 hello_world 相比,simplified_gemma 额外演示了受约束解码(--reject),并且把Gemma、ThreadingContext、MatMulEnv、KVCache等底层组件封装成了一个更接近实战的SimplifiedGemma类,是学习 gemma.cpp 库接口的绝佳起点。
二、构建:两种模式(远程 libgemma / 本地 gemma.cpp)
在gemma.cpp/examples/simplified_gemma目录下先执行 CMake 配置:
cmake -B build这会生成gemma.cpp/examples/simplified_gemma/build构建目录。注意:默认模式会通过 FetchContent 从 git 仓库的固定 commit 拉取libgemma(源码见 CMakeLists.txt:BUILD_MODE未指定时默认为remote,FetchContent_Declare(gemma GIT_REPOSITORY https://github.com/google/gemma.cpp.git GIT_TAG a9aa63fd...),同时还会拉取 google/highway 与 google/sentencepiece 两个依赖,分别固定到各自的 commit hash)。
如果你想基于当前本地版本的 gemma.cpp构建,改用:
cmake -B build -DBUILD_MODE=local在local模式下,CMake 通过FetchContent_Declare(gemma SOURCE_DIR ../../..)(CMakeLists.txt)直接引用从examples/simplified_gemma/build/向上三层即仓库根目录的源码,不再执行网络拉取。
切换构建配置前,务必清空 build 目录内容,避免缓存了旧的 FetchContent 结果导致配置错乱。
配置完成后进入构建目录执行 make:
cd build make simplified_gemma与顶层 gemma.cpp 项目一致,可用-j参数并行加速,例如make -j8 simplified_gemma。CMake 相关细节可从 CMakeLists.txt 确认:项目要求 C++17、未指定构建类型时默认Release,可执行文件simplified_gemma由run.cc编译而来,并链接hwy hwy_contrib sentencepiece libgemma四个库(第45-46行)。
三、运行:与 gemma.cpp 相同的三参数命令行
构建完成后,build目录下会出现simplified_gemma可执行文件。运行参数与主gemma程序完全一致,同样是三个模型参数:tokenizer 路径、压缩权重文件路径、模型类型:
./simplified_gemma --tokenizer tokenizer.spm --weights 2b-it-sfp.sbs --model 2b-it各参数含义(与顶层 README.md 的 gemma 运行参数一致):
| 参数 | 说明 | 示例值 |
|---|---|---|
--tokenizer | tokenizer 模型路径 | tokenizer.spm |
--weights | 压缩后的模型权重文件(.sbs) | 2b-it-sfp.sbs |
--model | 模型类型标识 | 2b-it |
权重与 tokenizer 文件从 Kaggle 的 Gemma cpp 版本页面下载(顶层 README.md 给出了完整的获取与解压流程,2b-it-sfp.sbs即 2B instruction-tuned 模型的 8-bit 切换浮点(SFP)压缩权重;2025-05-06 之后生成的单文件格式权重也可省略 tokenizer 参数)。
运行成功后会向终端输出一段问候语,例如:
"Hello, world! It's a pleasure to greet you all. May your day be filled with joy, peace, and all the things that make your heart soar.四、源码主流程:run.cc 的三步骨架
打开 run.cc,整个程序只有三个核心步骤,正是“把 gemma.cpp 当库用”的标准骨架:
第 1 步:解析命令行参数。gcpp::LoaderArgs loader(argc, argv);从命令行读取并校验 tokenizer、weights 等参数。LoaderArgs定义在 gemma_args.h,内部通过ArgsBase的InitAndParse完成解析,支持的字段包括tokenizer、weights、map(是否启用内存映射,-1 自动/0 否/1 是)、to_bf16(是否将权重转为 bf16)、wrapping(是否启用 prompt 包装)。
第 2 步:构造模型并生成。代码注释给出了三种用法:
- 最简用法:
SimplifiedGemma gemma(loader); - 显式传入路径:
gcpp::LoaderArgs loader("/path/to/tokenizer", "/path/to/weights", "model_identifier"); - 完整用法:额外传入
gcpp::InferenceArgs inference(argc, argv);与gcpp::ThreadingArgs threading(argc, argv);,再SimplifiedGemma gemma(loader, threading, inference);——不传则使用默认值。
第 3 步:硬编码 prompt 并生成。示例固定使用:
std::string prompt = "Write a greeting to the world."; gemma.Generate(prompt, 256, 0.6);即生成最多 256 个 token、采样温度 0.6的输出。Generate的完整签名(gemma.hpp)为:
void Generate(std::string& prompt, size_t max_generated_tokens = 1024, float temperature = 0.7, const std::set<int>& reject_tokens = {});三个可选参数分别为最大生成 token 数(默认 1024)、采样温度(默认 0.7)、以及用于受约束解码的被拒绝 token ID 集合。
五、深入 SimplifiedGemma:一个可复用的封装类
gemma.hpp 中的SimplifiedGemma类把 gemma.cpp 的底层组件组装成了可直接复用的封装。理解它的构造函数,也就理解了 gemma.cpp 作为库运行所需的最小对象集合(第35-45行):
SimplifiedGemma(const gcpp::LoaderArgs& loader, const gcpp::ThreadingArgs& threading = gcpp::ThreadingArgs(), const gcpp::InferenceArgs& inference = gcpp::InferenceArgs()) : ctx_(threading), // 线程上下文:管理线程池 env_(ctx_), // MatMul 执行环境 gemma_(loader, inference, ctx_), // 核心 Gemma 对象 kv_cache_(gemma_.Config(), inference, ctx_.allocator) { std::random_device rd; gen_.seed(rd()); // 随机数发生器,供采样使用 }成员对象与Gemma类的对应关系(见 gemma.h 中Gemma的公开接口):gemma_.Config()返回ModelConfig,gemma_.Tokenizer()返回分词器,gemma_.ChatTemplate()返回聊天模板——这些都在Generate内部被直接调用。
Generate的生成流水线分为三个阶段:
- 分词与包装:
gcpp::WrapAndTokenize(gemma_.Tokenizer(), gemma_.ChatTemplate(), gemma_.Config().wrapping, generated, prompt)将原始字符串转为 token ID 序列(使用模型配置中的 wrapping 设置决定是否套用对话模板)。 - 流式回调:定义
stream_token回调,每当生成一个 token 就被调用一次(见 gemma.hpp)。回调中通过generated < prompt_size判断当前仍是 prompt 阶段(静默跳过),否则调用gemma_.Tokenizer().Decode({token}, &token_text)把 token 解码为文本并std::cout << token_text << std::flush实时打印——这就是输出逐字“流式”出现的原因。 - 调用底层生成:组装
gcpp::RuntimeConfig后调用gemma_.Generate(runtime_config, tokens, 0, kv_cache_, env_, timing_info)。RuntimeConfig定义于 gemma_args.h,示例中配置了max_generated_tokens、temperature、随机数发生器gen、verbosity = 0、stream_token与accept_token。Gemma::Generate的签名(gemma.h)要求调用方显式传入pos(KV cache 中的位置,单轮对话传 0)、KVCache、MatMulEnv和TimingInfo——这四点正是库模式与交互式 CLI 的主要差异,也是二次开发时需要理解的关键约定。
六、受约束解码:--reject 拒绝指定 token
README 专门演示了受约束解码(constrained decoding)。运行时可加--reject标志,后面跟一串token ID:
./simplified_gemma [...] --reject 32338 42360 78107 106837 132832 143859 154230 190205两个使用要点:
--reject必须是命令行最后一个参数,因为它会消费其后所有的参数作为 token ID 列表;- 参数是一串数字形式的 token ID,例如示例中列出的 8 个 ID 是“greeting”一词在不同上下文中出现的各种变体。
其底层机制对应RuntimeConfig::accept_token回调(gemma.hpp):
.accept_token = & { return !reject_tokens.contains(token); },AcceptFunc的契约(gemma_args.h)是:对每个候选 token,返回 false 表示拒绝生成,返回 true 表示接受。因此--reject的实现就是维护一个std::set<int>,凡落在集合中的 token 一律返回 false,从候选集合中“屏蔽”掉。--reject的解析发生在LoaderArgs/InferenceArgs等通用参数解析之前,由示例自身负责收集(收集到的 ID 通过SimplifiedGemma::Generate的reject_tokens形参传入),这就是它必须放在最后的原因。
值得注意的是,accept_token是 gemma.cpp 提供的通用机制——除了拒绝列表,你完全可以实现自定义约束(例如只允许特定词表子集、强制输出数字等),这也是该示例对“库化使用”最具启发性的部分。
七、从模板到自有应用:可扩展的方向
基于该模板改造为自有应用时,参考 gemma_args.h 中InferenceArgs的参数表,可在构造SimplifiedGemma时传入并控制更多生成行为:
| 参数 | 默认值 | 说明 |
|---|---|---|
--max_generated_tokens | 4096 | 最大生成 token 数(示例中直接调用Generate(prompt, 256, 0.6)覆写为 256) |
--temperature | 1.0 | top-K 采样温度 |
--top_k | 1 | 采样时考虑的候选 token 数 |
--verbosity | 1 | 0 仅打印生成结果 / 1 标准终端 UI / 2 开发者调试信息 |
--seq_len | 8192 | 序列长度(上限由ModelConfig.max_seq_len决定) |
--deterministic | false | 使 top-K 采样确定性化 |
--prefill_tbatch | 256 | prefill 阶段每批最大 token 数 |
--decode_qbatch | 16 | decode 阶段每批最大查询数 |
InferenceArgs::CopyTo(gemma_args.h)负责把这些值同步进RuntimeConfig,并校验 batch 尺寸不超过MMStorage::kMaxM。
在此基础上,常见的改造方向包括:将硬编码 prompt 改为--prompt/--prompt_file参数驱动(非交互式一键生成,见InferenceArgs中对应字段);将Generate循环多次调用以实现多轮对话(配合--multiturn决定是否保留 KV cache);把stream_token回调里的std::cout替换为 WebSocket/文件输出,即可快速搭建一个最小生成服务——这一切都从 run.cc 这个不足 30 行的入口开始。
八、总结
simplified_gemma 用最小的代码量展示了 gemma.cpp 库模式的完整闭环:CMake 配置(本地/远程双模式)→ make 构建 → 三参数运行 → 流式输出 →--reject受约束解码。透过 gemma.hpp 与 run.cc 两处核心源码,可以清晰看到它如何把LoaderArgs、ThreadingContext、MatMulEnv、KVCache、RuntimeConfig组装成可复用的SimplifiedGemma类,而stream_token/accept_token两个回调则分别承载了输出流式化与解码约束两大扩展点。对于想要将 Gemma 能力嵌入自有 C++ 项目的开发者而言,这个模板是比直接阅读gemma交互式 CLI 更短、更聚焦的集成起点。
【免费下载链接】gemma.cpplightweight, standalone C++ inference engine for Google's Gemma models.项目地址: https://gitcode.com/GitHub_Trending/ge/gemma.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考