Ollama 的并发模型深度分析:从请求队列到 GPU Stream 的任务分派与同步机制
Ollama 的并发模型深度分析从请求队列到 GPU Stream 的任务分派与同步机制一、多用户并发推理时 GPU 利用率不饱和的根因追问Ollama 作为本地 LLM 推理的流行方案单用户场景下表现良好。但部署为内部推理服务后多用户并发访问时 GPU 利用率常低于 30%。问题不在于显存不足——模型已完整加载——而在于请求调度和 GPU Stream 管理策略未能充分利用 GPU 的计算并行度。Ollama 的服务端基于 Go 语言实现底层调用 llama.cpp 的 C 推理引擎其并发模型面临一个核心矛盾GPU 在同一时刻只能执行一个 CUDA Stream 中的 Kernel但不同 Stream 之间可以通过异步执行实现计算与传输的 Overlap。如何在保证推理正确性的前提下最大化 GPU 利用率是并发模型设计的关键命题。二、从请求入队到 Kernel 执行的端到端调度路径flowchart TB subgraph HTTP 接入层 A[HTTP Request] -- B[参数校验] B -- C[请求入队] end subgraph 调度层 C -- D{调度策略} D --|连续批处理| E[合并为新 Batch] D --|独立处理| F[分配 GPU Stream] E -- F end subgraph GPU 执行层 F -- G[Stream-0: 上下文编码] F -- H[Stream-1: 逐 Token 生成] G -- I[CUDA Event 同步] H -- I I -- J[结果回传] end subgraph 内存管理 K[KV Cache 分配器] K -.-|为每个请求分配| G K -.-|为每个请求分配| H endOllama 采用的调度策略是请求级串行 批处理内并行。当新请求到达时如果处于生成Decode阶段请求需要排队等待当前批次完成。这种设计简化了 KV Cache 的管理——同一批次的请求共享一次 Kernel Launch——但代价是 GPU 在批次切换间存在闲置窗口。值得注意的是llama.cpp 内部使用多 CUDA Stream 来重叠 Prefill上下文编码和 Decode逐 Token 生成的计算。但 Stream 间的同步依赖 CUDA Event如果事件等待链条过长实际并发的收益会被稀释。三、自定义调度层的 GPU Stream 管理实现以下代码实现一个基于优先级队列的请求调度器核心思想是通过预分配 GPU Stream Pool 避免创建/销毁开销use std::sync::Arc; use tokio::sync::{Mutex, Semaphore, mpsc}; use std::collections::BinaryHeap; /// 推理请求的优先级定义 /// 设计原因BinaryHeap 是最大堆Rust 中 Reverse 包装后变为最小堆 /// 使用 (priority, timestamp) 元组排序同优先级按到达时间 FIFO #[derive(Eq, PartialEq)] struct InferenceRequest { priority: u8, // 1最高实时对话, 5最低批处理 arrival_time: u64, // 到达时间戳保证同优先级 FIFO input_tokens: Vecu32, // 输入 Token IDs max_tokens: u32, // 最大生成 Token 数 callback: tokio::sync::oneshot::SenderVecString, } impl Ord for InferenceRequest { fn cmp(self, other: Self) - std::cmp::Ordering { // 优先级低的值排前面对应更高优先级 self.priority.cmp(other.priority) .then_with(|| other.arrival_time.cmp(self.arrival_time)) } } /// GPU Stream 池管理器 /// 设计原因创建 CUDA Stream 是昂贵的 GPU 操作约 1-5μs /// 预创建 Stream 池并复用避免运行时创建开销 /// 每个 Stream 绑定一个推理上下文保持 KV Cache 的内存局部性 struct GpuStreamPool { available: MutexVecStreamHandle, // 信号量控制并发度防止 GPU 显存超分 concurrency_limit: Semaphore, } struct StreamHandle { id: u32, // CUDA Stream 的不透明句柄 // 设计原因使用 cudarc 或 cust 库的包装类型 // 直接传裸指针存在生命周期管理风险 stream: std::ffi::c_void, // 实际为 CUstream // 该 Stream 当前绑定的 KV Cache 内存区域偏移量 kv_cache_offset: usize, // 该 Stream 的最大可用显存字节 available_vram: usize, } impl GpuStreamPool { /// 创建 Stream 池 /// pool_size 不应超过 GPU 可支持的并发 Kernel 数上限 /// 通常取 4-8受限于显存中的 KV Cache 容量 pub fn new(pool_size: usize, total_vram: usize) - Self { let vram_per_stream total_vram / pool_size; let mut streams Vec::with_capacity(pool_size); for i in 0..pool_size { // 实际场景中调用 cuStreamCreate streams.push(StreamHandle { id: i as u32, stream: std::ptr::null_mut(), kv_cache_offset: i * vram_per_stream, available_vram: vram_per_stream, }); } GpuStreamPool { available: Mutex::new(streams), concurrency_limit: Semaphore::new(pool_size), } } /// 获取一个可用的 Stream /// 设计原因使用 Semaphore 而非 MutexCondvar /// Semaphore 的 await 在无可用许可时挂起无需自旋或轮询 /// 等待期间不消耗 CPU pub async fn acquire(self) - StreamHandle { let _permit self.concurrency_limit.acquire().await .expect(Semaphore closed unexpectedly); let mut available self.available.lock().await; available.pop().expect( Semaphore permit acquired but no stream available) } /// 归还 Stream 到池中 pub async fn release(self, handle: StreamHandle) { let mut available self.available.lock().await; available.push(handle); // Semaphore permit 在 _permit Drop 时自动释放 } } /// 调度主循环 /// 设计原因使用 tokio::select! 同时监听请求队列和批次完成信号 /// 避免忙碌等待busy-wait让渡 CPU 给其他任务 async fn scheduler_loop( mut request_rx: mpsc::ReceiverInferenceRequest, pool: ArcGpuStreamPool, ) { let mut pending: BinaryHeapstd::cmp::ReverseInferenceRequest BinaryHeap::new(); loop { tokio::select! { Some(req) request_rx.recv() { pending.push(std::cmp::Reverse(req)); } _ tokio::time::sleep(std::time::Duration::from_millis(1)) { // 定时尝试调度从队列中取优先级最高的请求 // 设计原因不等待队列满才调度减少首 Token 延迟 if let Some(std::cmp::Reverse(req)) pending.pop() { let pool pool.clone(); tokio::spawn(async move { let stream pool.acquire().await; // 在此 Stream 上执行推理 run_inference(stream, req).await; pool.release(stream).await; }); } } } } } async fn run_inference( _stream: StreamHandle, _req: InferenceRequest) { // 实际推理逻辑调用 llama.cpp 的 C API todo!(集成 llama.cpp inference) }核心设计决策在于Semaphore的使用。相比自行维护可用 Stream 数量并通过条件变量通知Semaphore直接提供了等待可用资源的语义代码简洁且无忙等开销。on_drop的自动释放保证即使在 panic 路径上也不会泄漏许可。四、调度模型的适用边界基于优先级的请求调度在高负载下有饥饿风险——低优先级请求可能无限期等待。解决方案是引入 Aging 机制随着等待时间增长动态提升请求优先级。在实现中arrival_time字段配合定期扫描即可实现每次调度时将等待超过阈值的请求的priority字段递减。GPU Stream Pool 的固定大小是一个显式的 Trade-off池大小为 N 意味着最多 N 个请求同时使用 GPU。如果请求的上下文长度差异大有的 100 Token有的 100K Token固定 KV Cache 分配策略会导致空间浪费——短请求的 KV Cache 区域闲置。动态 KV Cache 分配vLLM 的 PagedAttention 方案可以缓解此问题但实现复杂度显著增加。对于吞吐优先的离线批量推理场景如数据标注、Embedding 生成请求级优先级调度的收益有限。此时应优先使用连续批处理Continuous Batching让新请求加入正在执行的批次最大化 GPU 的计算密度。五、总结Ollama 的请求级串行调度是 GPU 利用率偏低的根因瓶颈在批次切换的闲置窗口而非 GPU 算力。预创建 GPU Stream Pool Semaphore 并发控制消除 Stream 创建开销并实现无忙等的资源分配。优先级队列结合 Aging 机制可在保证高优先级请求低延迟的同时避免低优先级请求饿死。固定 KV Cache 分区策略简化内存管理但浪费显存高并发场景需考虑 PagedAttention 等动态分配方案。离线批量推理场景的优化重点应从请求调度转向连续批处理以最大化 GPU 计算密度。

相关新闻

Raft 集群的性能退化诊断:日志复制延迟的 flamegraph 分析与网络层优化方案

Raft 集群的性能退化诊断:日志复制延迟的 flamegraph 分析与网络层优化方案

Raft 集群的性能退化诊断:日志复制延迟的 flamegraph 分析与网络层优化方案 一、节点数增长时集群吞吐不升反降的诡异现象 在分布式系统中,直觉上增加节点应该提升吞吐量——更多节点意味着更多并行处理单元。然而 Raft 集群的实际表现恰恰相反&#xff…

2026/10/18 22:44:41 阅读更多 →
Rust 程序的内存泄漏排查:使用 heaptrack、valgrind 与自定义 Allocator 追踪分配路径

Rust 程序的内存泄漏排查:使用 heaptrack、valgrind 与自定义 Allocator 追踪分配路径

Rust 程序的内存泄漏排查:使用 heaptrack、valgrind 与自定义 Allocator 追踪分配路径 一、生产环境中的"温水煮青蛙"式内存增长 Rust 的所有权模型在编译期消除了 Use-After-Free 和 Double-Free,但这并不意味着内存泄漏从此绝缘。引用计数循…

2026/10/18 10:33:54 阅读更多 →
还在为开题报告头秃?这5个AI写作辅助网站让你效率翻倍!

还在为开题报告头秃?这5个AI写作辅助网站让你效率翻倍!

朋友们,写论文是不是让你抓狂?对着空白文档发呆、查重率压不下来、导师催稿催到心慌……别急!今天给大家推荐几款AI工具,只要用对了工具,真的能让你从焦虑中解脱! 不过先说清楚哈——AI不能替你写论文&…

2026/10/19 11:38:03 阅读更多 →

最新新闻

基于STC12C5A60S2的智能油烟机毕业设计:ADC、PWM与状态机实战

基于STC12C5A60S2的智能油烟机毕业设计:ADC、PWM与状态机实战

简介:面向单片机、嵌入式方向的本科毕业设计人员,这份智能油烟机项目以STC12C5A60S2单片机为核心,完整覆盖油烟机控制系统的程序编写、硬件设计与联调环节,适合作为课程设计、毕业设计或入门级嵌入式项目的参考资料。 压缩包共21…

2026/10/19 17:01:18 阅读更多 →
Claude Code CLI 安装配置与实战指南:Node.js 环境、BASE_URL 与 AUTH_TOKEN 详解

Claude Code CLI 安装配置与实战指南:Node.js 环境、BASE_URL 与 AUTH_TOKEN 详解

1. 环境准备与基础依赖梳理1.1 为什么 Node.js 是绕不开的第一道坎Claude Code 这个 CLI 工具本质上是跑在 Node.js 运行时上的一个命令行程序,所以不管你用 Windows、macOS 还是 Linux,第一步都得先把 Node.js 装好。很多人卡在第一步不是因为不会装&am…

2026/10/19 17:01:17 阅读更多 →
从内置到自定义:LangChain复杂任务编排的Custom Chain与Agent实战

从内置到自定义:LangChain复杂任务编排的Custom Chain与Agent实战

把LangChain从“会用”推向“能真正处理复杂任务”,核心就在于自己动手写Custom Chain和Custom Agent。内置的链解决线性流程还行,一旦任务要求循环判断、多渠道取数、多工具协同、中途修改参数,搭积木式的内置方案就完全不够用了。这篇文章我…

2026/10/19 17:01:17 阅读更多 →
老毛桃v7.0 UEFI启动盘制作全指南:解决双模式识别失败

老毛桃v7.0 UEFI启动盘制作全指南:解决双模式识别失败

简介:老毛桃U盘启动盘制作工具(UEFI版 装机版)v7.0是一款面向电脑初学者与系统维护人员的轻量级系统辅助工具,专为快速制作兼容UEFI与传统BIOS的U盘启动盘、安装原版Windows系统及执行PE环境下的系统维护任务而设计。资源包共2个文…

2026/10/19 17:00:16 阅读更多 →
open-code-review:面向上下文的代码审查新范式

open-code-review:面向上下文的代码审查新范式

1. “open-code-review”不是工具名,而是一套可落地的代码审查新范式你有没有遇到过这样的场景:团队里刚入职的新人提交了一段看似逻辑通顺的Python函数,用pytest跑全量测试都通过了,但上线后在高并发下偶发空指针——不是语法错误…

2026/10/19 17:00:16 阅读更多 →
open-code-review:基于git diff的轻量级AI代码审查协议

open-code-review:基于git diff的轻量级AI代码审查协议

1. 这不是另一个“AI代码审查工具”,而是一套可落地的开源协作新范式“open-code-review”这个词,最近在技术社区里出现得越来越频繁,但它远不止是“用AI做Code Review”的简单包装。我从去年底开始在三个不同规模的团队里推动类似实践——从…

2026/10/19 17:00:16 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →