在高性能大模型推理系统中,批处理(Batching)是提升吞吐量(Throughput)最有效的手段。然而,LLM(Large Language Model)推理场景的特殊性——输入Prompt长度不一、输出Token数量不可预测,使得传统的批处理策略显得捉襟见肘。
本文将深入剖析DeepSeek推理服务中的核心优化技术:Dynamic Batching(动态批处理),亦称为Continuous Batching(连续批处理),并探讨其在昇腾NPU环境下的实现细节。
1. 传统困境:Static Batching的效率黑洞
在传统的深度学习推理(如ResNet图像分类)中,Batching非常简单:凑够N张图,打包成一个Tensor,送入GPU计算。这种策略被称为Static Batching(静态批处理)。
但在LLM场景下,Static Batching面临两个致命问题:
1.1 Padding带来的算力浪费
假设我们设定Batch Size = 4,但这4个请求的输入长度分别是[10, 20, 100, 50]。为了构建一个规则的Tensor矩阵(Shape:[4, 100]),必须将短序列用Pad Token(通常是0)填充至最长序列的长度(100)。
这意味着:
- 请求1(长度10):有90%的计算量是在算无效的Pad。
- 请求2(长度20):有80%的计算量是无效的。
在极端情况下,如果混入了一个超长Prompt,整个Batch的推理延迟将被这个“长尾”请求拖垮,且大部分算力都在做无用功。
1.2 “队头阻塞”问题
LLM是自回归生成模型,生成长度是未知的。
- 请求A生成了10个Token就结束了(
<EOS>)。 - 请求B生成了500个Token还在继续。
在Static Batching模式下,必须等待请求B完全生成结束,请求A才能返回给用户。这导致请求A的用户感知延迟(Latency)极高,体验极差。这就像坐公交车,全车人必须等最后一名乘客下车才能发车。
2. 破局之道:Continuous Batching (Orca机制)
2023年,OSDI发表了一篇里程碑式的论文Orca: A Distributed Serving System for Transformer-Based Generative Models,提出了Iteration-level Scheduling(迭代级调度)的概念,彻底改变了LLM推理的游戏规则。
2.1 核心理念:不等车,随到随走
Continuous Batching 将调度的粒度从“请求级”下沉到了“迭代级”(Token级)。系统不再维护一个固定的Batch,而是维护一个动态的请求池。
每一次生成Token的迭代(Step),系统都会检查当前状态:
- 清理(Evict):检查哪些请求生成了
<EOS>结束符。如果有,立即释放其占用的显存槽位。 - 填充(Fill):如果当前Batch内的请求数未达到上限(Max Batch Size),或者Token总数未达到显存瓶颈,就从等待队列(Waiting Queue)中拉取一个新的请求插入。
- 合并(Merge):将新加入请求的Prefill(首字生成)阶段与现有请求的Decode(后续生成)阶段合并,组成一个新的Batch进行一次前向计算。
这种机制下,显卡几乎永远处于满载状态,且没有Padding浪费。短请求处理完立即返回,长请求继续生成,互不干扰。
2.2 显存管理的基石:PagedAttention
Dynamic Batching的高效运作离不开显存管理的革新。由于请求是随机进出的,如果使用连续内存存储KV Cache,必然导致严重的内存碎片化。
PagedAttention(vLLM的核心技术)借鉴了操作系统虚拟内存的分页思想:
- 将KV Cache切分成固定大小的Block(例如每块存储16个Token)。
- 在显存中维护一个Block Table,记录逻辑Block到物理Block的映射。
- 生成的Token可以存储在显存的任意位置,不再要求物理连续。
这就好比操作系统管理内存,无论进程申请多少内存,底层都是由离散的Page组成的。这使得显存利用率接近100%,从而支持更大的Batch Size。
3. 在昇腾上实现Dynamic Batching
在昇腾CANN架构上,我们通常通过MindIE (MindSpore Inference Engine)来启用这一特性,或者在PyTorch层结合Ascend Speed实现。
3.1 方案一:MindIE原生配置(推荐)
MindIE Service是华为官方推出的推理服务框架,底层已经深度集成了Continuous Batching和PagedAttention。
只需在config.json中进行简单配置:
{"scheduler":{"type":"continuous_batching","max_batch_size":128,// 最大并发请求数"max_input_len":4096,// 最大输入长度"max_output_len":2048,// 最大输出长度"max_waiting_queue":2000// 等待队列长度},"model":{"block_size":16,// PagedAttention的块大小"max_prefill_tokens":8192// 限制单次迭代处理的最大Token数,防止OOM}}关键参数调优建议:
block_size:通常设为16或32。太小会导致Block Table过大,太大会导致块内碎片(Internal Fragmentation)。max_prefill_tokens:非常关键。如果新进来的Prompt很长,为了避免Prefill阶段耗时过长导致Decode阶段卡顿(抖动),可以限制单次Prefill的Token数,启用Chunked Prefill(分块预填充)。
3.2 方案二:Python自定义调度(灵活性高)
如果你需要更复杂的调度策略(例如基于优先级的VIP通道),可以在Python层实现一个简易调度器。以下是一个简化版的实现逻辑:
classContinuousBatchScheduler:def__init__(self,model,max_bs=32):self.active_reqs=[]# 正在生成的请求self.waiting_queue=deque()# 等待队列self.model=model self.max_bs=max_bsdefstep(self):# 1. 移除已完成的请求(EOS或达到最大长度)# 这一步需要收集上一次推理的输出,判断stop_reasonfinished_ids=[req.idforreqinself.active_reqsifreq.is_finished()]self.active_reqs=[reqforreqinself.active_reqsifnotreq.is_finished()]# 释放显存Block(伪代码)self.block_manager.free(finished_ids)# 2. 动态填充新请求# 策略:在显存允许且Batch未满的情况下,尽可能多地加入新请求whilelen(self.active_reqs)<self.max_bsandself.waiting_queue:# 预估加入该请求后是否会OOMcandidate_req=self.waiting_queue[0]ifnotself.block_manager.can_allocate(candidate_req):breaknew_req=self.waiting_queue.popleft()self.active_reqs.append(new_req)self.block_manager.allocate(new_req)# 3. 构造Batch输入# 这一步极其复杂,需要处理Ragged Tensor(不规则张量)# 通常配合Flash Attention的varlen接口,传入cu_seqlensinputs,cu_seqlens,position_ids=self.prepare_inputs(self.active_reqs)# 4. 执行推理logits=self.model(inputs,position_ids)# 5. 更新请求状态self.update_requests(self.active_reqs,logits)defprepare_inputs(self,reqs):# 将所有请求的Token拼接成一个长的一维Tensorinput_ids=torch.cat([req.tokensforreqinreqs])# 计算累积长度,供Flash Attention使用seqlens=[len(req.tokens)forreqinreqs]cu_seqlens=torch.tensor([0]+list(accumulate(seqlens)),dtype=torch.int32)returninput_ids,cu_seqlens4. 挑战与权衡
虽然Dynamic Batching是提升吞吐量的神器,但在实际落地中也面临挑战:
4.1 调度开销(Scheduling Overhead)
在每一轮生成Token之间,都要运行一次Python调度逻辑(移除、添加、构造Tensor)。如果Python代码运行太慢(例如超过5ms),而NPU生成一个Token只需要10ms,那么调度开销就占了33%,严重拖慢整体速度。
- 优化策略:使用C++重写调度器(如vLLM的做法),或者使用CUDAGraph/AscendGraph固化计算图。
4.2 显存碎片化与Swap
虽然PagedAttention解决了碎片化,但当负载极高时,显存仍然可能耗尽。此时需要将部分KV Cache从NPU显存Swap Out到CPU内存,等有空位了再Swap In。这涉及复杂的内存分级存储管理。
4.3 精度对齐
由于Dynamic Batching使用了Flash Attention的变长接口和自定义的位置编码,验证其与HuggingFace原生实现的精度对齐(Perplexity一致性)是一个繁琐但必须的过程。
5. 总结
Dynamic Batching 实际上是将时间维度的不确定性(生成长度)转化为空间维度的确定性(显存填满)。在DeepSeek的高并发服务中,开启Dynamic Batching通常能带来2-4倍的吞吐量提升,是构建生产级推理服务不可或缺的基石。对于昇腾开发者而言,优先使用MindIE是最高效的路径,但在特殊场景下,理解其背后的调度原理对于深度调优至关重要。