ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI推理GPU调度优化:从假忙到高效,动态Batching与显存复用实战

AI推理GPU调度优化:从假忙到高效,动态Batching与显存复用实战 先说一个我反复见到的场景线上跑着一个大模型推理服务业务方天天投诉响应慢、请求排队你一看nvidia-smiGPU利用率只有20%上下显存却占了70%以上。于是老板催着加卡卡加了两倍吞吐没有翻倍该排队还是排队。这个问题我见过太多次了根子往往不在卡不够而在GPU资源调度这块没做透——算力没吃满、显存没复用、请求没合并、队列设计粗糙还经常出现低优先级任务把高优先级任务堵死的情况。这篇内容就是围绕AI模型推理场景下的GPU资源调度优化来写的。我先说清楚一个结论推理和训练的GPU用法完全不同训练是“一个任务吃满整张卡跑几个小时”推理是“大量小请求持续到达GPU大多数时间在等数据、等同步、等串行计算结束”。所以这里的优化重点不是“把卡跑满”而是在延迟、吞吐、显存三者之间找平衡。适合正在做模型部署、推理服务性能调优、AI平台资源管理的工程师看也适合刚把模型跑通、准备上生产的同学做参考。1. 推理场景的GPU为什么总是“假忙”先从这两个矛盾说起很多人在做推理优化时上来就调参数结果越调越乱就是因为没有理解推理服务在GPU层面到底处于什么状态。我先拆一下最常见的两个矛盾。1.1 显存很紧张算力却在休息大模型推理和传统CNN推理有一个显著差异权重本身很大7B模型仅FP16权重就要占14GB左右13B要26GB70B要140GB。这个权重是常驻显存的不管当时有没有请求它都占着。于是你会看到显存使用率很高但这不代表GPU在干活。真正干活的是SM流式多处理器也就是算力单元。它只有在矩阵乘法、注意力计算这些算子执行时才忙起来。问题在于单条请求的推理过程有严格的先后依赖先走prefill阶段预填充把输入的Prompt一次性算完生成KV Cache然后进入decode阶段逐token生成每一步只能并行处理当前token相关的那一小部分计算。在decode阶段每一步的计算量比prefill小得多SM利用率自然就掉下来了。整张A100的SM利用率跑30%甚至20%都是很常见的事情。我在排查过的服务里做过统计单请求并发数为1时A100上跑7B模型SM利用率长期在15%到35%之间波动。显存占着14GB以上算力一大半在空转。这就是第一个矛盾显存被常驻权重占满但算力因为单个请求的串行依赖而饥饿。1.2 单请求串行是最大的隐性浪费如果你的服务没有做请求合并Batching来了一个请求就创建一个推理任务独占GPU跑完再放行下一个那么GPU在两次请求之间至少要浪费一部分时间——前一个请求的收尾阶段算力已经空闲后一个请求的显存还没来得及分配。更隐蔽的浪费是请求内部的prefill和decode阶段时间差。一个短请求可能prefill只花10毫秒decode要花300毫秒一个长请求prefill可能要800毫秒。如果这些请求串行执行整张GPU的算力就跟着最慢的那个请求跑其他请求全部排队。用户感受到的就是延迟高、吞吐低而GPU自己也没占到便宜。这类问题通过Nsight Systems做一次profile就能看得很清楚时间轴上全是“空闲间隙”。有次我看一个服务的profile单张卡上GPU空闲时间占比超过了40%但业务方坚持说“请求量很大卡不够用”。后来加了动态Batching没加卡吞吐直接翻了2.6倍。1.3 调度优化的本质填满GPU空闲时间所以推理场景下的GPU资源调度本质就是在不破坏单请求时延的前提下把GPU的空闲时间填满。填法主要有三个方向把多个请求的计算合并成一个大矩阵乘法提升单次算力利用率。这就是Batching优化也是效果最立竿见影的一步。把KV Cache和相关显存做精细化管理让有限的显存容纳更多并发请求从而提高GPU的持续负载。把多个模型、多个业务方的请求按优先级合理编排避免低价值任务占用高价值资源。这三件事听着不复杂但落地时每一件都有不少细节。后面我把完整路径拆开讲包括每个阶段用什么工具去验证、参数怎么调、常见的坑在哪里。2. 动手优化前先搞清楚四个指标和一个工具链推理服务优化最忌讳拍脑袋。我看过有人把gpu_memory_utilization从0.9改成0.5理由是“显存占用太高了”结果吞吐反而降了30%。所以动手之前先把指标口径和排查工具理清楚。2.1 四个核心指标的含义和计算口径推理GPU调度涉及四类指标它们之间是互相牵制的关系指标含义常见口径注意点吞吐量Throughput单位时间内完成的请求数每秒请求数QPS或每分钟完成的推理次数别只看QPS要看token级吞吐即每秒生成token数时延Latency单个请求从进入到返回的耗时一般看P50、P95、P99平均时延没意义长尾直接影响用户体验算力利用率GPU SM的有效工作时间占比nvidia-smi里的利用率这个值高不代表延迟好可能是Batching过大导致排队显存占用率显存使用量与总量之比看显存分配峰值、均值、回收曲线峰值接近上限时要警惕OOM和显存碎片我特别想强调一下“算力利用率”这个指标。很多同学看到利用率低就调大Batch这是对的但不能只看平均值。比如服务偶发大批量请求时利用率冲到98%但P99延迟从300ms暴涨到3秒用户直接超时。这里的问题不是算力不够而是调度策略没有做“延迟感知”。所以正确的做法是同时盯住P99延迟和SM利用率它们之间的平衡点才是你要找的目标。2.2 显存分配模式比峰值更值得关注推理服务里显存分配是动态的。每次请求进来会分配KV Cache空间请求结束释放。所以显存使用量是一条锯齿形曲线。只看nvidia-smi的瞬时值往往看不到问题的全貌。我自己的习惯是记录显存分配曲线重点看三点峰值有没有逼近设备上限。如果峰值接近上限说明需要增加max_num_seqs限制或降低gpu_memory_utilization。锯齿幅度大不大。如果波动剧烈说明分配释放频繁显存碎片化风险高。是否存在持续增长不回落。如果是大概率是有显存泄漏别优化了先修Bug。举个例子我优化过一个服务显存占用3天之内从62GB涨到78GB重启后恢复然后继续涨。最开始以为是碎片问题但用nvidia-smi --query-gpumemory.total,memory.used,memory.free采样后发现是某个后处理模块在并发请求结束后没有释放中间张量。修完之后显存曲线平稳多了吞吐也稳了。2.3 用nvidia-smi和DCGM透视真实负载定位问题前先搭建一套能持续记录的监控。单次执行nvidia-smi看瞬间状态是不够的因为推理负载波动很快。我自己常用的组合是# 持续采样GPU利用率、显存、温度、功耗间隔1秒 nvidia-smi dmon -s pucvmet -d 1 # 查询指定GPU的细粒度信息 nvidia-smi --query-gpuindex,utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw \ --formatcsv -l 1如果需要更完整的指标建议用DCGMData Center GPU Manager。它可以直接接Prometheus暴露DCGM_FI_PROF_GR_ENGINE_ACTIVE、DCGM_FI_DEV_MEM_COPY_UTIL、DCGM_FI_DEV_GPU_UTIL这些指标。特别是DCGM_FI_PROF_PIPE_TENSOR_ACTIVE能告诉你张量核心实际忙了多久比单纯看GPU利用率更接近真相。比如我遇到过一次异常GPU利用率显示95%但QPS很低。一查DCGM_FI_PROF_PIPE_TENSOR_ACTIVE只有40%说明有大半时间在跑非张量计算比如CPU拷贝、内存搬运、编译内核而不是真正在做矩阵运算。这就是为什么只看GPU利用率是不靠谱的原因。2.4 快速自检清单在开始改配置之前我建议你先把下面几项过一遍[ ] 当前服务是单请求串行还是已经做了Batching[ ] 请求的Prompt长度分布是什么是否有大量超长输入拖慢P99[ ] 当前max_num_seqs最大并发序列数是多少有没有因为限制太严导致GPU排队[ ] 显存分配曲线是平稳的还是剧烈锯齿状[ ] GPU的Tensor Core利用率而不是单纯的GPU利用率是多少[ ] 请求排队发生在哪一层是前端超时还是推理框架内部排队这一套检查下来基本能定位到问题的大致方向。接下来才进入真正的优化动作。3. 显存复用和动态Batching把“来了一个算一个”改成“攒一批算一批”在所有优化手段里动态Batching的收益是最直接、最可预期的。但实现方式很讲究不同框架的默认行为也差异很大。这一节我把原理、参数和实测结果放在一起讲清楚。3.1 静态Batching为什么不够用最早期的Batching是非常简单的“静态Batching”设定一个固定Batch Size比如32凑满32个请求才一起处理。这个方案的问题很明显——请求不是均匀到达的高峰期可能超过32低峰期只有两三个凑不齐就得等延迟直接飙上去凑满了又可能阻塞后续高优请求。后来的改进是“动态Batching”到了一个Batch就执行执行完再收下一批。这解决了“凑不齐空等”的问题但长请求仍然会卡住Batch里的短请求因为同一个Batch必须等最长的那个decode步骤执行完才能整体进入下一步。短请求被长请求拖累的情况依旧存在。3.2 Continuous Batching与前缀复用目前主流推理框架vLLM、TensorRT-LLM、TGI都实现了Continuous Batching也叫“连续批处理”或“级联批处理”。它的核心思路是不再把请求固定在一批里而是每步decode结束之后重新检查所有序列的状态。完成了的请求立刻移出Batch释放它的显存和计算位置新到的请求立即补进来。这样做的好处是只要GPU还有剩余算力新请求就能立刻开始执行不用等“整批完成”。这个机制极大提高了设备利用率也是vLLM这类框架吞吐表现好的主要原因。还有一项容易被忽略的优化是Prefix Caching前缀缓存也叫RadixAttention。如果多个请求有相同的Prompt前缀比如同一个System Prompt、同一个知识库上下文框架可以把这段共用前缀对应的KV Cache缓存起来新请求直接跳过这部分计算和显存分配。实测下来对于多轮对话、RAG这类前缀复用率高的场景吞吐能提升20%到50%显存开销也明显下降。3.3 KV Cache和显存水位管理KV Cache是推理显存消耗的另一大头。前面说过每个token对应的KV Cache量级在0.5MB左右具体取决于模型结构如果模型输入输出很长并发又高KV Cache会迅速占满显存。这里的核心问题是KV Cache不够了怎么办传统的做法是把显存划出一块固定区域当作KV Cache池超出部分要么报错要么把请求排到队尾等待。PagedAttention的做法则不同它把KV Cache切成分页大小的块按需分配块之间通过索引表串联。这样显存碎片大幅减少利用率也能提上去相当于把操作系统的虚拟内存思想搬到了GPU显存管理上。在vLLM这类框架里和显存水位管理直接相关的参数有几个gpu_memory_utilization控制框架占用显存的比例。默认0.9太高容易在并发高时频繁OOM我常用的区间是0.75到0.85。max_num_seqs最大并发序列数。它决定了KV Cache能分给多少个请求。设置太大可能导致单个请求可用的KV Cache变少反而降低单请求的最大生成长度设置太小则并发吞吐上不去。max_model_len模型支持的最大序列长度。它会影响KV Cache池的规划如果值设得比实际业务大很多预留的KV Cache空间会被浪费不如按业务上限精确设置。3.4 量化不是万能的但该做就做这里顺带说量化。很多人看到权重占显存太大就急着上INT8量化。量化确实能显著降低显存占用比如7B模型从14GB降到7GB左右KV Cache还可以进一步用INT8存储腾出大量空间给并发请求。但不能只看显存收益还要看精度和算力变化。如果用CPU做反量化或算子不支持融合INT8在GPU上的提速效果可能并不明显甚至因为反量化额外开销导致单次推理变慢。我在1张T4上跑过测试FP16转INT8后显存节省约40%但端到端吞吐只提升15%左右瓶颈反而变成了算子融合程度。所以我的建议是如果模型支持且部署框架已经做了算子级INT8优化比如TensorRT-LLM那就果断用如果只是简单地在PyTorch里套一层torch.quantization要慎重跑完整测试再上线。# 一个简单的配置示例vLLM启动参数 python -m vllm.entrypoints.openai.api_server \ --model /model_path/7b \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.85 \ --max-num-seqs 128 \ --max-model-len 4096 \ --enable-prefix-caching4. 多模型和多人共用一张卡共享型推理服务的调度策略比单模型推理更麻烦的是在一张或几张GPU上同时跑多个模型、多个业务方。很多时候你没法给每个模型单独分配一张卡那就必须有一个共享调度的方案。这一节是生产环境里最容易出问题的部分。4.1 GPU共享的几种主流方式对比共享方式原理优点缺点适用场景时间片轮转多个进程轮流使用GPU实现简单兼容性最好上下文切换开销大利用率不高低负载、对延迟不敏感MPSMulti-Process ServiceCUDA进程共享一个GPU上下文由MPS server统一调度比时间片效率高能复用内核执行MPS本身可能成为瓶颈需要额外配置多个小模型并发推理MIGMulti-Instance GPU将GPU物理切分为多个独立实例实例间隔离性强显存算力独立单实例性能受限A100/H100才支持多租户强隔离场景显存 优先级调度框架层用不同优先级队列编排请求共享整卡算力灵活度高能按业务权重分配需要应用层配合平台型推理服务我实际使用下来最容易被忽视的是MPS。它和普通多进程访问的区别在于MPS模式下多个进程的计算内核会被合并调度显存和计算资源能更高效复用。但它的坑是如果某个进程发起了cudaMalloc或者调用某个不支持MPS的库会拖慢整体性能。所以MPS适合模型加载完成、稳定运行阶段不适合频繁动态加载模型的场景。4.2 请求队列和优先级的设计思路多业务共享一张卡最怕的是“一视同仁”。一个实时性要求极高的人机对话请求和一堆离线批量分析任务排在同一条队列里前者会被堵到完全不可用。这里需要做分级调度。我在实现平台型推理服务时用的策略是三级队列优先级最高的“交互型请求”占比小延迟敏感直接进“优先通道”抢占算力。普通“API型请求”延迟要求中等走常规Batch通道。“离线批量任务”对延迟无要求只在GPU空闲时执行比如通过填充裕度控制是否启动。具体到实现可以用类似优先队列的机制在推理框架前加一层调度模块。比如在Triton里可以通过配置Dynamic Batcher的Priority字段来实现不同请求的优先级区分如果想更灵活就在自己的Gateway里根据请求类型打标签再决定放入哪个队列。# Triton Dynamic Batcher 配置片段示例 dynamic_batching { preferred_batch_size: [4, 8, 16] max_queue_delay_microseconds: 200 priority_queue_policy { default_priority_queue_policy { default_timeout_microseconds: 1000 allow_timeout_override: true } } }这里有一个关键点高优先级队列要设置超时上限不能让高优请求无限等待低优请求释放资源。同时要防止高优请求过多时低优任务被饿死——我一般给每个队列设置权重按权重分配算力而不是设绝对抢占。4.3 从静态分配走向资源池化再往上走一步就是把GPU资源变成“池子”而不是绑定给某个具体模型。Kubernetes配合Device Plugin做GPU调度已经是比较成熟的做法但没有资源池化时一个Pod占用一张卡即使只用了20%算力卡也不能给别的任务用。更好的做法是引入“显存单元化”概念把显存当作可分配的最小单位比如256MB为一格按业务峰值需求分配显存而不是整卡分配。这样多个模型可以共用一个GPU实例。Kubernetes里的做法是给不同请求打上显存配额标签并让调度器按显存大小选择节点。NVIDIA的MIG可以做到物理隔离但MIG的粒度固定在几个档位不如显存配额灵活。如果你想自己搭一套轻量化的方案可以考虑用NVIDIA的CUDA MPSGKE/ACK这类容器平台的扩展点或者直接用支持共享显存的框架比如NVIDIA Triton的多模型并发执行Multi-Model并发。我自己用过VGPU方案遇到老驱动兼容问题比较多生产环境更建议用官方支持较好的MPS/MIG或专业平台方案。4.4 关于GPU实例化的一个常被误解的点热词里有“gpu实例化到底减少的是什么?具体原理是什么”这个值得单独说一下。很多人以为GPU实例化Instantiation / Context减少的是显存占用其实不准确。GPU实例化减少的是“CUDA Context建立和切换的开销”。每个CUDA进程都会有自己的上下文上下文的初始化、显存映射、内核模块加载都有成本。当你反复创建销毁短生命周期进程时比如每次请求拉起一个Python子进程跑推理上下文创建的开销会占大头。实例化复用就是提前把Context建好避免重复开销。如果服务是长驻进程模式一次初始化跑很久那实例化带来的收益就很小了。这也是为什么我强烈建议推理服务用长驻的推理引擎进程而不是每次请求拉进程。后者不仅有上下文开销还会带来显存申请释放的碎片化问题。5. 一次完整的调度优化实战从排队五分钟到P99 800ms前面讲了很多概念这一节我用一个真实业务场景的复盘把优化路径完整串一遍。这个案例综合了动态Batching、优先级调度和显存管理收益也比较典型。5.1 开局一个“看起来什么问题”的典型服务场景是某对话类AI服务背后是一个7B模型。业务方的反馈是“高峰期请求响应太慢经常排队几分钟”技术团队的初步判断是“卡不够需要加2张A100”。我接手时看到的状态单张A100服务用的是未做Batching的长驻进程每个请求单独推理。显存占用65%左右权重14GB 部分缓存算力利用率不到25%。请求量确实大高峰期每秒大约8到10个请求每个请求的推理耗时为800ms到1500msQPS和延迟完全对不上。5.2 先量基线再动手改我第一步是搭建监控基线用DCGM采集了48小时的数据。核心结果如下P50延迟约950msP99延迟约2.8秒高峰期排队请求平均35个GPU平均利用率23%GPU显存平均占用64%Tensor Core利用率约18%这时候问题已经很明确GPU没有在干活但请求在等。等的原因是服务一次只能处理一个请求没有做并发复用。加卡虽然能缓解排队但4张A100才能达到原本1张卡应该在合理调度下就能达到的水平。5.3 三轮改动的具体动作和实测结果第一轮改动切换推理框架为vLLM启用动态Batchingmax_num_seqs设为32gpu_memory_utilization设为0.8。没有改模型、没有改业务代码。结果吞吐从每秒不到8个请求提升到22个请求P99降到1.4秒。GPU利用率上升到45%。收益明显但还没有达到理想状态。原因是max_num_seqs设置偏保守且部分Prompt的长度差异极大导致Batch内计算不均衡。第二轮改动把max_num_seqs提升到64打开enable_prefix_caching并把max_model_len从默认的8192精确设为业务实际需要的4096。结果吞吐进一步提升到每秒30个请求P99降到900ms。显存占用控制在78%左右。这里的关键收益来自前缀缓存——对话场景中System Prompt占输入很大比例前缀缓存直接跳过了一部分prefill计算。第三轮改动接入优先级队列把“实时对话请求”和“后台批量生成请求”分开。后台任务只在GPU负载低于40%时启动。结果高峰期实时请求的P99稳定在800ms左右后台任务吞吐也没有降低。最终没有加卡单张A100扛下了原来预计需要3张卡才能扛住的量。5.4 最终落地的配置参考这里给出最终部署的参考配置不同业务需要微调python -m vllm.entrypoints.openai.api_server \ --model /model_path/7b \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.8 \ --max-num-seqs 64 \ --max-model-len 4096 \ --enable-prefix-caching \ --swap-space 4同时在前端增加一个简单的请求通道分流模块根据请求类型打上优先级标签后台任务与大流量请求隔离。监控上重点盯P99延迟、Tensor Core利用率、KV Cache使用量三个指标任何一个指标异常都立即告警。5.5 这次实战最有价值的三个判断复盘这次优化我觉得最有价值的判断有三条“加卡”是所有手段里代价最高、收益最低的先确认GPU是否真的有负载再谈扩容。动态Batching不是调一个参数就完事而是要结合模型长度分布、并发模型数、业务时延要求反复校准。优先级调度的收益容易被低估。它不是锦上添花在混合负载场景下是刚需。6. 生产环境里的调度坑我替你踩过不少最后这部分是踩坑集合。很多坑不是第一天爆发而是运行一段时间后才出现排查起来特别费劲。我把值得重点说的问题列在这里。6.1 动态Batching不是万能药连续批处理对常规输入效果很好但在两类场景下会翻车一类是超长文本生成单个请求的序列长度远超Batch内其他请求把一个Batch的算力资源全拖住另一类是prefill计算量占比极高的请求比如长Prompt的RAG问答它的prefill可能要几百毫秒decode反而很快。遇到这两类情况我建议在框架层面专门配置“长请求分流”或“独立模型副本”。比如一个模型实例专门处理长文本另一个处理常规文本避免互相干扰。这种“按需拆实例”的做法比在同一个实例里强行混跑要稳定得多。6.2 显存碎片和长尾OOM长驻服务跑几天后显存碎片化会越来越严重。显存总量还有剩余但连续的显存块不足新的请求申请不到足够空间直接OOM。这个问题在频繁创建销毁请求的服务里特别常见。缓解手段有三个限制并发请求数防止并发峰值过高定期通过torch.cuda.empty_cache()或框架的GC机制释放缓存如果条件允许在主业务低峰期定时重启模型实例。最后一种方法看似粗暴但在生产环境里是最有效的手段之一。6.3 副本数量不是越多越好很多人做弹性伸缩时只看QPSQPS高了就加副本但忽略了显存和算力之间的平衡。如果每增加一个副本只增加了显存占用但算力已经饱和加副本不仅不会提升吞吐反而会把GPU的显存池挤爆导致所有副本一起变慢。我这里有一个简单的判断公式当单卡算力利用率已经超过85%而显存利用率不到70%时增加副本数量是有用的当显存利用率超过90%而算力利用率低于40%时说明显存不够用但算力闲着应该做显存优化而不是加副本。6.4 监控告警怎么设才不会被报警淹没监控告警的阈值设计是门学问。我的建议是不要只盯着“GPU利用率90%”这种单一指标因为它不能反映真实瓶颈。更好的组合是P99延迟超过目标值持续2分钟触发生成告警Tensor Core利用率超过85%持续5分钟触发扩容建议KV Cache使用率超过85%持续3分钟触发显存告警OOM事件一旦发生立即告警并记录现场日志其中KV Cache使用率是最容易忽略但最该监控的指标。它直接决定了服务还能不能接受新请求一超过阈值排队和超时就会同步爆发。6.5 最后一点体会回过头看推理GPU资源调度这件事不是一次性的项目更像是一个持续迭代的运维课题。业务请求的分布形态一变原先的参数组合就可能不再适用。我现在的习惯是每次发布新模型版本后重新跑一遍基线和关键实验而不是把老参数一直沿用下去。调度方案有没有效果最终要看业务延迟和系统吞吐而不是看某个单一指标有多好看。
返回列表