十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ik_llama.cpp 中的 Lookahead Decoding 实战指南:基于 n-gram 并行验证的自回归加速示例

ik_llama.cpp 中的 Lookahead Decoding 实战指南:基于 n-gram 并行验证的自回归加速示例 ik_llama.cpp 中的 Lookahead Decoding 实战指南基于 n-gram 并行验证的自回归加速示例【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp导读本文围绕 ik_llama.cpp 仓库中的 examples/lookahead/README.md 与配套示例程序 lookahead.cpp 展开系统讲解 lookahead decoding前瞻解码技术在该项目中的完整落地方式。读者读完本文后将掌握lookahead decoding 的核心原理n-gram 缓存 并行验证、W/N/G 三个关键超参数的含义与作用、示例程序如何通过多序列 batch 与 KV cache 序列操作实现一次前向解码验证多个 token以及如何编译、运行并用日志指标评估该技术的实际效果。文中所有实现细节均可在仓库源码中逐一对应验证。什么是 Lookahead DecodingLookahead decoding 是一种与 speculative decoding 类似的自回归加速技术由 lmsys 团队于 2023 年 11 月提出。其核心思想是利用 n-gram 缓存n-gram pool保存历史生成中频繁出现的 token 序列在每轮解码时以这些历史 n-gram 作为草稿并行提交给模型验证一次前向解码即可确认多个 token从而减少模型调用次数、提升生成吞吐。该示例在 ik_llama.cpp 中的定位是一个演示Demonstration程序正如 README 开篇所述Demonstration of lookahead decoding technique它的作用不是替代正式生产路径例如 speculative.cpp 提供的完整推测解码实现而是以最小可读的代码直观展示 lookahead decoding 的三个核心机制如何在 llama.cpp 的底层 API 上运作n-gram 容器记录历史 token 序列多序列并行 batch把验证 n-gram 与 lookahead token 一次性交给llama_decodeKV cache 序列操作通过llama_kv_cache_seq_cp/llama_kv_cache_seq_rm/llama_kv_cache_seq_keep在多个序列之间复制、清理缓存避免重复计算。从源码结构看lookahead.cpp 全部 486 行都围绕上述机制展开是学习 llama.cpp 多序列 batch 与 KV cache 高级 API 的优秀范例。核心超参数W、N、G示例在 lookahead.cpp 中硬编码了三个超参数它们是 lookahead decoding 的形状参数参数默认值含义代码位置W15lookahead window每轮生成的并行 lookahead token 数也是参与验证的历史 Jacobi 迭代窗口宽度const int W 15;N5n-gram 大小缓存与验证的 token 序列长度const int N 5;G15max verification n-grams每个首 token 下最多缓存的 n-gram 条数环形缓冲区容量const int G 15;理解这三个参数需要结合 n-gram 容器的内存布局。在 lookahead.cpp 中struct ngram_container { ngram_container(int n_vocab, int N, int G) { cnt.resize(n_vocab); head.resize(n_vocab); tokens.resize(n_vocab * G * (N - 1)); } int n_total 0; std::vectorint cnt; std::vectorint head; // [n_vocab][G][N - 1] // for each token of the vocab, keep a ring-buffer of capacity G of n-grams of size N - 1 std::vectorllama_token tokens; };这里的关键设计是容器按词汇表首 token索引n_vocab维即以某个 token 开头的所有历史 n-gram 归为一组每个首 token 下维护一个容量为 G 的环形缓冲区每个 n-gram 存储N - 1个后续 token首 token 由数组下标隐式确定不占用存储见注释the first token of the n-gram is determined by the index in the container so it is not storedcnt[ft]记录以 tokenft开头的已缓存 n-gram 数量head[ft]是环形缓冲区写指针。因此当 W15、N5、G15 时每个首 token 最多缓存 15 条长度为 4 的 n-gram。总内存为n_vocab * G * (N - 1)个 token与词表大小成正比——这也是 G 不宜过大的原因之一。整体流程从初始化到生成循环lookahead.cpp 的主流程可分为四个阶段1. 初始化与 prompt 处理程序复用 common 库的参数解析与模型加载与 main 示例一致gpt_params_parse解析命令行参数llama_init_from_gpt_params完成模型与上下文初始化common_tokenize对 prompt 分词。随后将 prompt 拆成两次llama_decode执行llama_decode(ctx, llama_batch_get_one( inp.data(), n_input - 1, 0, 0)); llama_decode(ctx, llama_batch_get_one(inp.back(), 1, n_input - 1, 0));2. 创建 WG1 条并行序列lookahead decoding 的验证阶段需要同时维护多条候选序列它们共享同一份 KV cachellama_kv_cache_seq_cp只是把 token 归属到新序列不额外分配内存见 llama.h 中 this does not allocate extra KV cache memory 的说明。序列编号约定如下代码注释原话// for each decoded batch, we have at most W G 1 distinct sequences: // seq_id 0 : the current input token // seq_id [1, W] : tokens from the past N - 1 Jacobi iterations // seq_id [W 1, W G] : verification n-grams初始化时先把主序列 0 复制给其余所有序列for (int s 1; s W G 1; s) { llama_kv_cache_seq_cp(ctx, 0, s, -1, -1); }batch 容量也按W G 1个序列、n_ctx个 token 初始化llama_batch batch llama_batch_init(params.n_ctx, 0, W G 1);3. 初始化 lookahead tokenJacobi 迭代窗口tokens_j是一个(N-1) × W的二维数组保存过去 N-1 轮 Jacobi 迭代的 lookahead token。示例提供了两种初始化方式默认关闭随机初始化采用递增序号for (int j 0; j N - 1; j) { tokens_j[j].resize(W); for (int i 0; i W; i) { if (0) { // initialize randomly from the prompt tokens tokens_j[j][i] all[1 rand() % (all.size() - 1)]; } else { // initialize with a sequence of increasing numbers tokens_j[j][i] 100 i; } } }代码中保留了两条注释路径随机采样自 prompt token或以递增数字序列初始化。这些初始 token 在后续迭代中会逐步被真实采样 token 替换因此在演示场景下对最终生成质量影响有限。4. 生成主循环主循环while (true)每轮做四件事组装 batch把当前 token、验证 n-gram、剩余 lookahead token 一次性写入 batchllama_decode并行前向一次推理得到所有候选位置的 logits逐 token 验证与采样沿 n-gram 链依次验证命中则接受并继续KV cache 清理与序列重建移除未命中内容把最佳序列复制回序列 0 再分发给其余序列。batch 组装三种 token 的并行编排batch 组装逻辑对应代码中的掩码示意图W5, N4, G2 的例子来自源码注释// Batch: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // T: -2 -2 -2 -2 -1 -1 -1 -1 -1 0 0 0 0 0 0 // Info: I L L L L L L L L L L L L L L V V V V V V // Pos: 0 1 2 3 4 1 2 3 4 5 2 3 4 5 6 1 2 3 1 2 3 ( n_past) // Logits: 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1batch 中每个 token 可以同时属于多个序列这正是common_batch_add的seq_ids参数的作用见 common.cpplogits 标记为 true 的位置才会输出 logits 用于采样。组装顺序分三部分a当前 token属于全部W G 1个序列需要 logitscommon_batch_add(batch, id, n_past, seq_id_all, true);b验证 n-gram先从容器中取出以当前 token 开头的g_cur条 n-gram每条作为独立序列W 1 g排队const int g_cur ngrams_observed.cnt[id]; ngrams_cur.resize(g_cur); for (int g 0; g g_cur; g) { ... ngrams_cur[g].seq_id W 1 g; ... } for (int j 0; j N - 1; j) { for (int g 0; g g_cur; g) { const int idx id*(N - 1)*G g*(N - 1); const llama_token t ngrams_observed.tokens[idx j]; ngrams_cur[g].tokens [j 1] t; ngrams_cur[g].i_batch[j 1] batch.n_tokens; common_batch_add(batch, t, n_past j 1, { W 1 g }, true); } }代码注释特别说明验证 n-gram 要排在 lookahead token 之前queue this before the lookahead tokens for less KV cache fragmentation减少 KV cache 碎片化。clookahead token第一层j0的 token 属于多个序列其余层只属于单序列// fill the remaining W - 1 tokens for the first level for (int i 1; i W; i) { seq_id_look.resize(W - i); for (int j 0; j W - i; j) { seq_id_look[j] i j 1; } common_batch_add(batch, tokens_j[0][i], n_past i, seq_id_look, false); } // fill the rest of the levels for (int j 1; j N - 1; j) { for (int i 0; i W; i) { common_batch_add(batch, tokens_j[j][i], n_past j i, { i 1 }, j N - 2); } }注意第一个 lookahead 位置i0即紧邻当前 token 的位置已由验证 n-gram 序列覆盖因此这里从 i1 开始填充。logits 仅在最后一层j N - 2置 true——验证只需要每层第一个位置的 logits 与最后一个 lookahead 层这大幅减少了 logits 计算开销。验证与接受一次前向解出多个 tokenllama_decode成功后程序进入验证循环for (int v 0; v N; v)v 0对 batch 中第 0 个位置当前 token 的下一位采样得到新 tokenidv 0从仍处于 active 的验证 n-gram 中取对应层位置采样若某条 n-gram 的下一 token 与已采样 token 不一致则将其标记为 inactivengrams_cur[g].active false一旦所有 active n-gram 都失配i_batch 0立即跳出循环本轮只接受已确认的 token。验证匹配的核心代码// verify across active n-grams for (int g 0; g (int) ngrams_cur.size(); g) { if (ngrams_cur[g].active) { if (v N - 1) { ngrams_cur[g].active false; } else { if (id ! ngrams_cur[g].tokens[v 1]) { ngrams_cur[g].active false; } } } }被接受 token 的打印带颜色区分首 token 普通输出后续被验证接受的 token 以浅青色\033[0;96m输出方便肉眼观察每次一次解出多个 token的效果。每次接受都会更新 lookahead token 窗口tokens_j逐层上移并把新观察到的 n-gram 写入容器。n-gram 写入前会做去重过滤is_unique检查并遵循环形缓冲区写指针head[ft]的循环推进ngrams_observed.cnt[ft] std::min(G, ngrams_observed.cnt[ft] 1); ngrams_observed.head[ft] (head 1) % G;注意cnt用std::min(G, ...)封顶保证环形缓冲区不会越界覆盖未初始化区域这是tokens数组按n_vocab * G * (N - 1)精确分配的前提。KV cache 管理序列复制、裁剪与碎片化控制KV cache 是本示例最依赖底层 API 的部分涉及三个核心函数全部定义于 llama.hAPI作用说明llama_kv_cache_seq_cp(ctx, src, dst, p0, p1)把序列 src 的 token 归属复制给序列 dst不分配新 KV 内存只是共享归属llama.hp0 0表示[0, p1]p1 0表示[p0, inf)llama_kv_cache_seq_rm(ctx, seq_id, p0, p1)删除序列中指定位置区间的 KV 单元seq_id -1表示作用于所有序列llama.hllama_kv_cache_seq_keep(ctx, seq_id)仅保留属于指定序列的 token删除其余用于收敛到最佳序列llama.h每轮生成结束时的清理逻辑// KV cache management // if no verification token matched, we simply remove all cells from this batch - no fragmentation llama_kv_cache_seq_rm(ctx, -1, n_past, -1); if (seq_id_best ! 0) { // if a verification token matched, we keep the best sequence and remove the rest // this leads to some KV cache fragmentation llama_kv_cache_seq_keep(ctx, seq_id_best); llama_kv_cache_seq_cp (ctx, seq_id_best, 0, -1, -1); llama_kv_cache_seq_rm (ctx, seq_id_best, -1, -1); for (int s 1; s W G 1; s) { llama_kv_cache_seq_cp(ctx, 0, s, -1, -1); } }两条分支的注释非常关键无验证命中直接删除本 batch 新增位置n_past之后的所有 KV 单元不产生碎片有验证命中用seq_keep保留最佳序列、seq_cp把它复制回序列 0、seq_rm删除其余再重新分发到所有候选序列——这一路径必然产生一定 KV cache 碎片。示例还支持--dump-kv-cache参数params.dump_kv_cache配合llama_kv_cache_view_update与llama_kv_cache_dump_view_seqs实时打印 KV cache 中各序列的分布直观观察序列复制与碎片化过程if (dump_kv_cache) { llama_kv_cache_view_update(ctx, kvc_view); llama_kv_cache_dump_view_seqs(kvc_view, 40); }编译与运行编译示例通过 examples/CMakeLists.txt 的add_subdirectory(lookahead)纳入标准 CMake 构建目标名为llama-lookahead见 examples/lookahead/CMakeLists.txt。构建方式与仓库其他示例一致# 在仓库根目录执行 cmake -B build cmake --build build --config Release --target llama-lookahead -j运行与 main 示例 相同示例复用gpt_params解析体系因此--model、--prompt、-n生成 token 数、-c上下文大小等标准参数均可用# 假设模型为 GGUF 格式 ./build/bin/llama-lookahead -m /path/to/model.gguf \ -p Once upon a time \ -n 128 \ -c 2048运行结束后程序会输出性能统计lookahead.cppencoded 7 tokens in 0.021 seconds, speed: 333.333 t/s decoded 128 tokens in 4.215 seconds, speed: 30.367 t/s W 15 N 5 G 15 n_predict 128 n_accept 45其中n_predict是实际生成含验证通过的 token 总数n_accept是通过 n-gram 验证被一次接受的 token 数。两者之差近似等于实际执行的llama_decode轮数因此n_accept / n_predict可以粗略衡量并行验证的收益比值越高说明每轮前向解出的 token 越多理论加速比越大。该数值与文本的重复模式密度高度相关——重复性强的文本代码、模板、常见短语命中率更高这是 lookahead decoding 天然的适用场景。需要注意W、N、G 三个参数当前是编译期硬编码常量const int调整它们需要修改 lookahead.cpp 后重新编译。同时由于 n-gram 容器按词表大小分配n_vocab * G * (N - 1)的存储超大词表模型如 DeepSeek 类 128K 词表会显著放大 G 的内存占用调参时应权衡。与仓库其他推测解码路径的关系lookahead decoding 与仓库中成熟的 speculative decoding对应 speculative 示例同属草稿-验证范式但机制不同speculative decoding需要一个额外的草稿模型draft model或 n-gram 草稿器ngram-cache相关实现见 common/ngram-cache.cpp由草稿模型快速生成候选 token再由目标模型一次验证lookahead decoding不需要额外模型完全依赖自生成的 n-gram 缓存作为草稿来源属于单模型的自举式加速。从源码结构看lookahead 示例的 n-gram 容器ngram_container与common/ngram-cache模块在理念上同源但实现独立、聚焦教学演示。若读者希望在生产环境使用更完整的推测解码能力可参考 speculative.cpp 与 examples/speculative 的完整实现。小结本文以 examples/lookahead/README.md 为纲结合 lookahead.cpp 的源码逐段拆解了 ik_llama.cpp 中 lookahead decoding 的完整实现原理用历史 n-gram 作为草稿通过多序列 batch 一次前向验证多个 token参数Wlookahead 窗口、Nn-gram 长度、G每首 token 缓存条数共同决定并行度与内存占用实现common_batch_add组装多序列 batch、llama_decode并行前向、三层 KV cache 序列 API 管理缓存生命周期验证n_accept指标可直接观测验证命中量配合--dump-kv-cache可观察 KV cache 序列布局与碎片化。该示例是学习 llama.cpp 多序列 batch 与 KV cache 高级 API 的最小可运行范本无论是理解 lookahead decoding 原理还是想在此基础上实验 W/N/G 参数对生成速度的影响它都是理想的起点。【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表