Ollama 并发推理的底层调度机制请求队列、KV Cache 复用与显存碎片治理一、100 个并发请求打到 Ollama4 张 GPU 仅跑出了 1.2 张的吞吐在一个高并发的推理场景中。100 个并发请求同时到达 Ollama 服务。4 张 RTX 4090 的 GPU 利用率为第一张 85%其余三张均低于 20%。有效吞吐仅约 1.2 张卡的水平。问题不在 GPU 算力而在调度策略。Ollama 底层使用的是 llama.cpp 的 server 模式。在并发调度的设计中存在三个核心问题。第一是请求队列的调度策略过于简单先进先出 批次合并。第二是 KV Cache 没有跨请求复用机制。第三是长期运行后的显存碎片导致可用 Batch 逐渐缩小。要理解 Ollama 并发推理的性能瓶颈。需要深入到 llama.cpp 的请求调度逻辑中。理解每个请求的生命周期。以及它们如何竞争 GPU 显存和计算资源。二、llama.cpp server 的请求调度架构调用方的请求到达后进入处理管线。从队列等待到结果返回经过多个关键阶段。graph TD A[客户端请求] -- B[HTTP Server 接收] B -- C[请求队列 (FIFO)] C -- D[槽位分配 Slot Allocator] D -- E[Slot 0: 处理中] D -- F[Slot 1: 处理中] D -- G[Slot 2: 空闲] D -- H[Slot N: 空闲] E -- I[KV Cache 分配] F -- I I -- J[Prompt 处理 (Prefill)] J -- K[逐 Token 生成 (Decode)] K -- L[结果流式返回] subgraph KV Cache 管理 M[KV Cache 池] M -- N[槽位独占 KV 区域] M -- O[Prefix 共享缓存] M -- P[碎片: 32MB~256MB 不连续空洞] end I -- M J -- M K -- M subgraph 监控指标 Q[槽位利用率] R[KV Cache 碎片率] S[Prompt 队列深度] T[Token/s 总计] end D -.- Q P -.- R C -.- S K -.- Tllama.cpp 的调度模型是槽位制Slot-based。预分配固定数量的处理槽位。每个槽位对应一个正在处理的对话上下文。槽位数受限于显存大小。因为每个槽位需要独占一块 KV Cache 区域。对于 8B 模型在 24GB 显存上设置 4096 上下文。单槽位约需 4GB KV Cache。可以开 46 个槽位。接收到请求后首先进入请求队列等待。Slot Allocator 检查是否有空闲槽位。以及是否有足够的空闲 KV Cache 分配给新请求。如果槽位满或 KV Cache 不足。请求在队列中等待。Prompt Processing也称为 Prefill阶段计算所有 Prompt token 的 KV 值并填充到 KV Cache 中。这个阶段是计算密集型的。大批量的 Prompt 会一次性消耗大量 GPU 算力。Decode 阶段逐 token 生成输出。每个 token 的计算量远小于 Prompt Processing。但需要访问完整的 KV Cache。三、并发调度的关键瓶颈分析#!/bin/bash # Ollama 并发推理的性能分析脚本 MODELllama3:8b-instruct-q4_k_m OLLAMA_HOSTlocalhost:11434 # # 阶段一单请求基线 # echo 单请求基线测试 time curl -s http://${OLLAMA_HOST}/api/generate \ -d {\model\:\${MODEL}\,\prompt\:\写一篇1000字的文章\,\stream\:false} \ | jq .eval_count, .eval_duration # # 阶段二并发请求压测 # echo -e \n 并发压测10并发 # 使用 ab 或 wrk 工具进行压测 # 以下为简化的并发测试脚本 for i in $(seq 1 10); do curl -s http://${OLLAMA_HOST}/api/generate \ -d {\model\:\${MODEL}\,\prompt\:\计算 123*${i}7\,\stream\:false} done wait # # 阶段三KV Cache 利用率分析 # echo -e \n KV Cache 状态 curl -s http://${OLLAMA_HOST}/api/ps | jq .models[].slots[].state # # 阶段四持续运行后的碎片观察 # echo -e \n 运行 100 轮请求后观察 for i in $(seq 1 100); do curl -s http://${OLLAMA_HOST}/api/generate \ -d {\model\:\${MODEL}\,\prompt\:\随机数${i}\,\stream\:false} \ /dev/null done # 检查显存碎片率 nvidia-smi --query-gpumemory.used,memory.free --formatcsv # 典型观察结果 # 运行 100 轮后显存 used 持续增长 # 但 free 显存中有大量碎片化的空隙 # 导致实际可分配的 KV Cache 远小于 free 值并发性能的瓶颈分布在三个层面。第一是槽位数的硬限制。每个槽位的 KV Cache 预分配是固定大小的。当所有槽位都在使用时。新请求只能在队列中等待。增大槽位数确实能提升并发度。但显存是有限的。需要在槽位数和 Batch Size 之间权衡。第二是 Prefill 阶段的批处理效率。如果有 4 个请求在 Decode 阶段。第 5 个请求的 Prefill 只能单独执行。无法与其他 Decode 的请求合并。这浪费了 GPU 的并行计算能力。改进方向是将 Prefill 请求也合并为一个 Batch。与 Decode 在同一 forward pass 中执行Continuous Batching。第三是 KV Cache 的碎片化。随着请求的生命周期不同短的 10 token、长的 1000 token。KV Cache 的分配和释放产生碎片。系统运行一段时间后。总空闲显存可能仍有很多。但无法分配出连续的 KV Cache 块。表现为可用槽位数减少。四、优化方向与生产建议提升 Ollama 并发推理性能有四个优化方向。第一个优化是启用 Continuous Batching。这是 llama.cpp 社区仍在开发中的特性。它允许 Prefill 和 Decode 请求在同一批次中混合执行。而不是分别处理。据社区测试Continuous Batching 可以将并发吞吐提升 23 倍。第二个优化是共享 KV Cache Prefix。在相同 System Prompt 的并发请求中。System Prompt 的 KV 值可以被所有请求共享。只需要计算一次。这将 System Prompt 的计算和显存消耗降低到单次。对于 System Prompt 较长的场景效果显著。第三个优化是 KV Cache 的碎片整理。定期对 KV Cache 池做压缩整理。将分散的空间合并。类似于操作系统中的内存碎片整理。可以在请求量低谷期执行。第四个运维优化是设置合理的num_ctx。对于短对话场景如问答。num_ctx2048即可。没必要设为 4096 或更大。每减少 1024 上下文每个槽位可节省约 0.5GB 显存。多出的显存可以用于增加槽位数。五、总结Ollama/llama.cpp 的并发调度基于槽位模型。槽位数受限于显存大小。多出的请求在 FIFO 队列中等待。Prefill 与 Decode 的分离处理是并发效率低的主要原因。Continuous Batching 可以将两者合并提升 23 倍吞吐。KV Cache 碎片化在长期运行后降低可用槽位数。需要碎片整理机制或启用 Prefix 共享缓存。System Prompt 前缀共享对于长 System Prompt 场景有显著收益。将 System Prompt 的计算和显存消耗降低到单次。num_ctx的调优直接影响并发能力。短对话场景降低到 2048 可以释放显存用于增加槽位。