从 HTTP 请求到 Token:服务器与 CLI 工具
前置知识
- ›第 1–4 篇文章
- ›对 HTTP API 和 REST 规范有基本了解
从 HTTP 请求到 Token:服务器与 CLI 工具
前面几篇文章介绍的内容——GGML 张量、计算图、KV 缓存、解码循环——都属于 libllama 的范畴,这是一个对调用方式没有任何假设的 C/C++ 库。而 tools/ 目录下的应用层,才是 llama.cpp 与真实用户打交道的地方:一个兼容 OpenAI 接口的 HTTP 服务器、交互式 CLI、量化工具,以及更多实用程序。
本文将重点介绍服务器基于 slot 的并发模型、任务队列架构、API 接口设计,以及 llama.cpp 中一个颇具意思的设计决策:CLI 并没有实现自己的推理循环,而是直接复用了服务器的整套基础设施。
服务器架构概览
服务器架构围绕三个核心组件展开,分散在 tools/server/ 目录下的多个头文件中:
server_context 是整个系统的调度中心。它定义于 tools/server/server-context.h,采用 pimpl(指针隐藏实现)模式,将内部细节封装在 server_context_impl 之后。它负责管理模型、上下文、slot 以及主事件循环。
server_queue 负责任务的提交与分发。它定义于 tools/server/server-queue.h,提供线程安全的任务队列,支持延迟任务(当所有 slot 都繁忙时进行排队)、优先级调度,以及在空闲期间自动卸载模型的休眠模式。
server_task 代表一个工作单元。它定义于 tools/server/server-task.h,任务携带类型信息(补全、嵌入、取消等)和输入参数,还可以包含子任务以支持多步骤操作。
flowchart TD
HTTP["HTTP Request\n/v1/chat/completions"] --> ROUTES["Route Handler"]
ROUTES --> TASK["Create server_task"]
TASK --> QUEUE["server_queue.post()"]
QUEUE --> LOOP["Main Event Loop"]
LOOP --> SLOT["Assign to Slot"]
SLOT --> DECODE["llama_decode()"]
DECODE --> SAMPLE["Sample token"]
SAMPLE --> RESULT["server_task_result"]
RESULT --> SSE["SSE Stream / JSON Response"]
主事件循环在单线程(即"主循环"线程)中运行,按顺序处理队列中的任务。这是经过深思熟虑的设计——它避免了并发调用 llama_decode() 带来的复杂性,否则就需要维护多个上下文或精细的锁机制。并发能力由 slot 系统来实现。
Slot 系统
服务器通过"slot"抽象,在单个已加载模型上同时处理多个推理请求。每个 slot 代表一个独立的推理流,拥有各自的:
- 提示词状态与已生成的 token
- 采样参数和采样器链
- 在 KV 缓存中的位置
- 流式传输状态(用于 SSE 响应)
当新请求到来时,服务器将其分配给一个空闲 slot。主循环遍历所有活跃 slot,通过调用 llama_decode() 并传入各自的 batch 来推进每个 slot 的进度。这是一种协作式多任务机制——所有 slot 共享同一份模型权重和 llama_context,但各自在 KV 缓存中拥有独立的序列 ID。
sequenceDiagram
participant R1 as Request 1
participant R2 as Request 2
participant Q as server_queue
participant L as Main Loop
participant S1 as Slot 0
participant S2 as Slot 1
participant CTX as llama_context
R1->>Q: Completion task
R2->>Q: Completion task
Q->>L: Dequeue tasks
L->>S1: Assign Request 1
L->>S2: Assign Request 2
loop Update cycle
L->>S1: Prepare batch
L->>S2: Prepare batch
L->>CTX: llama_decode(combined batch)
CTX-->>S1: Logits for seq 0
CTX-->>S2: Logits for seq 1
S1->>R1: Stream token (SSE)
S2->>R2: Stream token (SSE)
end
slot 数量可在启动时通过 --parallel N 配置。每增加一个 slot,就会占用一部分上下文窗口——在 4 个 slot 和 n_ctx=8192 的配置下,每个 slot 大约有 ~2048 个位置可用(实际实现中 KV 缓存的分区方式更为精细)。
提示: 如果服务器只为单个用户服务,
--parallel 1是最优选择。每增加一个 slot 都会缩短单请求可用的上下文长度,并增加内存消耗。对于多用户并发场景,将--parallel设置为预期并发数,并相应地扩大--ctx-size。
API 接口与路由注册
服务器提供三类 API,在 tools/server/server.cpp 中完成注册:
兼容 OpenAI 的端点:
POST /v1/chat/completions— 支持流式输出的对话补全POST /v1/completions— 文本补全POST /v1/embeddings— 生成嵌入向量GET /v1/models— 列出可用模型
原生端点:
POST /completion— 原始补全,支持完整参数控制POST /tokenize— 对文本进行分词POST /detokenize— 将 token 还原为文本GET /health— 服务器健康检查GET /props— 服务器属性
兼容 Anthropic 的端点:
POST /v1/messages— Anthropic Messages API 格式
每个路由处理器遵循相同的处理模式:解析 JSON 请求、创建携带相应参数的 server_task、将其提交到队列,再通过 server_response_reader 等待结果。对于流式响应,结果以 Server-Sent Events(SSE)的形式发送。
所有路由处理器都包裹在 ex_wrapper() 中,它会捕获异常并转换为对应的 HTTP 错误响应——参数非法时返回 400,内部错误时返回 500。
CLI 复用服务器内部实现
llama.cpp 中最出人意料的架构决策,体现在 tools/cli/cli.cpp 中:
#include "server-context.h"
#include "server-task.h"
struct cli_context {
server_context ctx_server; // <-- the same server_context!
json messages = json::array();
// ...
};
交互式 CLI 没有实现自己的推理循环。cli_context 直接包装了一个 server_context,并通过与 HTTP 处理器完全相同的任务/队列接口与之交互。当你在 CLI 中输入一条消息时,它会创建一个 server_task,将其提交到服务器队列,并通过 server_response_reader 读取结果。
classDiagram
class server_context {
+load_model()
+start_loop()
+get_response_reader()
}
class server_http_context {
Routes: /v1/chat/completions
Routes: /v1/completions
Creates server_tasks
}
class cli_context {
+ctx_server: server_context
+messages: json
Creates server_tasks
}
server_context <-- server_http_context : "routes to"
server_context <-- cli_context : "wraps"
这一设计带来了几个显著优势:
-
功能对齐 — 服务器支持的所有功能(工具调用、多模态输入、语法约束、推测解码)在 CLI 中自动可用,无需额外实现。
-
统一代码路径 — 推理管道中的 bug 修复可以同时作用于服务器和 CLI,不存在两者逐渐出现分歧的风险。
-
简化测试 — CLI 实际上充当了服务器核心逻辑的集成测试,而无需启动完整的 HTTP 栈。
其代价是:CLI 需要承载服务器依赖(任务队列、slot 管理等)的额外开销,尽管它实际上只使用一个 slot。
公共工具层
common/ 目录提供了所有工具共享的基础设施:
参数解析 — common/arg.h 提供 common_params_parse(),负责处理各工具所接受的数百个 CLI 参数(模型路径、上下文大小、GPU 层数、采样参数等)。所有工具共用同一个 common_params 结构体。
采样封装 — common/sampling.h 在 libllama 底层 llama_sampler 链的基础上,提供了更高层的接口,统一处理配置、重置和参数管理。
对话模板引擎 — common/jinja/ 目录包含一个兼容 Jinja2 的模板引擎,用于应用对话模板。当模型在其 GGUF 元数据中(通过 tokenizer.chat_template 键)提供对话模板时,该引擎会将用户/助手/系统消息渲染为模型期望的格式。
flowchart TD
subgraph "common/ utilities"
ARG["arg.h / arg.cpp\nArgument parsing"]
SAMP["sampling.h\nSampling wrappers"]
CHAT["chat.h\nChat template application"]
JINJA["jinja/\nJinja2 template engine"]
LOG["log.h\nLogging utilities"]
end
subgraph "Tools"
CLI["tools/cli"]
SRV["tools/server"]
QNT["tools/quantize"]
BENCH["tools/bench"]
end
CLI --> ARG
CLI --> CHAT
SRV --> ARG
SRV --> SAMP
SRV --> JINJA
QNT --> ARG
BENCH --> ARG
工具生态与模型转换
除了服务器和 CLI,llama.cpp 还提供了多个专用工具:
tools/quantize/ — 在不同量化格式之间转换模型(例如 F16 → Q4_K_M)。它读取 GGUF 文件,按目标格式对每个张量重新量化,并输出新的 GGUF 文件。支持通过 --imatrix 使用重要性矩阵进行数据驱动的量化。
tools/bench/ — 对 prompt 处理和 token 生成吞吐量进行性能基准测试。
tools/perplexity/ — 通过在测试语料上计算困惑度(perplexity)来评估模型质量。
tools/imatrix/ — 从校准数据中计算重要性矩阵,用于提升量化质量。
tools/tts/ — 使用语音合成模型实现文字转语音功能。
tools/mtmd/ — 用于支持图像和音频输入的多模态模型工具。
模型转换 — convert_hf_to_gguf.py 脚本是将新模型引入 llama.cpp 的主要途径。它读取 HuggingFace 模型检查点(PyTorch .bin 或 SafeTensors .safetensors),将张量名称映射为 GGUF 约定,把超参数写入 GGUF 元数据,最终输出 .gguf 文件。gguf-py/ 库提供了转换器所使用的 Python GGUF 读写工具。
提示: 在量化模型时,Q4_K_M 通常是质量与体积之间最好的平衡点。在使用激进量化级别(Q2_K 及以下)时,建议配合校准数据集使用
--imatrix以获得最佳效果。
下一步
至此,我们已经走完了从 HTTP 请求到 token 输出的完整链路。本系列的最后一篇文章将转向实战:手把手带你为 llama.cpp 贡献一个新的模型架构,涵盖从 GGUF 转换、计算图构建到测试验证,每一个需要修改的文件都会逐一讲解。