跳到主要内容

数据截至 (上游 commit d7a2074112d2)

05 · server 与 CLI:slot 状态机与连续批处理

这一章讲什么: tools/server 怎么把单请求的 libllama 变成多并发服务,llama-cli 为什么变成了「内嵌 server 的客户端」。这一层没有新的数学,全是工程:队列、状态机、批处理。


1. 它要解决的小问题

libllama 的一次 llama_decode 服务一个 context。做成服务要回答三个问题:

  1. 并发: 十个用户同时来请求,一个模型实例怎么接?
  2. 效率: 各请求进度不同,有的在读 prompt、有的在逐字生成,怎么把它们的计算拼进同一张 batch 而不互相等?
  3. 兼容: 客户端都按 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_PROMPTprompt 读完,本拍开始生成
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-baseserver->start(params) 内嵌拉起(tools/cli/cli-context.cpp:118-130);生成走 generate_completionclient.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.cppllama_server
节拍主循环tools/server/server-context.cppserver_context_impl::update_slots
拼批tools/server/server-context.cpppre_decodeserver_batch::render
分发与采样tools/server/server-context.cpppost_decodeprocess_token
slot 定义tools/server/server-context.cppserver_slotslot_state
任务队列tools/server/server-queue.cppserver_queue
HTTP 路由tools/server/server.cpp路由注册段(server.cpp:246-277)
CLI 主程序tools/cli/cli.cppllama_cli
CLI↔server 桥tools/cli/cli-server.h / cli-context.cppcli_server::startcli_context::generate_completion
量化工具tools/quantize/quantize.cppmain(调 llama_model_quantize)
困惑度评测tools/perplexity(工具主程序)