ik_llama.cpp 的 Swift 批处理生成示例 llama-batched-swift 完整指南
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
本指南围绕 ik_llama.cpp 仓库中的 Swift 批处理示例 examples/batched.swift/README.md 展开,系统讲解如何使用 Swift 语言调用 llama 库实现「多序列并行生成」。读者将掌握该示例的构建方式、命令行用法、完整的源码执行流程(从模型加载、prompt 分词、KV 缓存规划到多流采样生成),以及它与 C++ 版 examples/batched/batched.cpp 的对应关系,可直接据此在 macOS 上运行或二次开发自己的 Swift 推理程序。
示例定位:Swift 克隆的 batched 示例
examples/batched.swift是 ik_llama.cpp 官方 C++ 示例examples/batched的 Swift 移植版。C++ 版batched用于演示"从给定 prompt 进行批处理生成"(batched generation):同一个 prompt 在同一个上下文中并行生成多条独立序列。Swift 版完整复刻了这一思路,用 Swift 的 Foundation 与 llama 库的 C API 实现了同样的多流生成逻辑。
两者在功能上完全对应:
- C++ 版通过
gpt_params命令行解析,支持-m、-p、-n、-np等参数; - Swift 版采用更精简的
CommandLine.arguments位置参数,用法为MODEL_PATH [PROMPT] [PARALLEL]。
从源码结构看,Swift 版刻意保持了与 C++ 版一致的算法骨架:先评估 prompt,再把 KV 缓存复制到各并行序列,最后逐 token 采样并组批解码。这使得它非常适合作为学习 llama 批处理 API 的 Swift 入门参考。
目录结构与构建方式
该示例共 4 个文件,结构如下:
examples/batched.swift/ ├── README.md # 使用说明 ├── Makefile # xcodebuild 构建脚本 ├── Package.swift # Swift Package 定义 └── Sources/ └── main.swift # 全部实现源码Makefile:一键构建
examples/batched.swift/Makefile 的内容如下:
.PHONY: build build: xcodebuild -scheme llama-batched-swift -destination "generic/platform=macOS" -derivedDataPath build rm -f ./llama-batched-swift ln -s ./build/Build/Products/Debug/llama-batched-swift ./llama-batched-swift它使用xcodebuild以llama-batched-swiftscheme 构建 macOS 平台产物,并把 Debug 目录下的可执行文件软链接到项目根目录。因此构建命令就是:
make构建产物即为根目录下的./llama-batched-swift。
Package.swift:依赖仓库根目录的 llama 包
examples/batched.swift/Package.swift 声明了包元信息:
swift-tools-version: 5.5,平台要求macOS(.v12)及以上;- 依赖声明
.package(name: "llama", path: "../../"),即把仓库根目录本身作为 Swift Package 依赖; executableTarget名为llama-batched-swift,源码路径为Sources,链接 Foundation 与 AppKit 框架。
值得注意的是,仓库根目录的 Package.swift 将 llama 库组织为llama这个 library target,其 sources 包含src/llama.cpp、src/llama-vocab.cpp、src/llama-sampling.cpp、ggml/src/ggml.c、ggml/src/ggml-backend.c等核心文件,并在 Darwin 平台追加ggml/src/ggml-metal.m与 Metal 资源、启用GGML_USE_ACCELERATE与GGML_USE_METAL宏——这意味着 Swift 示例在 macOS 上可以自动获得 Metal GPU 加速与 Accelerate 矩阵运算支持。头文件通过根目录的spm-headers(如 spm-headers/llama.h)暴露给 Swift 模块。
命令行用法与参数说明
按 examples/batched.swift/README.md,程序调用方式为:
./llama-batched-swift MODEL_PATH [PROMPT] [PARALLEL]三个位置参数的含义,结合 Sources/main.swift 第 4-17 行的解析逻辑可以确认:
| 参数 | 位置 | 默认值 | 说明 |
|---|---|---|---|
MODEL_PATH | 第 1 个 | 必填 | GGUF 模型文件路径,缺失时打印用法并退出(exit(1)) |
PROMPT | 第 2 个 | "Hello my name is" | 生成使用的提示词 |
PARALLEL | 第 3 个 | 1 | 并行序列数量n_parallel,仅在可转为整数时生效,否则取 1 |
一个典型调用(对应 C++ 版 README 中-np 4的效果):
./llama-batched-swift ./models/llama-7b-v2/ggml-model-f16.gguf "Hello my name is" 4与 C++ 版 examples/batched/README.md 中的./llama-batched -m ... -p "Hello my name is" -np 4相比,Swift 版将参数从命令行选项简化为位置参数,同时把n_len(序列总长度,含 prompt)硬编码为 32。
源码全流程解析
Sources/main.swift 共约 260 行,是整个示例的核心。下面按执行顺序逐段拆解。
1. 参数解析与常量
let modelPath: String = arguments[1] let prompt: String = arguments.count > 2 ? arguments[2] : "Hello my name is" let n_parallel: Int = arguments.count > 3 && Int(arguments[3]) != nil ? Int(arguments[3])! : 1 let n_len: Int = 32n_len为序列总长度(含 prompt token),硬编码为 32,与 C++ 版params.n_predict = 32对应。
2. 后端初始化与模型加载
llama_backend_init() defer { llama_backend_free() } let model_params = llama_model_default_params() guard let model = llama_load_model_from_file(modelPath.cString(using: .utf8), model_params) else { print("Failed to load model") exit(1) } defer { llama_free_model(model) }llama_backend_init()初始化 llama 后端,配合defer在程序退出时llama_backend_free()释放,体现 Swift 的资源管理风格;llama_load_model_from_file使用默认模型参数加载 GGUF 模型,失败即退出。
3. Prompt 分词与 KV 缓存容量规划
var tokens = tokenize(text: prompt, add_bos: true) let n_kv_req = UInt32(tokens.count) + UInt32((n_len - Int(tokens.count)) * n_parallel)tokenize(第 217-228 行)封装了llama_tokenizeC API:按 UTF-8 字节数分配 token 缓冲,add_bos加首 token,special tokens传false,把 C 指针结果拷贝进 Swift 数组后释放内存。
关键公式n_kv_req = tokens + (n_len - tokens) * n_parallel与 C++ 版 examples/batched/batched.cpp 第 57 行的n_kv_req = tokens_list.size() + (n_predict - tokens_list.size())*n_parallel完全一致。它估算出所有并行序列合计需要的 KV 缓存 token 数:prompt 共享一次,之后每个序列各占(n_len - tokens)个新位置。
4. 上下文创建与容量校验
var context_params = llama_context_default_params() context_params.seed = 1234 context_params.n_ctx = n_kv_req context_params.n_batch = UInt32(max(n_len, n_parallel)) context_params.n_threads = 8 context_params.n_threads_batch = 8 let context = llama_new_context_with_model(model, context_params) let n_ctx = llama_n_ctx(context) if n_kv_req > n_ctx { print("error: n_kv_req (%d) > n_ctx, the required KV cache size is not big enough\n", n_kv_req) exit(1) }seed = 1234固定随机种子,保证可复现;n_ctx直接设为所需的n_kv_req;n_batch = max(n_len, n_parallel)保证单次llama_decode能容纳整个 prompt 或一整轮的多序列 token;- 线程数硬编码为 8(C++ 版则走
gpt_params默认值); - 若估算的 KV 需求超过实际上下文容量则报错退出,与 C++ 版的防御逻辑一致。
5. 构造 batch 并评估 prompt
var batch = llama_batch_init(max(Int32(tokens.count), Int32(n_parallel)), 0, 1) batch.n_tokens = Int32(tokens.count) for (i, token) in tokens.enumerated() { batch.token[i] = token batch.pos[i] = Int32(i) batch.n_seq_id[i] = 1 if let seq_id = batch.seq_id[i] { seq_id[0] = 0 } batch.logits[i] = 0 } batch.logits[Int(batch.n_tokens) - 1] = 1 if llama_decode(context, batch) != 0 { print("llama_decode() failed") exit(1) }这是理解批处理 API 的关键:
llama_batch_init(max(tokens, n_parallel), 0, 1)预分配 batch 槽位,第三个参数1是每个 token 的序列 ID 数量上限;- 每个 prompt token 填入
token、pos(位置)、seq_id[0] = 0(归属序列 0); - 只有最后一个 prompt token 的
logits置 1,llama_decode只为需要 logits 的 token 计算输出——这正是 C++ 版第 124-125 行注释"llama_decode will output logits only for the last token of the prompt"的机制; - 源码第 84-88 行以 TODO 注释的形式演示了 Swift 中访问
batch.seq_id[i]可选指针的写法(C++ 里是直接的batch.seq_id[i][0])。
6. 复制 KV 缓存到各并行序列
for i in 1 ..< n_parallel { llama_kv_cache_seq_cp(context, 0, Int32(i), 0, batch.n_tokens) }llama_kv_cache_seq_cp将序列 0(prompt 计算得到的 KV 状态)复制到序列i,区间为[0, batch.n_tokens)。这样所有并行序列共享 prompt 的 KV 结果而无需重复计算。C++ 版第 132-136 行保留了同样的 API 调用(注释掉的版本用-1, -1表示全区间),两者互为印证。
7. 生成主循环:采样与组批
var streams: [String] = .init(repeating: "", count: n_parallel) var i_batch = Int32 var n_cur = batch.n_tokens var n_decode = 0 let t_main_start = ggml_time_us() while n_cur <= n_len { batch.n_tokens = 0 for i in 0 ..< n_parallel { if i_batch[i] < 0 { continue } // 该流已结束 let logits = llama_get_logits_ith(context, i_batch[i]) var candidates: [llama_token_data] = ... // top_k=40, top_p=0.9, temp=0.4 llama_sample_top_k(context, &candidates_p, top_k, 1) llama_sample_top_p(context, &candidates_p, top_p, 1) llama_sample_temp(context, &candidates_p, temp) let new_token_id = llama_sample_token(context, &candidates_p) if llama_token_is_eog(model, new_token_id) || n_cur == n_len { i_batch[i] = -1 continue } // 追加 token 到 batch,准备下一轮解码 batch.token[Int(batch.n_tokens)] = new_token_id batch.pos[Int(batch.n_tokens)] = n_cur batch.n_seq_id[Int(batch.n_tokens)] = 1 seq_id[0] = Int32(i) batch.logits[Int(batch.n_tokens)] = 1 i_batch[i] = batch.n_tokens batch.n_tokens += 1 n_decode += 1 } if batch.n_tokens == 0 { break } n_cur += 1 if llama_decode(context, batch) != 0 { ... } }这段循环是整个批处理的核心机制,值得重点理解:
- 每轮先清空 batch,再为每个活跃流采样一个 token,因此一次
llama_decode同时推进多条序列——这就是并行解码的效率来源; - 采样链路为
top_k(40) → top_p(0.9) → temp(0.4) → llama_sample_token,与 C++ 版 batched.cpp 第 179-187 行的参数与顺序逐一对应;注释中还保留了llama_sample_token_greedy的贪心采样替代方案; i_batch[i]记录每个流最新 token 在 batch 中的索引,下一轮据此取llama_get_logits_ith拿到该流的 logits;- 遇到 EOG(end of generation)token 或达到
n_len时,将该流标记为-1结束; - 单流模式(
n_parallel == 1)下逐 token 立即打印到 stdout,多流模式则累积到streams[i],最后统一输出完整序列; - 第 156 行
llama_token_is_eog用于识别模型定义的结束 token(如 EOS),保证生成能自然终止。
8. 多字节 UTF-8 的 token 解码缓冲
token_to_piece(第 230-261 行)封装了llama_token_to_piece,处理了 Swift 字符串拼接多字节字符的经典坑:
- 先用 8 字节小缓冲探测;若返回负数,说明实际字节数更多,则按
-nTokens重新分配精确缓冲; - 当解码出的字节可能构成不完整的多字节 UTF-8 序列(例如 CJK 字符被拆到相邻 token)时,先把字节暂存到
buffer,凑满 4 字节(UTF-8 单字符最大长度)或能组成合法字符串后再转为String,从而避免乱码。
这正是"多流累积打印"场景下保证中文等字符输出正确的关键实现,也是相比 C++ 版common_token_to_piece需要多处理的 Swift 特有部分。
9. 计时与统计
let t_main_end = ggml_time_us() print("decoded \(n_decode) tokens in ... s, speed: ... t/s") llama_print_timings(context)ggml_time_us()统计解码耗时并计算 tokens/s;llama_print_timings(context)输出 load time、sample time、prompt eval time、eval time、total time 等详细分解,与 C++ 版输出格式一致。
运行效果示例
参照 C++ 版 examples/batched/README.md 的运行输出,Swift 版以-np 4等价参数(第 3 参数传 4)运行时会打印:
n_len = 32, n_ctx = ..., n_batch = ..., n_parallel = 4, n_kv_req = ... generating 4 sequences ... sequence 0: Hello my name is ... sequence 1: Hello my name is ... sequence 2: Hello my name is ... sequence 3: Hello my name is ... decoded N tokens in X.XX s, speed: XX.XX t/s即同一个 prompt 派生出 4 条内容各异的续写(因seed=1234固定,可重复复现),并给出整体解码速度。C++ 版 README 中的示例输出展示了 n_parallel=4 时全部 4 个流在约 3.57 秒内解码 108 token、约 30.26 t/s 的典型结果,Swift 版输出结构与其一一对应。
与 C++ 版的关键差异速览
| 维度 | C++ 版 batched | Swift 版 llama-batched-swift |
|---|---|---|
| 参数形式 | -m/-p/-n/-np选项 | 位置参数MODEL_PATH [PROMPT] [PARALLEL] |
| 模型/上下文参数 | 走gpt_params默认值与命令行覆盖 | llama_model_default_params()+ 硬编码 seed/n_threads |
| batch 填充 | common_batch_add辅助函数 | 手动赋值token/pos/n_seq_id/seq_id/logits字段 |
| token 解码 | common_token_to_piece | 自实现token_to_piece,含 UTF-8 缓冲 |
| 构建 | CMake 目标 | make(内部 xcodebuild)+ SwiftPM |
| 目标平台 | 全平台 | macOS 12+(底层可走 Metal) |
扩展建议与注意事项
- 更换模型:把
MODEL_PATH换成任意 GGUF 文件即可,例如仓库 models 目录下的模板模型或自行量化的模型; - 调整并行度:
PARALLEL参数越大,单轮解码的 batch 越满,理论上越能发挥 GPU/多核并行;但需注意n_kv_req随n_parallel线性增长,超出n_ctx会触发源码第 60-63 行的容量校验错误; - 修改生成长度:
n_len = 32硬编码于 main.swift 第 17 行,二次开发时可改为从参数读取; - 采样策略:若想要确定性输出,可参考注释改用
llama_sample_token_greedy;若要更高多样性,可调大temp; - 学习价值:该示例是理解
llama_batch结构与llama_decode批处理语义的最小完整 Swift 实现,配合 C++ 版 batched.cpp 对照阅读,可快速掌握 llama 库的底层解码流程。
总结
examples/batched.swift以不到 300 行的 Swift 代码完整复刻了 ik_llama.cpp 的批处理生成示例,覆盖了后端初始化、模型加载、prompt 分词、KV 容量规划、batch 构造、KV 序列复制、多流采样组批、UTF-8 缓冲解码与计时统计的全链路。它既是 Swift 开发者上手 llama 库的最佳范例,也是理解并行解码与 KV 缓存复用的直观教材。阅读本文后,你可以直接make构建并运行./llama-batched-swift MODEL_PATH [PROMPT] [PARALLEL],并以此为骨架扩展出自己的 Swift 推理应用。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考