当前位置: 首页 > news >正文

【AI Infra 核心】从零剖析大模型服务框架:如何榨干 GPU 算力实现极致推理吞吐?

🚀【AI Infra 核心】从零剖析大模型服务框架:如何榨干 GPU 算力实现极致推理吞吐?

摘要:上一篇我们通过 PagedAttention 解决了大模型推理时的“显存爆炸”危机。但在实际的生产环境中,光有显存是不够的。老板花重金买的 A100/H100,如果 GPU 利用率只有 20%,那无异于暴殄天物。今天,我们深入剖析 LLM 服务框架(如 vLLM、TGI、TensorRT-LLM)的核心调度机制,用代码手写一个Continuous Batching(连续批处理)调度器,带你彻底搞懂如何榨干 GPU 的每一滴算力!

一、 传统 Static Batching 的原罪:“木桶效应”

在传统的深度学习(比如 ResNet 图像分类)中,为了提高 GPU 的吞吐量,我们通常会把多个请求拼成一个 Batch 一起送进显卡。这叫Static Batching(静态批处理)

但在大语言模型(LLM)的世界里,这套玩法行不通了。为什么?

因为 LLM 生成文本的长度是不可预测的。假设我们把 3 个请求打成一个 Batch 送去推理:

  • 请求 A:只需要生成 5 个 Token 就结束了(比如回答“北京的首都是哪?”)。
  • 请求 B:需要生成 50 个 Token。
  • 请求 C:需要生成 200 个 Token(比如写一篇小作文)。

在静态批处理下,整个 Batch 必须等待最长的“请求 C”全部生成完毕,才能整体返回并接收下一批任务。
结果就是:请求 A 的计算在第 5 步就结束了,在剩下的 195 步里,它对应的 GPU 算力完全处于闲置(Padding/Idle)状态!这就是典型的“木桶效应”,导致 GPU 算力被白白浪费。

二、 破局之道:Continuous Batching (连续批处理)

为了打破木桶效应,Orca 论文率先提出了Iteration-level Scheduling(迭代级调度),也就是后来工业界熟知的Continuous BatchingIn-flight Batching

它的核心思想非常暴力且有效:打破 Batch 的静态边界,把调度粒度细化到每一个 Token(每一次 Iteration)!

  1. 系统维护一个“正在运行”(Running)的请求池。
  2. 在每一个 Token 生成的 Step(即一次 Forward Pass)结束后,检查池子里的请求。
  3. 如果请求 A 生成完毕(遇到了 EOS token),立即把它踢出 Batch,返回给用户。
  4. 立即从等待队列中拉取一个新的请求 D,把它塞进刚刚空出来的 Batch 位置。
  5. GPU 继续执行下一个 Step 的计算。

通过这种“动态进出”的机制,GPU 永远处于满载状态,没有一滴算力被浪费在无意义的 Padding 上。

三、 纸上得来终觉浅:Python 徒手实现 Continuous Batching 调度器

为了让大家直观理解底层的调度逻辑,我们剥离掉复杂的 CUDA 和网络通信代码,用纯 Python 面向对象的方式,写一个极简版的 Continuous Batching 核心引擎。

1. 定义请求对象 (Request)

首先,我们需要一个类来记录每个请求的状态(是等待中、正在运行,还是已完成)。

fromenumimportEnumclassStatus(Enum):WAITING=0# 在队列中等待RUNNING=1# 正在 GPU 中生成FINISHED=2# 生成完毕classRequest:def__init__(self,req_id:int,prompt_len:int,max_output_len:int):self.req_id=req_id self.prompt_len=prompt_len self.max_output_len=max_output_len self.generated_tokens=0self.status=Status.WAITINGdefis_finished(self):# 简单模拟:达到最大输出长度或随机遇到终止符视为完成returnself.generated_tokens>=self.max_output_lendef__repr__(self):returnf"Req(id={self.req_id}, tokens={self.generated_tokens}/{self.max_output_len})"

2. 构建核心调度器 (Scheduler)

这是整个框架的大脑,它决定了每一个 Forward Step 中,哪些请求上 GPU,哪些请求下车。

classContinuousBatchingScheduler:def__init__(self,max_batch_size:int):self.max_batch_size=max_batch_size self.waiting_queue=[]# 等待处理的请求self.running_batch=[]# 正在 GPU 上跑的请求defadd_request(self,request:Request):"""接收上游网关发来的新请求"""self.waiting_queue.append(request)print(f"[API] 接收到新请求{request.req_id},加入等待队列。")defstep(self):""" 模拟一次 GPU 的 Forward Pass (生成一个 Token) """# 1. 踢出已完成的请求finished_reqs=[reqforreqinself.running_batchifreq.is_finished()]forreqinfinished_reqs:req.status=Status.FINISHED self.running_batch.remove(req)print(f" [-] 请求{req.req_id}已完成,离开 Batch。")# 2. 从等待队列拉取新请求,填补 Batch 空缺whilelen(self.running_batch)<self.max_batch_sizeandself.waiting_queue:new_req=self.waiting_queue.pop(0)new_req.status=Status.RUNNING self.running_batch.append(new_req)print(f" [+] 动态拉取请求{new_req.req_id}进入 Batch。")# 3. 模拟 GPU 推理:为当前 Batch 中的所有请求生成 1 个 Tokenifnotself.running_batch:print("GPU 闲置中...")returnFalse# 没有任务了print(f" [GPU Compute] 当前 Batch 状态:{self.running_batch}")forreqinself.running_batch:req.generated_tokens+=1returnTrue

3. 运行测试:见证奇迹的时刻

我们来模拟一个真实的请求流,看看它与静态 Batching 有什么不同。

# 初始化一个最大 Batch Size 为 3 的调度器scheduler=ContinuousBatchingScheduler(max_batch_size=3)# 模拟并发到来 5 个长度不一的请求scheduler.add_request(Request(1,prompt_len=10,max_output_len=2))scheduler.add_request(Request(2,prompt_len=12,max_output_len=5))scheduler.add_request(Request(3,prompt_len=8,max_output_len=3))scheduler.add_request(Request(4,prompt_len=20,max_output_len=4))scheduler.add_request(Request(5,prompt_len=15,max_output_len=2))# 驱动 GPU 进行推理,直到所有任务完成step_count=1whilescheduler.waiting_queueorscheduler.running_batch:print(f"\n=== GPU Step{step_count}===")scheduler.step()step_count+=1

运行结果解析:
在 Step 3 结束时,请求 1 已经达到了最大长度(2),被立刻踢出 Batch。同时,处于等待队列中的请求 4 立刻被补位拉入 Batch 中参与 Step 4 的计算。没有任何一个 GPU 时钟周期被用来做无意义的 Padding 等待!整体吞吐量在真实场景下可以翻 20 倍以上。

四、 进阶与博弈:Prefill 与 Decode 的分离

在上述代码中,我们做了简化,把每个 Step 视作等价的。但在真实的 LLM 推理中,阶段分为两步:

  • Prefill 阶段(预填充):处理用户的 Prompt,是计算密集型的矩阵乘法(GEMM)。
  • Decode 阶段(逐字生成):生成后续的 Token,是访存密集型的矩阵向量乘法(GEMV)。

在高端的工程落地中(如 vLLM 中的 Chunked Prefill 技术),为了防止新加入请求的超长 Prompt 把正在生成的 Decode 请求卡住(导致卡顿),会将长的 Prefill 切割成小块(Chunk),与 Decode 任务混合在一个 Batch 里执行。

五、 总结

大模型推理架构优化的本质,就是一场“压榨计算资源”“对抗显存墙”的战争。
通过结合上篇博客的PagedAttention(解决空间碎片)和本篇的Continuous Batching(解决时间碎片),现代 AI Infra 终于能够将 GPU 的利用率推向极致,让千亿参数模型的大规模商用化成为可能。

懂模型结构只能决定你能否跑通一个 Demo,而懂底层 Infra 调度,才决定了你能否扛住双十一级别的千万级高并发。

http://www.jsqmd.com/news/721173/

相关文章:

  • jQuery Masked Input项目架构分析:从Grunt构建到模块化设计
  • Forge模组进阶:深入Mixin内部机制,从字节码层面理解你的代码如何‘注入’Minecraft
  • 如何在5分钟内使用Ignite搭建你的第一个静态网站
  • SwiftyCam与AVFoundation对比:为什么选择这个简单易用的相机框架
  • 终极分布式训练指南:pytorch-image-models多节点加速实战
  • Centaur Emacs 代码补全与智能提示:提升开发效率的秘诀
  • Scroll Reverser深度解析:macOS设备专属滚动方向终极指南
  • 告别官方版!手把手教你用PyInstaller打包最新版LabelImg(保留自定义分类)
  • 别再乱设分片了!聊聊Elasticsearch分片数与周期索引的那些最佳实践
  • 5分钟打造你的终端视频通话:p2pvc极简入门指南
  • TypeScript交集计算终极指南:5步掌握Intersection类型挑战
  • 3分钟掌握Material-UI折叠面板:从基础到嵌套结构全攻略
  • AllTalk TTS Docker部署指南:容器化环境下的最佳实践
  • 第50篇:AI项目开发全流程复盘——从构思、实现到部署的完整指南(踩坑总结)
  • 杰理AC696X SDK实战:三种MIC能量采集方法,让你的灯效随声而动(附源码)
  • MyBatis ResultHandler实战:轻松导出百万数据到Excel,告别内存溢出
  • 基于安卓的生鲜配送智能补货系统毕设
  • 重塑WPF辉煌?基于DirectX 的现代.NET UI框架Jalium
  • FaceMaskDetection:10分钟快速上手开源人脸口罩检测项目
  • 正能量的本质的庖丁解牛
  • 别被官方文档坑了!用REDS数据集训练RealBasicVSR时,这几个配置细节决定成败
  • 别再硬编码了!用EPICS数据库实现一个温控系统,从Modbus设备到CSS界面全流程
  • Helm-Intellisense性能优化:如何配置linting和自动补全的最佳实践
  • 终极指南:如何在Source SDK 2013中打造智能NPC的近战与远程攻击系统
  • 别再死记公式了!用Python代码手搓一个Graph Transformer,直观理解它与GNN/Transformer的异同
  • TPFanCtrl2:ThinkPad风扇精准控制的开源解决方案
  • 论文查重软件怎么选?2026年实用工具整理盘点
  • Ambie白噪音应用:终极生产力提升工具完整指南
  • 告别代码泥潭:clean-code-javascript教你构建面向未来的可扩展系统
  • 大数据系列(五) Flink:真正的实时流处理,毫秒级延迟怎么做到的?