SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-22

自托管LLM为何5个用户就卡顿?并发配置与KV缓存

Ollama默认并行请求数为1,5个用户中4个会排队。本文解释连续批处理、KV缓存、prefill、Decode和队列深度如何共同决定LLM服务器的并发上限。

为什么自托管 LLM 在用户增多时会变慢?

自托管 LLM 在有 5 个并发用户时出现停顿,是因为服务器仍然一次只生成一条回复,另外 4 个请求只能排队等待。Ollama 文档明确说明了默认设置:OLLAMA_NUM_PARALLEL 是“每个模型同时处理的最大并行请求数,默认为 1”。系统没有故障。5 个用户中有 4 个正在等待处理。

解决办法通常不是更换更大规格的服务器,而是使用能在同一次前向传播中处理多个请求的推理引擎,并准备足够的额外内存来保存所有用户的会话上下文。这两部分都很重要,但真正决定上限的是后者。

每个请求都要经历的两个阶段

Prefill 一次性读取整个提示词,并为其构建注意力缓存。提示词中的所有 token 会一起通过模型,因此 prefill 是一次大型矩阵乘法,受算力吞吐限制。随后,Decode 每次生成一个 token。每个 token 都需要再次从内存中读取模型的完整权重,而针对单个 token 执行的计算量很小。因此,Decode 受内存带宽限制。

这种不对称性正是批处理有效的原因。为一个用户执行 Decode 时,每生成一个 token 都要读取约 5 GB 的权重,但大部分计算单元处于空闲状态。加入第二个请求后,引擎只需读取这 5 GB 权重一次,然后基于这些权重计算两个 token。第二个用户几乎不会增加额外耗时。严格按顺序逐个处理请求会失去这一优势。

有两个指标决定用户的实际感受。TTFT(首 token 时间)等于排队等待时间加 prefill 时间。ITL(token 间延迟)是连续输出的 token 之间的间隔,由 Decode 决定。服务器变慢通常是这两个指标之一变差导致的,而对应的优化方法并不相同。在修改任何设置前,应先确定需要解决的是哪个指标;通过分别测量 prefill 和 Decode 的耗时即可确认。

静态批处理会让所有请求等待最慢的响应

静态批处理是最简单的实现方式。应用代码自行对请求分组时,通常得到的就是这种方式。引擎收集 N 个请求并并行处理,然后一直占用所有槽位,直到组内生成时间最长的请求完成。

一名用户请求生成 1,200 个 token 的摘要,会让批处理中另外四个只需一行的回答一直占用槽位,因为批处理要等最慢的请求完成后才会释放槽位。

这会带来两个问题。已完成的序列仍占用槽位,却不再执行有效计算。因此,随着输出长度变化,有效吞吐量会下降;而聊天请求的输出长度通常差异很大。批处理形成后仅晚一个步骤到达的请求,必须等整个批处理排空后才能开始 prefill。这意味着它的 TTFT 取决于其他用户的长篇输出。

连续批处理按 token 接收和退出请求

连续批处理以单个解码步骤为调度粒度。每个步骤结束后,调度器会移除刚输出停止 token 的序列,然后将等待中的请求加入空闲槽位。在第 40 步结束的响应会在第 40 步释放其槽位,而不是等到整个批次结束。

这并不特殊。llama-server 将 -cb, --cont-batching 记录为“是否启用连续批处理(也称动态批处理)(默认:启用)”,vLLM 也围绕这一机制构建。Ollama 同样支持并行请求。默认配置只是将数量限制为 1,因此许多人误以为硬件不支持并发,实际原因是配置禁止了并发。

已发布的连续批处理结果通常是在数据中心显卡上测得的。这些显卡既有剩余计算能力,也有数十 GB 的缓存空间。结果的变化趋势同样适用于您的设备,但数值规模并不相同。原因见下面的内存部分。

预填充会与解码争用相同的计算资源

当有新请求到达,而此时已有四个回复正在流式生成时,系统必须先对新请求的提示词执行预填充,而预填充需要大量计算。如果调度器为这次预填充单独分配一个步骤,四个正在流式生成的用户在该步骤期间都收不到新 token。对于较长的提示词,每个已打开的窗口都会出现明显停顿。这就是人们所说的服务器在其他人点击发送时出现卡顿。

分块预填充会将较长的提示词拆分为多个部分,并将每个部分与正在运行的解码任务放入同一个步骤。vLLM 的调优指南直接说明了这种权衡:较小的分块预算“由于减慢解码的预填充次数更少,因此可获得更好的 ITL”,而较高的值“可获得更好的首 token 时间(TTFT),因为可以在一个批次中处理更多预填充 token”。您需要选择优先保障哪类用户体验:等待回复开始生成的用户,还是正在观看文本流式输出的用户。

提示词长度决定了影响程度。一个包含 6,000 个 token 的提示词和一个包含 200 个 token 的回答,意味着需要执行 6,000 个 token 的预填充工作,而解码只有 200 个步骤。检索增强聊天和较长的系统提示词都会将您带入这种情况,因此预填充不再是可忽略的计算量,而会成为用户需要等待的主要环节。前缀缓存可在较长部分重复出现时提供帮助:vLLM 提供 --enable-prefix-caching,可以复用共享提示词前缀的缓存,而不必为每个请求重新计算该前缀。

首先耗尽的是 KV cache

每个活动会话中的每个 token,都会在模型的每一层留下一个 key 向量和一个 value 向量。这就是 KV cache(key/value cache)。它使解码过程无需为每个新 token 重新计算完整提示词。每个 token 所需的缓存大小由模型结构固定:2(一个 key 和一个 value)乘以层数,再乘以 key/value head 数量、head 维度和每个值所占的字节数。从模型的 config.json 中读取这些数值。

计算一次后,容量上限就不再神秘。一个典型的 8B 模型有 36 层、8 个 key/value head,head 维度为 128,并使用 16-bit 保存缓存。每个 token 的开销为 2 36 8 128 2 bytes,即 147,456 bytes,约 144 KiB。一个包含 8,192 个 token 的会话因此需要约 1.2 GB 的缓存。5 个这样的会话约需 6 GB,还要加上模型权重。这才是“能容纳多少用户”的实际答案。

并发会放大上下文占用,工具的说明也明确指出了这一点。Ollama 的 FAQ 写道:“对于指定模型,并行请求处理会按并行请求数增加上下文大小。例如,2K 上下文配合 4 个并行请求时,将产生 8K 上下文,并额外分配内存。” 所需 RAM 会按 OLLAMA_NUM_PARALLEL 乘以 OLLAMA_CONTEXT_LENGTH 进行扩展。在 llama-server 中,通过 -c 请求的上下文会在 -np 个槽位之间分配,因此单独提高槽位数会缩小每个请求可使用的上下文容量。应从启动日志中读取每个槽位的上下文大小,不要自行假设。

vLLM 则会预先分配内存。--gpu-memory-utilization(默认值为 0.92)表示“供模型执行器使用的 GPU 内存比例”。模型权重占用内存后剩余的部分会成为分页 KV 池。该池空间不足时,调度器会驱逐请求,而不是让请求失败:

WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.

在 vLLM 的 V1 engine 中,默认的抢占模式是 RECOMPUTE。因此,被驱逐的请求会丢弃缓存,并在重新获得调度时再次执行 prefill。这部分工作会执行两次。文档警告称,“抢占和重新计算可能会对端到端延迟产生不利影响”。这条日志是解释以下现象的最佳依据:某个用户的等待时间远高于其他用户,但整体平均延迟看起来仍然正常。将 disable_log_stats=False 设置为记录累计计数,或从 vLLM 暴露的 Prometheus 指标中读取抢占计数器。

2、5 和 20 个并发用户时会发生什么变化

2 个用户。 在缓存余量充足的 GPU 上,几乎察觉不到影响,因为第二个解码流只需在第一个解码流的基础上增加很少时间。在仅使用 CPU、内存为 4 到 8 GB 的 VPS 上,并发并不是免费的:两个流共享同一组 vCPU 和同一内存带宽,因此每个用户看到的每秒 token 数大约减半,而缓存需求却翻倍,面对的可用预算还小得多。

5 个用户。 这时默认配置不再够用,问题首先表现为队列。将 OLLAMA_NUM_PARALLEL 设为 1 时,4 个人会等待最先请求长回答的用户;轮到自己时,每个人都能看到正常速度。提高并行数后,问题会改变:5 个槽位、每个槽位使用 8K 上下文,需要容纳 40K token 的缓存。如果显存放不下,推理引擎会将层卸载到系统内存;如果内存也放不下,服务器就会使用交换空间,每秒 token 数会骤降。

20 个用户。 聊天界面中的 20 个人通常不等于 20 个并发请求。在购买硬件前,最需要理解的就是这一点。用户读完回复后,通常会思考 20 到 60 秒才发送下一条消息,因此其会话大部分时间处于空闲状态。20 个代理,或 20 个文档摘要任务,则是 20 个始终处于活动状态的真实流,没有任何空闲时间。这需要的是另一种服务器。将 编码代理指向自己的 Ollama 服务器 的开发者,更接近第二种情况,而不是第一种,因为只要任务仍在运行,代理就会持续发送请求,不会留下人工用户阅读时的停顿。

用户是并发使用,还是仅保持登录?

在确定配置前,先计算正在处理的请求数。计算很简单:处理中请求数 = 用户数 × 每轮生成所需秒数 ÷ 两轮之间的间隔秒数。

  1. 先测量单路请求的实际速度,同时测量 prefill 和 decode。不要直接采用其他设备的数据:在自己的设备上测量每秒生成的 token 数,并使用测得的结果。
  2. 估算使用周期。20 个聊天用户,每轮生成需要 12 秒,每 90 秒发送一轮,则 20 * 12 / 90,约为 2.7 个处理中请求。
  3. 将 slot 数设置为略高于该值,然后检查内存:slot 数 × 每个请求的上下文大小,必须小于实际可用的缓存 token 数。
  4. 保持队列较短,使超出的请求能够快速且明确地失败。

可用缓存 token 数 = 权重占用后的空闲内存 ÷ 上一节中的每 token 成本。一张 24 GB 显存的显卡运行 8B 模型的 16-bit 版本时,权重约占用 16 GB;在默认利用率下,约有 6 GB 可用缓存,足够容纳约 5 个 8K 对话。若要容纳更多请求,可以缩短每个请求的上下文,或将缓存存储为 8-bit(llama-server 需要 --cache-type-k q8_0)。这两种方式都能牺牲一部分性能或容量来提高并发。在购买硬件前,应先阅读这种取舍的实际成本:GPU VPS 与 API token 何时达到成本平衡。

当 Ollama 的默认值不再满足需求

通过服务单元提高并行数,因为 shell 导出的变量不会传递给 systemd 管理的守护进程。

sudo systemctl edit ollama.service
[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"
sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama ps

systemctl show 应输出你刚设置的 3 个变量。如果没有输出,说明 drop-in 未保存,后续操作都不会生效。ollama ps 随后会列出已加载的模型。显示的大小会大于权重本身,因为 4 个槽位各使用 8,192 个 token 时,还会为缓存预留 32,768 个 token。如果你预计模型会全部放在 GPU 上,但 PROCESSOR 列显示模型有一部分位于 CPU,说明你请求的缓存超过了显卡剩余容量。请降低这两个数值中的一个。通常,缩小上下文更安全;但上下文窗口过小会静默截断较长的提示词,而不是返回错误。因此,与其不断调小数值直到模型能够加载,不如有意确定 num_ctx 的大小。

还应重新检查队列默认值。Ollama 最多会排队 OLLAMA_MAX_QUEUE 个请求,而且“默认值为 512”。超过该数量后,它会返回“表示服务器过载的 503 错误”。对于一次只能处理 4 个请求的服务器,深度为 512 的队列是无法兑现的承诺,因为排在第 300 位的客户端会在轮到它之前很久就超时。较短的队列会返回应用可以重试或报告的错误,这比一直无法结束的加载指示器更好。

进行实际测试。在两个终端中同时发送 2 个请求,并观察两边的结果。如果第二个请求直到第一个请求完成后才产生任何输出,说明并行设置未生效。

真正的推理服务引擎开始发挥价值时

当您有一块仍有余量的 GPU,并且通常有超过四个请求确实处于处理中时,vLLM 才值得额外的配置工作。它按 token 调度,使用分页缓存来重新利用空闲碎片,并将闲置的显存转换为并发能力,而不是让其保持空闲。截至 2026 年 8 月,文档中的安装和启动只需两条命令:

uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instruct
curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "Qwen/Qwen2.5-1.5B-Instruct",
        "messages": [{"role": "user", "content": "Say hello."}]
    }'

如果响应中包含 choices 数组,说明服务器已启动且模型已加载。在负载下,影响最大的两个参数是 --max-num-seqs(“单次迭代中要处理的最大序列数”)和 --max-num-batched-tokens(“单次迭代中可处理的最大 token 数”)。前者限制并发数。后者是前文介绍的分块预填充预算。

当同时处理的请求少于约四个,或服务器没有受支持的 GPU 时,vLLM 会增加复杂度,但收益很小。它需要 CUDA 级别的显卡,并会在启动时占用大部分显存;对于 4 到 8 GB 的 VPS,这种取舍并不合适。此时应选择更小的模型、较短的上下文,以及由您控制的队列。Ollama 与 vLLM 作为推理服务引擎的差异完整介绍了如何选择,在 VPS 上运行 Qwen 3 8B则说明了在增加第一个额外用户前,中等规模模型需要哪些资源。

folklore 掩盖的权衡

连续批处理会提高总吞吐量,通常也会改善中位延迟,因为排队的请求可以更早开始。尾延迟则会朝相反方向变化,而这一点很少被提及。

每个步骤中新增一个序列都会增加少量工作,因此批次填满后,所有请求的 ITL 都会上升。新到达请求的 prefill 会占用原本可供流式请求使用的步骤时间。在缓存压力下,调度器会抢占请求,使尚未生成完毕的请求重新从 prefill 开始。

聊天 UI 展示的是尾延迟,而不是平均值。即使完成请求的总时间较短,流式输出在句子中间暂停两秒也会让用户认为服务已失常。在预期负载下测量 p95 TTFT 和 p95 ITL,并将每秒平均生成 token 数视为容量指标,而不是用户体验的描述。

实际配置应据此确定。将并发数限制在内存允许值以下,留出少量余量,使引擎无需抢占。短而可预测的队列优于因反复抖动而失效的深批处理,因为用户等待四秒后能够平稳接收流式输出,比立即开始却停顿两次的体验更好。

速度变慢时的检查方法

每个请求本身都正常,但等待时间很长。 这是队列问题,不是速度问题。先检查并行设置。模型运行正常,只是一次处理一个请求。

Ollama 返回 HTTP 503。 队列已满。可能是服务器确实已达到容量上限,也可能是 OLLAMA_MAX_QUEUE 被有意设置得较低,以便丢弃负载。这正是该设置的作用。

CPU 服务器在负载下每秒生成的 token 数骤降。 发生时运行 vmstat 1。如果 si 和 so 列非零,说明机器正在使用 swap,因此每生成一个 token 都要从磁盘读取权重。修改配置无法解决这个问题。请减小模型规模或减少并发槽位数。

每 10 个用户中有 1 个的等待时间远长于其他用户。 在 vLLM 日志中搜索 preempted。通常原因是发生了抢占并需要重新计算,说明对于允许的上下文长度,缓存容量不足。

即使服务器空闲,TTFT 仍然很差。 这是预填充问题,不是并发问题。长提示词会在生成第一个 token 前消耗实际处理时间,因此应先检查提示词大小和前缀缓存,再检查硬件。如果只有安静一段时间后返回的第一个用户需要长时间等待,而后续用户都正常,那么问题根本不是预填充,而是 Ollama 卸载了模型,又重新从磁盘读取权重。可以通过让模型在请求之间保持驻留来排除这一原因。

FAQ

为什么第二个人使用我的自托管 LLM 时,速度会变慢?

通常它根本没有变慢,而是在排队。Ollama 发布时将 OLLAMA_NUM_PARALLEL 设置为 1,因此第二个请求必须等待第一个请求生成最后一个 token。让一个用户保持流式输出,同时让另一个用户等待,并测量前者的输出速度,即可区分这两种情况:如果第二个请求开始处理后,其每秒 token 数正常,说明是排队,提高并行数即可解决。如果两个流的速度都降到一半,说明它们确实在共享内存带宽,这是硬件限制。

一块小型 GPU 可以为多少个并发用户提供服务?

应计算内存,而不是用户数。先计算权重,再计算 KV cache。KV cache 的开销为:每个 token、每个活跃会话需要 2 乘以层数,乘以键/值头数,乘以头维度,再乘以每个元素的字节数。典型的 8B 模型有 36 层、8 个键/值头,头维度为 128;使用 16-bit 时,每个 token 的开销约为 144 KiB。因此,一个包含 8,192 个 token 的会话大约需要 1.2 GB。用于以 16-bit 保存该模型的 24 GB 显卡,约有 6 GB 可用于 cache。这相当于在完整上下文下支持约五个会话;缩短上下文后还可以支持更多会话。

连续批处理会让每个用户的回复变慢吗?

中位延迟通常会降低,因为请求不必再等待整个批次完成。尾延迟会增加。每增加一个序列,每个解码步骤都要增加工作量;新请求到达时,其 prefilling 会占用流式用户部分步骤的处理时间;被抢占的请求还必须执行两次 prefill。应测量 token 间延迟的 p95,而不是平均值,因为聊天窗口会明显显示暂停,而平均值可能掩盖这些暂停。

我应该提高 OLLAMA_NUM_PARALLEL,还是改用 vLLM?

先提高并行数。这样无需额外成本,只需添加一个 drop-in 文件;当四个人排在一个很长的回复后面时,这通常可以解决问题。限制因素是内存:并行请求会按比例增加必须保存的上下文,因此要注意是否有层溢出到 CPU。当 GPU 还有足够 VRAM,且实际同时处理的请求超过约四个时,再改用 vLLM。此时,paged cache 和按 token 调度带来的收益通常会超过其开销。

增加 CPU 核心能解决运行缓慢的 LLM 服务器吗?

不能解决用户最明显感受到的那部分问题。解码每生成一个 token 都要从内存读取整个模型,因此受 RAM 带宽限制。当带宽达到饱和后,增加 CPU 核心不会继续提升性能。Prefill 可以随 CPU 核心数增加而扩展,因此更多核心可以缩短长提示词的首 token 延迟。在 4 到 8 GB 的 VPS 上,限制因素通常是内存容量。实际有效的解决方案通常是使用更小的模型或缩短上下文,而不是增加 vCPU。