剖析llama.cpp服务端Slot的本质——每个Slot对应独立KV cache和位置计数器,以及连续批处理如何通过共享前向传播实现近乎免费的并发。
一个 llama.cpp server slot 就是模型上的一个座位:一个对话的 KV 缓存、一个位置计数器、一组采样器状态。座位的数量和每个座位的长度是同一个预算,而 --parallel 就是你分割它的方式。
服务器加载一个模型,通过给每个进行中的请求分配一个 slot 来为其提供服务。一个 slot 拥有 KV 缓存的一个区域以及与之配套的簿记工作;默认开启的 continuous batching 然后将每个繁忙 slot 的 token 交错插入共享的前向传递,这正是并发之所以值得拥有的原因——批大小为 1 的生成会让算术单元闲置,而用第二个和第三个序列填满它们几乎不花额外成本。
-np,即 --parallel,设置 slot 的数量。它的默认值是 -1,即自动。当每个 slot 都忙时到达的请求不会被拒绝;而是延迟到某个 slot 空闲,这体现为延迟而非错误,并在 Prometheus 输出中单独计数为 llamacpp:requests_deferred。
-c 不是每个对话获得的上下文大小,而是总量,llama.cpp 在 src/llama-context.cpp 中从中推导出每个序列的数值:
if (cparams.kv_unified) {
cparams.n_ctx_seq = cparams.n_ctx;
} else {
cparams.n_ctx_seq = cparams.n_ctx / cparams.n_seq_max;
cparams.n_ctx_seq = GGML_PAD(cparams.n_ctx_seq, 256);
}
在非 unified 的情况下,就是简单地除以序列数量,然后向上填充到 256 的倍数。所以 -c 32768 -np 4 会给出四个各 8192 token 的 slot,而一个需要 12k prompt 的请求会在宣称上下文为 32k 的服务器上失败。这种意外是这两个参数最常见的误读。
填充还有一个后果。因为 n_ctx 之后会被重新计算为 n_ctx_seq × n_seq_max,一个不可整除的配对会被调整,llama.cpp 会说明这一点:n_ctx is not divisible by n_seq_max - rounding down to N。你还会看到 n_ctx_seq (X) < n_ctx_train (Y) —— 模型的全部容量将不会被利用,或者相应的溢出警告,这两行是算术落到了你预期位置的最快确认。
上面那个分支是值得读两遍的部分。通过 --kv-unified —— 一个跨所有序列共享的 KV 缓冲区 —— n_ctx_seq 被设置为整个 n_ctx,各 slot 从一个公共池中获取,而不是各自拥有固定的一片。当你的流量不均匀时这更好,因为一个长对话可以使用大部分缓存,而三个短的只使用很少,而不是每个 slot 都预留四分之一但可能永远用不到。
这对标志是 -kvu、--kv-unified 和 -no-kvu、--no-kv-unified,文档中记录的默认值在 slot 数量为自动时是启用的。所以"-c 会划分吗"这个问题的答案是:取决于你处于哪种模式,而日志行中命名的 n_ctx_seq 是你运行时的权威。共享池不是免费的——序列现在会争抢容量,所以一个长的会挤掉一个短的,失败模式从"确定的 per-slot 限制"变成了"取决于还有什么在运行"。
GET /slots,默认启用,返回每个 slot 一个对象,包含其 id、n_ctx、是否正在处理、实际生效的采样参数以及一个包含 n_decoded 和 n_remain 的 next_token 块。从这个响应中读取 n_ctx 是直接检查上述划分的方式,而不是靠推理。
这个响应上还有两个字段对容量工作很重要。一个请求可以指定 id_slot 来将自身固定到特定 slot,默认为 -1 表示"任意空闲的",这偶尔有用但通常是个错误:固定会破坏调度器并让其他 slot 闲置。而且因为 prompt 缓存是 per-slot 的,一个对话落在哪个 slot 决定了它的前缀是否仍被缓存——这是为什么在负载下第二轮有时快有时慢的原因。这个交互是 prompt 缓存页的主题。
curl -s http://127.0.0.1:8080/slots | jq '.[] | {id, n_ctx, is_processing}'
# and the aggregate view
curl -s http://127.0.0.1:8080/metrics | grep -E 'busy_slots|requests_deferred'
llamacpp:n_busy_slots_per_decode 这个数字告诉你 slot 数量是否在起作用:如果在请求被延迟时它接近 1,则瓶颈不在并发;如果它接近你的 slot 数量,那么增加 slot 会有帮助——直到内存阻止你。
从内存开始,而不是从你想要的并发开始。KV 缓存是随两个数字都 scaling 的部分,在非 unified 的情况下总量由 -c 单独决定——分割改变的是谁可以使用它,而不是有多少。随 slot 数量改变的是计算缓冲区和调度,而不是缓存大小,所以真正的问题是:你更偏好哪种形式的失败:一种可预测地拒绝长 prompt 的 per-slot 限制,还是一个长对话会降级所有人的共享池。
然后从你必须接受的最长 prompt 来设置 slot 数量。如果那个 prompt 是 16k 而你不在运行 unified 模式,-c 必须至少是 slot 数量乘以 16k。如果这个乘积放不下,--parallel 是需要让步的地方,因为一个延迟的请求只是等待,而一个被截断的请求是错误的。"这能否放下"背后的每 token 内存算术在 context-size 页面中有详细展开,而决定来自多个 slot 的 token 如何被打包到一次传递中的批处理标志在 batch 和 ubatch 页面中。
棘手的情况是一个没有任何 slot 能容纳的请求——一个 40k token 的文档到达一个 slot 大小为 8k 的服务器。在本地没有答案,只能用不同标志重启,所以这是人们把托管的长上下文模型放在本地模型后面的边界。这意味着一个调用点同时与两个具有不同限制和不同流式形状的后端通信;像 Multigrid 这样的网关存在就是为了把这变成一条路由规则而不是第二个客户端。
llama.cpp's --ctx-size Flag and What It Costs in Memory
Batch and Micro-Batch Size Flags in llama.cpp
Prompt Caching in llama.cpp's Server