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

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

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

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

当自托管 LLM 在 5 个并发用户时出现卡顿,通常是因为服务器被设置为一次仅生成一个回复,其余四个请求在队列中等待。Ollama 的文档明确指出其默认设置:OLLAMA_NUM_PARALLEL 是“每个模型同时处理的最大并行请求数,默认值为 1”。系统并未损坏,服务软件本身可以同时处理多个请求。由于设置限制,你的 5 个用户中有 4 个正在排队等待。

解决此问题通常不需要更换更强的硬件。你需要的是一个能够通过单次前向传递(forward pass)处理多个请求的服务引擎,并确保有足够的剩余内存来在处理过程中保存所有用户的对话上下文。这两者缺一不可,其中内存容量决定了你的并发上限。

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

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 记录为“是否启用连续批处理(又称动态批处理)(默认:启用)”,并在其特性中列出了“支持多用户的并行解码”。自 2025 年 11 月合并的一项变更起,默认启动 4 个槽位(-np 默认为 -1,即自动)。vLLM 正是围绕这一理念构建的。Ollama 同样支持并行请求服务。默认配置仅将数量上限设为 1,这就是为什么许多人认为自己的硬件无法实现并发,而实际上是配置限制了该功能。

已发布的连续批处理结果通常是在数据中心显卡上测得的,这些显卡既有冗余算力,又有数十 GB 的缓存空间。这些结果的形态适用于你的设备,但其规模并不适用,下方的内存章节解释了原因。

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

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

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

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

最先耗尽的内存是 KV 缓存

每个活跃对话中的每个 token 都会在模型的每一层留下一个键向量(key vector)和一个值向量(value vector)。这就是 KV 缓存(key/value cache),它使解码过程无需为每个新 token 重新计算整个提示词。每个 token 的缓存值数量由模型结构决定:2(一个键,一个值)乘以层数,再乘以键/值头数,最后乘以头维度。从模型的 config.json 中读取这些数值。将其乘以每个值占用的字节数即可得到缓存大小,最后一个因子是运行时设置,而非模型本身的一部分:llama-server 默认以 f16(2 字节)存储缓存,并支持诸如 q8_0 到 -ctk 和 -ctv 等更小的类型。

计算一次后,内存上限就不再神秘。一个典型的 8B 模型拥有 36 层、8 个键/值头和 128 的头维度,以 16 位存储缓存,每个 token 占用 2 36 8 128 2 字节。即 147,456 字节,约 144 KiB。因此,一个 8,192 token 的对话大约需要 1.2 GB 的缓存。五个这样的对话在模型权重之外还需要约 6 GB 内存,这才是决定能容纳多少用户的真正答案。

并发会成倍增加上下文需求,工具文档对此有明确说明。Ollama 的 FAQ 指出:“针对同一模型进行并行请求处理,会导致上下文大小按并行请求数增加。例如,2K 上下文配合 4 个并行请求,将导致 8K 上下文及额外的内存分配。” 所需 RAM 与 OLLAMA_NUM_PARALLEL 乘以 OLLAMA_CONTEXT_LENGTH 成正比。在 llama-server 中,默认的自动槽位计数(auto slot count)会在一个统一的 KV 缓冲区上运行所有槽位,因此整个 -c 上下文是一个单一的资源池,供各槽位调用。若自行设置 -np,则除非同时传递 --kv-unified,否则统一缓冲区将关闭,此时通过 -c 请求的上下文会被分配到 -np 个槽位中,单独增加槽位数量会缩减每个请求可容纳的容量。请从启动日志中读取每个槽位的上下文大小,不要仅凭猜测。

vLLM 则采用预分配机制。--gpu-memory-utilization(默认 0.92)是“模型执行器所占用的 GPU 内存比例”。除非设置 --kv-cache-memory-bytes,否则 vLLM 会根据该比例推断 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 个并发用户时会发生什么变化

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)数量设置得略高于该值,然后根据内存进行核对:槽位数量乘以单次请求的上下文大小,必须小于您实际拥有的缓存 token 总量。
  4. 保持队列长度较短,以便在溢出时能快速且直观地发现失败。

可用缓存 token 总量等于扣除模型权重后的剩余内存,除以各 token 的内存占用。一张 24 GB 的显卡运行 16-bit 的 8B 模型时,权重占用约 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 还有余量,且确实有大约 4 个以上请求同时处理时,vLLM 才值得额外的配置工作。它按 token 调度,采用分页缓存以复用空闲碎片,并将剩余 VRAM 转化为并发能力,而不是让其闲置。截至 October 2026,文档中的 GPU 安装和启动只需执行 3 条命令。vllm download-kernels 会获取预编译内核,否则 vLLM 会在启动时下载并编译这些内核:

uv pip install vllm --torch-backend=auto
vllm download-kernels
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 个时,vLLM 会增加复杂度,但收益很小。此时不再要求 GPU:截至 October 2026,CPU 安装指南已记录如何在 x86 和 Arm CPU 上进行“基本模型推理和服务”,并且从 version 0.17.0 起提供预构建的 x86 wheel,同时提供 CPU Docker 镜像。不过,它也会沿用相同的内存使用方式。在 CPU 上,它会将系统 RAM 的 0.92 作为利用率比例,并将权重占用之外的内存分配给 KV cache。因此,默认情况下,它会在启动时占用大部分机器内存,除非使用 --kv-cache-memory-bytes 限制缓存大小。对于 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 数(TPS)正常,说明是在排队,调大并行数即可解决;如果两个流的速度都减半,说明是在共享内存带宽,这是硬件限制。

一块小型 GPU 可以同时服务多少个并发用户?

应计算内存占用,而非用户数。首先是模型权重,其次是 KV 缓存。KV 缓存的开销计算方式为:2 × 层数 × KV 头数 × 头维度 × 字节数,按每个 token、每个活跃会话计算。一个典型的 8B 模型(36 层、8 个 KV 头、头维度 128)在 16 位精度下,每个 token 占用约 144 KiB,因此 8,192 个 token 的会话大约需要 1.2 GB。一张 24 GB 的显卡在加载该模型后,剩余约 6 GB 可用于缓存,这大约支持 5 个全上下文会话;如果缩短上下文,则可支持更多。

连续批处理(Continuous Batching)会降低每个用户的回复速度吗?

中位延迟通常会改善,因为请求不再需要等待整个批次完成。但长尾延迟会变差。每个新增序列都会增加解码步骤的计算量,新请求的预填充(prefill)会占用流式传输用户的一部分计算资源,且被抢占的请求需要进行两次预填充。请测量 p95 的 token 间延迟,而不是平均值,因为聊天窗口中的停顿在平均值中会被掩盖,但在实际体验中非常明显。

我应该调大 OLLAMA_NUM_PARALLEL 还是迁移到 vLLM?

先尝试调大并行数。这是免费的,只需一个配置文件即可生效,且能解决四个人排队等待一个长回复的常见问题。内存是主要限制:并行请求会成倍增加需要占用的上下文空间,因此需监控是否有层被卸载到 CPU。当你的 GPU 有充足的显存,且同时有超过 4 个请求真正处于运行状态时,再考虑迁移到 vLLM,因为此时分页缓存(paged cache)和按 token 调度带来的收益才会超过其开销。

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

对于用户最关心的部分,不能。解码过程需要为每个 token 从内存读取整个模型,因此受限于内存带宽;一旦带宽饱和,增加核心数将不再有帮助。预填充阶段确实会随核心数扩展,因此更多的核心可以缩短长提示词的首个 token 响应时间。在 4 到 8 GB 的 VPS 上,主要的限制通常是内存容量,有效的解决方法是使用更小的模型或更短的上下文,而不是增加 vCPU。

这篇文章对您有帮助吗?