SSD Nodes Learn 🎉 VPS $4.99/月起
指南 Matt Connor作者: Matt Connor

自托管LLM为何5个用户就变慢?

单用户运行正常,5个用户却开始爬行?本文解释Ollama默认并发数为1,以及批处理、KV缓存、prefill和队列深度如何决定实际并发上限。

为什么自托管 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(time to first token,首 token 时间)等于排队等待时间加 prefill 时间。ITL(inter-token latency,token 间延迟)是流式输出 token 之间的间隔,由 decode 决定。服务器变慢通常是这两个指标之一变差导致的,而对应的优化方法并不相同。

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

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

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

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

连续批处理按每个 token 接纳和退出请求

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

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

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

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

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

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

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

最先耗尽的是 KV 缓存

每个活跃会话中的每个 token,都会在模型的每一层留下一个 key 向量和一个 value 向量。这就是 KV 缓存(key/value cache)。它使 decode 阶段无需为每个新 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 个 slot 中,因此单独提高 slot 数量会缩小每个请求可使用的上下文容量。应从启动日志中读取每个 slot 的上下文大小,不要自行假设。

vLLM 则会预先分配内存。--gpu-memory-utilization(默认值为 0.92)表示“用于模型执行器的 GPU 内存比例”。模型权重占用之后的剩余内存会成为分页 KV 池。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 引擎中,默认的抢占模式是 RECOMPUTE。因此,被驱逐的请求会丢弃缓存,重新加入调度后再次执行 prefill。这部分工作会执行两次。文档警告称,“抢占和重新计算可能对端到端延迟产生不利影响”。对于某个用户等待时间远高于其他用户、但平均延迟看起来仍然正常的情况,这条日志是最直接的解释。将 disable_log_stats=False 设置为记录累计次数,或从 vLLM 提供的 Prometheus 指标中读取抢占计数器。

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

两个用户。 如果 GPU 还有充足缓存,几乎感觉不到变化,因为第二个解码流只需在第一个解码流的基础上增加很少的处理时间。对于配备 4 到 8 GB RAM 的纯 CPU VPS,这并不是零成本:两个流会共享同一组 vCPU 和相同的内存带宽,因此每个用户看到的每秒 token 数大约减半,而缓存需求却翻倍,但可用内存预算要小得多。

五个用户。 此时默认配置通常不够用,问题首先表现为排队。将 OLLAMA_NUM_PARALLEL 设为 1 时,4 个人必须等待提出长回答请求的用户;轮到他们处理后,每个人都能恢复正常速度。提高并行数后,问题会变成另一种形式:5 个并行槽位、每个槽位使用 8K 上下文,需要容纳 40K token 的缓存。如果显存放不下,推理引擎会将部分层卸载到系统 RAM;如果 RAM 也放不下,服务器就会使用交换空间,每秒 token 数会急剧下降。

20 个用户。 聊天界面中的 20 个人通常不等于 20 个并发请求。在购买硬件前,最需要理解的就是这一点。用户阅读回复后,通常会思考 20 到 60 秒才发送下一轮消息,因此其会话大部分时间处于空闲状态。20 个 agent 或 20 个文档摘要任务则不同:它们是 20 个没有任何空闲时间的真实处理流。这需要另一种配置的服务器。

并发用户,还是仅登录用户?

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

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

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

Ollama 的默认设置不足时

通过服务单元提高并行数,因为 shell 中的 export 不会传递给由 systemd 管理的 daemon。

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 应输出刚才设置的三个变量。如果没有输出,说明 drop-in 未保存,后续操作都不会生效。随后,ollama ps 会列出已加载的模型。显示的大小会大于权重本身,因为 4 个槽位各使用 8,192 个 token,会额外预留 32,768 个 token 的缓存。如果你预期模型全部位于 GPU 中,但 PROCESSOR 列显示其中一部分位于 CPU,说明请求的缓存超过了显卡的剩余容量。将两个数值中的一个调低。

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

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

真正的推理服务引擎何时值得投入

当您拥有尚有余量的 GPU,并且确实同时处理大约 4 个以上请求时,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 数”)。前者限制并发数。后者是前文介绍的分块预填充预算。

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

民间经验掩盖的权衡

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

每一步中增加一个序列都会带来少量额外工作,因此批次填满时,所有请求的 ITL 都会上升。新请求的预填充会占用本可分配给流式请求的一部分处理时间。在缓存压力下,调度器会抢占请求,使尚未生成完毕的请求重新从预填充阶段开始。

聊天界面反映的是尾延迟,而不是平均值。即使完成请求所需的总时间较短,句子中途暂停两秒也会让用户觉得服务失灵。在预期负载下测量 p95 TTFT 和 p95 ITL,并将每秒平均生成 token 数视为容量指标,而不是用户体验的描述。

实际配置也由此确定。将并发上限设置为略低于内存允许的上限,使引擎无需进行抢占。短而可预测的队列优于因反复抖动而失效的深批次,因为用户等待四秒后能够持续流畅地接收输出,体验通常好于请求立即开始但中途停顿两次。

速度变慢时的检查项

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

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

CPU 服务器在负载升高时,每秒生成的令牌数骤降。 在问题发生时运行 vmstat 1。如果 siso 列的值非零,说明机器正在使用 swap,因此每生成一个令牌都要从磁盘读取模型权重。调整其他配置无法解决此问题。请减小模型规模或减少并发槽位数。

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

即使服务器空闲,TTFT 仍然很高。 这是预填充问题,不是并发问题。长提示词会在首个令牌出现前消耗实际处理时间,因此应先检查提示词大小和前缀缓存,再检查硬件。

FAQ

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

多数情况下,速度并没有变慢,而是请求进入了队列。Ollama 默认将 OLLAMA_NUM_PARALLEL 设置为 1,因此第二个请求必须等待第一个请求生成最后一个 token。可以让一个用户持续生成,同时让另一个用户等待,并测量前一个用户的流式输出时间,以区分这两种情况:如果第二个请求开始后,每秒生成的 token 数正常,说明是排队,提高并行数即可解决。如果两个流都以一半的速度运行,说明它们确实在共享内存带宽,这是硬件限制。

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

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

continuous batching 会让每个用户的回复变慢吗?

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

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

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

增加 CPU 核心数能解决 LLM 服务器速度慢的问题吗?

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

#vllm#ollama#batching#throughput#self-hosted-ai