数据截至 (上游 commit d7a2074112d2)
05 · server 与 CLI:slot 状态机与连续批处理
这一章讲什么:
tools/server怎么把单请求的 libllama 变成多并发服务,llama-cli为什么变成了「内嵌 server 的客户端」。这一层没有新的数学,全是工程:队列、状态机、批处理。
1. 它要解决的小问题
libllama 的一次 llama_decode 服务一个 context。做成服务要回答三个问题:
- 并发: 十个用户同时来请求,一个模型实例怎么接?
- 效率: 各请求进度不同,有的在读 prompt、有的在逐字生成,怎么把它们的计算拼进同一张 batch 而不互相等?
- 兼容: 客户端都按 OpenAI 的 API 写,得说同一种 HTTP 方言。
llama-server 的答案是 slot 状态机 + 逐节拍拼批。
2. 思路:slot 管状态,batch 管算力
直觉是把「对话」和「计算」解耦:
- slot = 一个请求的生命周期容器:持有自己的 KV 序列(seq_id)、自己的采样器链、自己的进度。slot 数 =
-np(n_parallel),是并发上限。 - server_batch = 一个节拍的「工单」:把所有活跃 slot 这一步该算的 token 收集起来,渲染成一张
llama_batch,一次 decode 全算掉。 - 每个节拍 = 一遍
update_slots():拼批 → decode → 逐 slot 采样并产出。
slot 状态机(tools/server/server-context.cpp:100-107):
| 状态 | 含义 |
|---|---|
IDLE | 空闲,可接新任务 |
STARTED | 已分配任务,准备读 prompt |
PROCESSING_PROMPT | 正在预填充 |
DONE_PROMPT | prompt 读完,本拍开始生成 |
GENERATING | 逐 token 生成中 |
(另有 WAIT_OTHER:子 slot 等父 slot 先处理共享前缀——prompt 缓存的配套状态。)
3. 图示:一个节拍
任务队列(HTTP 线程投递)
│
▼
update_slots() ← 每拍执行一次(tools/server/server-context.cpp:2677)
│
├─ pre_decode() ① 拼批
│ slot 满了做 context shift;把 GENERATING 的 slot 各添 1 个 token,
│ 把在填 prompt 的 slot 按配额添若干 token → batch.render()
│
├─ decode() ② 计算
│ 整批一次 llama_decode(太大就按 n_batch 切片重试)
│
└─ post_decode() ③ 分发
逐 slot:取出自己那个位置的 logits → 采样 → 流式回包
→ 到 EOS/上限则释放 slot
怎么读这张图: 三段的输入输出都是「batch」——slot 只在 ①③ 和 batch 打交道,从不直接碰模型。这就是「连续批处理」在 llama.cpp 的形态:没有 vLLM 那样的抢占式调度,靠每拍重新拼批实现「谁有活谁上车」。
4. 真实实现:拼批、计算、分发
4.1 拼批(pre_decode + render)
pre_decode(tools/server/server-context.cpp:2882)先处理 context 溢出:某 slot 的 prompt 将超出 n_ctx 时,做 context shift——保留前 n_keep 个 token,从 KV cache 里删掉中间一段(slot.mem.seq_rm),其余位置前移(seq_add 负偏移),见 tools/server/server-context.cpp:2924-2925。这让长对话永不中断,代价是被删段的历史「遗忘」。
然后把各 slot 待算的 token 收进 server_batch,render()(tools/server/server-context.cpp:201-213)摊平成 llama_batch:每行是 (token, pos, seq_id, 是否要 logits)。
4.2 计算(decode)
update_slots 把整批按 n_batch 上限切成若干段,逐段送 llama_decode(tools/server/server-context.cpp:2843-2876)。失败(典型:KV cache 放不下)会缩小批量重试——continue 分支带着更新后的 n_batch 再来一遍(tools/server/server-context.cpp:2862-2865)。
4.3 分发(post_decode)
post_decode(tools/server/server-context.cpp:3757)逐 slot 走:
- prompt 刚读完的(
DONE_PROMPT):embedding/rerank 任务直接出结果释放;生成任务转入GENERATING。 - 生成中的:取自己在 batch 里的下标
tok_idx = slot.i_batch - off,用自己的采样器链抽一个 token(common_sampler_sample,tools/server/server-context.cpp:3829),common_sampler_accept回灌状态,再按流式与否发 SSE 增量。
注意「每个 slot 一条采样器链」:不同请求可以带不同的 temperature/top_p,互不影响。
5. HTTP 层与 API 面
路由注册集中在 tools/server/server.cpp:246-277,OpenAI 兼容端点:
| 端点 | 用途 |
|---|---|
POST /v1/chat/completions | 聊天(流式/非流式) |
POST /v1/completions、/completion | 补全(含 legacy) |
POST /v1/embeddings、/v1/rerank | 向量 / 重排 |
GET /health、/props | 健康与元信息 |
HTTP 用的是单头文件库 cpp-httplib(README.md:122)。请求进队列(server-queue.cpp),节拍循环消费;响应走 SSE 逐 token 推。/health 刻意在模型加载完成前就可应答(tools/server/server.cpp:463 注释)——编排系统可以先把进程拉起来再等就绪。
值得一提的周边能力(都在这一层,不在 libllama):聊天模板与工具调用解析(common/chat*.cpp + jinja)、JSON Schema 强制约束输出(common/json-schema-to-grammar.cpp → 编进采样链的 grammar)、投机解码草稿模型(common/speculative.cpp)、prompt 前缀缓存复用(第 4 章 cell 共享的上层使用者)。
6. CLI:为什么变成 了 server 的客户端
llama-cli 经历了一次重构,现在它的形态值得单独说:
llama-cli 进程
│
├─ 起一个线程跑 llama_server()(随机空闲端口)
│ tools/cli/cli-server.h:31-56
│
└─ 主线程当 HTTP 客户端:POST /v1/chat/completions(SSE)
tools/cli/cli-context.cpp:350-365
证据链:cli_context::init 里若不指定 --server-base 就 server->start(params) 内嵌拉起(tools/cli/cli-context.cpp:118-130);生成走 generate_completion → client.post_sse("/v1/chat/completions", ...)(tools/cli/cli-context.cpp:350-365)。
这个决策的收益: 历史上 CLI 自己维护一套交互式生成循环(前缀匹 配、重生成、多轮状态),server 维护另一套;合并后只剩 server 一份逻辑,CLI 只负责终端 UI 和会话管理。代价是「命令行聊天」这件事现在默认过一遍 localhost HTTP——对本地工具体验无感,对架构清爽度是巨大的。
7. 关键细节/坑
-np同时切 KV cache。 总上下文n_ctx按 slot 数均分,开 4 并发每槽就只有 1/4 的上下文预算——「并发越高、单对话越短」是这个架构的硬约束。- context shift 默认行为要看参数。 不开
ctx_shift时超限直接报错结束(tools/server/server-context.cpp:2887-2894);开了则以遗忘中段为代价续写;多模态会话直接不支持(GGML_ABORT,tools/server/server-context.cpp:2896-2899)。 - 它是「够用就好」的调度。 没有优先级、没有抢占、没有分页 KV;峰值吞吐场景请去看 vLLM 的 PagedAttention 路线。
- slot 释放即遗忘。 默认不留存 KV;要靠 prompt cache 复用前缀得走共享任务/缓存机制,不是免费的。
8. 代码地图
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| server 入口 | tools/server/server.cpp / main.cpp | llama_server |
| 节拍主循环 | tools/server/server-context.cpp | server_context_impl::update_slots |
| 拼批 | tools/server/server-context.cpp | pre_decode、server_batch::render |
| 分发与采样 | tools/server/server-context.cpp | post_decode、process_token |
| slot 定义 | tools/server/server-context.cpp | server_slot、slot_state |
| 任务队列 | tools/server/server-queue.cpp | server_queue |
| HTTP 路由 | tools/server/server.cpp | 路由注册段(server.cpp:246-277) |
| CLI 主程序 | tools/cli/cli.cpp | llama_cli |
| CLI↔server 桥 | tools/cli/cli-server.h / cli-context.cpp | cli_server::start、cli_context::generate_completion |
| 量化工具 | tools/quantize/quantize.cpp | main(调 llama_model_quantize) |
| 困惑度评测 | tools/perplexity | (工具主程序) |