模型服务的批处理调度:攒批策略、超时控制与吞吐量优化
模型服务的批处理调度攒批策略、超时控制与吞吐量优化一、为什么一次处理 1 个请求和一次处理 16 个请求总耗时差距能到 4 倍GPU 推理有一个独特的性能特征单次推理和批量推理的延迟差距不大但吞吐量差距巨大。一块 A100 GPU 处理一个 prompt 需要 2 秒但同时处理 16 个 prompt 可能只需要 3 秒。这是因为矩阵乘法等操作在大 batch 下可以充分利用 GPU 的并行计算单元和高带宽显存。batch size 太小GPU 的许多计算单元闲着batch size 增大到一定程度利用率饱和再增大吞吐提升不再明显。这个特性决定了模型服务的调度策略和传统的 CPU 服务截然不同。传统服务追求来一个处理一个越快越好而模型服务需要攒一批再处理吞吐优先。问题在于攒批就意味着等待。如果攒齐 16 个请求需要 3 秒那第一个请求的等待时间就变成了 3 秒——对实时判题场景来说这个延迟不可接受。这就是批处理调度的核心矛盾吞吐量和延迟的博弈。flowchart TD A[请求到达] -- B[加入攒批缓冲区] B -- C{调度决策点} C -- D{缓冲区请求数 最大批大小?} D --|是| E[立即发起批推理] D --|否| F{最早请求等待时间 超时阈值?} F --|是| E F --|否| G[继续等待更多请求] G -- B E -- H[组装 batch prompt] H -- I[调用模型批量推理] I -- J[拆分推理结果] J -- K[逐个返回给对应请求]二、动态批处理的两大调度策略策略一固定窗口攒批。设置一个固定的时间窗口如 100ms在这个窗口内到达的所有请求组成一个批次。窗口到期后不管批大小是多少立即发起推理。这个策略简单直接延迟上限可控最长等待 窗口大小但批大小不确定——低峰期可能只有一个请求批处理的效果体现不出来。策略二双条件触发。同时设置批大小上限和等待超时。缓冲区请求数达到上限时立即触发缓冲区等待时间超时时也立即触发。哪个条件先满足就用哪个条件执行。这个策略在高峰期自动变成大 batch 高吞吐在低峰期自动变成小 batch 低延迟适应性最好。无论哪种策略都有一个关键的设计第一个请求的等待时间和最后一个请求的等待时间差异很大。在使用双条件触发时超时阈值应该基于第一个请求的等待时间来计算确保每个请求的最大等待延迟都有上界。三、批处理调度器的生产实现 模型批处理调度器 设计要点 1. 双条件触发批满或超时以第一个请求的等待时间为准 2. 异步返回每个请求持有自己的 Future推理完成后 set_result 3. 背压控制缓冲区满时拒绝新请求防止内存溢出 import asyncio import time from dataclasses import dataclass from typing import List, Dict, Any dataclass class BatchRequest: 单个批处理请求 prompt: str future: asyncio.Future # 用于异步返回结果的 Future arrival_time: float # 到达时间用于超时判断 request_id: str # 唯一标识用于结果匹配 class ModelBatchScheduler: def __init__( self, max_batch_size: int 16, # 最大批大小 max_wait_ms: int 200, # 最长等待时间毫秒 max_buffer_size: int 1000 # 缓冲区最大容量背压控制 ): self.max_batch_size max_batch_size self.max_wait_ms max_wait_ms / 1000.0 # 转换为秒 self.max_buffer_size max_buffer_size self.buffer: List[BatchRequest] [] self._loop_task: asyncio.Task None # 调度循环的任务引用 async def submit(self, prompt: str, request_id: str) - Dict[str, Any]: 提交一个推理请求 返回一个 awaitable调用方 await 它即可等待推理结果 如果缓冲区满直接抛异常拒绝 # 背压控制缓冲区满时拒绝新请求 # 这个保护很重要 —— 如果推理速度跟不上请求速度缓冲区会无限增长 # 最终导致 OOM。拒绝策略让上游感知到压力并做限流或降级。 if len(self.buffer) self.max_buffer_size: raise BufferFullError( f批处理缓冲区已满 (max{self.max_buffer_size}) ) # 创建 Future用于异步返回结果 future asyncio.get_event_loop().create_future() request BatchRequest( promptprompt, futurefuture, arrival_timetime.monotonic(), request_idrequest_id ) self.buffer.append(request) # 如果这是第一个进入空缓冲区的请求启动调度循环 if len(self.buffer) 1 and self._loop_task is None: self._loop_task asyncio.create_task(self._batch_loop()) # 等待推理结果 return await future async def _batch_loop(self): 批处理调度主循环 持续检查缓冲区满足条件时触发批推理 try: while self.buffer: # 等待批满或超时 await self._wait_for_batch_condition() if not self.buffer: break # 取出一批请求进行处理 batch self._pop_batch() # 异步执行批量推理不阻塞调度循环 asyncio.create_task(self._process_batch(batch)) finally: self._loop_task None async def _wait_for_batch_condition(self): 等待批处理条件满足 两个条件之一满足即可 - 缓冲区请求数达到 max_batch_size - 最早请求的等待时间超过 max_wait_ms while self.buffer: # 条件1批已满 if len(self.buffer) self.max_batch_size: return # 条件2最早请求等待超时 first_arrival self.buffer[0].arrival_time elapsed time.monotonic() - first_arrival if elapsed self.max_wait_ms: return # 还没满足条件短暂等待后重试 # sleep 时间要够短保证批满时能立刻响应 await asyncio.sleep(0.01) # 10ms 检查间隔 def _pop_batch(self) - List[BatchRequest]: 从缓冲区取出一批请求 batch_size min(self.max_batch_size, len(self.buffer)) batch self.buffer[:batch_size] self.buffer self.buffer[batch_size:] return batch async def _process_batch(self, batch: List[BatchRequest]): 执行批量推理 组装 prompt → 调用模型 → 拆分结果 → 逐个 set_result try: # 组装批量 prompt prompts [req.prompt for req in batch] # 调用模型批量推理 # 这里假设 model.infer_batch 是异步方法 results await model_service.infer_batch(prompts) # 拆分结果并返回假设结果顺序与输入顺序一致 for req, result in zip(batch, results): if not req.future.done(): req.future.set_result(result) except Exception as e: # 批量推理整体失败所有请求都标记为异常 # 生产环境可以更精细化处理如部分失败时重试 for req in batch: if not req.future.done(): req.future.set_exception(e) class BufferFullError(Exception): 缓冲区满异常 pass调度循环中的asyncio.sleep(0.01)看似粗暴实际上在 asyncio 中是高效的做法。10ms 的检查间隔在批大小未满的情况下引入了微小的延迟但在批满的情况下len(self.buffer) self.max_batch_size条件在每次循环都会被快速检查到。异步设计中_process_batch使用了asyncio.create_task而不是await。这个区别很关键如果 batch processing 本身需要 5 秒用await会让调度循环在 5 秒内无法接收新请求——这正好违背了批处理的本意——让后续到达的请求能进入下一个批次。四、批处理的边界场景冷启动批处理系统刚启动时只有零星的请求如果 max_wait_ms 设为 200ms这些请求都要等满 200ms 才能被处理。解决方案是设置动态等待时间——通过近期的请求到达率来预测再等多久可能凑够一个批次如果概率很低就直接发送。请求大小不均不同题目的 prompt 长度差异很大。简单的判断语法正确性的 prompt 可能只有 500 tokens复杂算法分析的 prompt 可能有 3000 tokens。GPU 的显存是按最长的 prompt * batch_size 来分配的如果批次中有特别长的 prompt会大量浪费显存。解决方案是按 prompt 长度做分组批处理相同长度区间的请求组成一批。批次取消用户可能在等待过程中取消了请求关闭页面。这时候对应的 prompt 应该从缓冲区中移除释放 Future 占用的资源。五、总结批处理调度是 GPU 推理场景下的核心优化手段。双条件触发批满 超时是最实用的调度策略它在高负载时自动利用批处理提高吞吐在低负载时自动缩短等待时间保证延迟。实现上需要注意背压保护、异步调度循环、失败时批量异常处理等细节。批大小和超时阈值这两个参数没有统一的最优值需要根据实际压测数据来调优但原则是一致的在延迟可接受的范围内批越大越好。

相关新闻

基于若依开发国内java开源cms框架,推荐一款免费开源java cms内容管理系统,

基于若依开发国内java开源cms框架,推荐一款免费开源java cms内容管理系统,

RuoYi-Fast-CMS 网站内容管理系统介绍 版本:v4.8.3 | RuoYi-Fast-CMS 一、系统概述 RuoYi-Fast-CMS 是一款基于 若依管理系统(RuoYi-Fast) 二次开发的专业级 Java CMS 网站内容管理系统。系统以 “让内容管理更简单、更灵活” 为设计理念&am…

2026/7/21 16:43:19 阅读更多 →
AI 辅助前端技术债务量化:复杂度指标、变更频率与风险热力图的关联分析

AI 辅助前端技术债务量化:复杂度指标、变更频率与风险热力图的关联分析

AI 辅助前端技术债务量化:复杂度指标、变更频率与风险热力图的关联分析 一、技术债务的隐性问题:为什么"感觉有问题"不足以驱动重构 前端团队对技术债务的直觉判断往往基于两类信号:代码"看着乱"和开发者"改着疼&qu…

2026/7/23 3:03:23 阅读更多 →
编译原理硬核词法分析器实战——转换图、最长匹配与 Lex/Flex(九)

编译原理硬核词法分析器实战——转换图、最长匹配与 Lex/Flex(九)

1. 定位导航 #8 给了规约语言(正则定义),本篇给执行装置: 正则定义→画成转换图→直译成代码(或交给 Lex/Flex 自动生成)\text{正则定义} \xrightarrow{\text{画成}} \text{转换图} \xrightarrow{\text{直译成}} \text{代码} \quad\big(\text{或交给 Lex/Flex 自动生成}\…

2026/7/24 5:52:46 阅读更多 →

最新新闻

三大主流大模型API调用实战与优化指南

三大主流大模型API调用实战与优化指南

1. 大模型API调用实战指南 最近在开发一个智能写作助手时,需要同时对接多个主流大语言模型的API。经过两周的踩坑和调试,终于实现了通过API Key稳定调用DeepSeek、GLM和OpenAI三大平台的经验。分享下我的完整实现方案和避坑心得。 2. 核心工具选型与准…

2026/7/24 9:08:00 阅读更多 →
3GB显存设备运行SD3 Medium的ComfyUI优化方案

3GB显存设备运行SD3 Medium的ComfyUI优化方案

1. 项目概述:3GB显存设备的SD3 Medium实战挑战 当Stable Diffusion 3 Medium(SD3 Medium)模型发布时,大多数教程都默认用户拥有8GB以上的显存设备。但现实中,大量创作者仍在使用GTX 1060、RTX 3050等3-4GB显存的老设备…

2026/7/24 9:08:00 阅读更多 →
AI专著生成工具:核心技术解析与选型指南

AI专著生成工具:核心技术解析与选型指南

1. AI专著生成工具的核心价值解析 在学术写作领域,专著创作向来被视为最具挑战性的任务之一。传统专著写作需要作者具备系统的知识架构、严谨的逻辑思维和持久的创作耐力,从选题立项到最终成书往往需要数年时间。而AI专著生成工具的出现,正在…

2026/7/24 9:08:00 阅读更多 →
C++自定义异常类设计:从基础原理到工业级实现

C++自定义异常类设计:从基础原理到工业级实现

1. 项目概述:为什么我们需要自定义异常类?在C的世界里,异常处理是构建健壮、可靠程序的关键防线。标准库提供了一套基础的异常类型,比如std::runtime_error、std::logic_error,它们能处理很多通用错误。但当你深入到一…

2026/7/24 9:08:00 阅读更多 →
嵌入式RTC模块深度解析:从日历闹钟到低功耗定时器实战

嵌入式RTC模块深度解析:从日历闹钟到低功耗定时器实战

1. 项目概述与RTC核心价值 在嵌入式系统开发中,尤其是那些对功耗极其敏感、需要长时间独立运行的设备里,一个可靠且低功耗的实时时钟(RTC)模块往往是系统设计的“心脏”。它不仅仅是记录一个简单的秒数,更是维系整个系…

2026/7/24 9:08:00 阅读更多 →
SuperCLUE报告解析:2025中文大模型技术趋势与应用

SuperCLUE报告解析:2025中文大模型技术趋势与应用

1. SuperCLUE报告的核心价值解析 SuperCLUE作为中文大模型领域的权威测评基准,其发布的《2025年中文大模型发展全景报告》具有三个维度的独特价值: 首先在技术评估层面,报告建立了覆盖语言理解、生成质量、逻辑推理、多模态交互等12个核心能…

2026/7/24 9:06:59 阅读更多 →

日新闻

用Highcharts 创建可拖拽三维散点立方体3D图表

用Highcharts 创建可拖拽三维散点立方体3D图表

该案例基于Highcharts scatter3d 三维散点图实现空间立方体散点可视化,核心特色:三维 X/Y/Z 三轴空间,所有散点分布在 0~10 立方体空间内;散点使用径向渐变实现立体 3D 圆球质感;支持鼠标 / 触屏拖拽画布,…

2026/7/24 0:00:29 阅读更多 →
AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls:进程创建路径上的 DLL 入口 AppCertDlls 位于 HKLM\System\CurrentControlSet\Control\Session Manager\AppCertDlls。本文的程序功能是只读列出这个键在 64 位和 32 位注册表视图中的全部值,并显示每条值的来源、名称、类型和可安全显示的数…

2026/7/24 0:00:29 阅读更多 →
我的编程之路:第一篇博客

我的编程之路:第一篇博客

大家好,我是一名编程初学者,同时这也是我编程学习之路上的第一篇博客。在这里,我想要向大家介绍我的一些想法和规划。a.自我介绍我是一个刚刚接触编程的新手,目前在学习c语言,我对编程世界充满了强烈的好奇。当然&…

2026/7/24 0:00:29 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/24 3:59:20 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/24 1:23:39 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/23 17:49:47 阅读更多 →

月新闻