
大模型的推理服务跑到一半GPU算力看起来没吃满但用户反馈首字延迟很高吞吐也上不去。这种问题如果只盯着某一个算子去调往往找不到真正的痛点。这篇文章整理的就是我最近用 Ascend Profiler 对 vLLM 在昇腾硬件上的完整推理链路做端到端性能拆解的一次实践记录。从请求进入队列开始到调度、显存分配、算子下发再到设备侧执行每一步都拉出来看数据最终定位到瓶颈做了几项针对性优化。如果你也在用 vLLM 部署大模型尤其是跑在昇腾环境上或者手里有类似的推理性能问题不知道怎么下手这篇文章应该能给你一套可以复用的排查思路。工具层面的具体操作我会写细但更重要的是背后的分析方法什么时候该看 Host 侧什么时候该盯 Device 侧拿到一个 profile 文件后第一眼该看什么。1. 项目背景与整体设计思路1.1 为什么端到端视角比单算子分析更重要很多人在做推理性能优化时习惯抓几个耗时算子的数据就开始调。比如发现 MatMul 占比高就去找融合算子的替代方案发现 RMSNorm 耗时大就去调小算子融合路径。这种打法在单模型单场景下有一定作用但在 vLLM 这种带有调度器、显存管理器、连续批处理引擎的复杂系统里很容易掉进局部最优的坑。vLLM 的推理链路由多个环节串联而成HTTP 请求接入、tokenizer、调度器决定哪些请求进入本轮 step、显存分配器为 KV Cache 腾挪空间、若干个 decode step 循环执行、采样后返回结果。任何一个环节出现阻塞都会拉长端到端延迟但单算子 profiling 只会告诉你某个算子跑了多久不会告诉你为什么前一个 step 结束后 GPU 空等了几十微秒也不会告诉你为什么明明有显存余量但调度器只放行了少量请求。这就是端到端视角的价值所在。用 Ascend Profiler 的完整链路打点把 Host 与 Device 的时间线对齐每一段空闲、每一次同步等待、每一个算子的执行区间都在时间轴上暴露出来瓶颈在哪一目了然。1.2 本次实践的软硬件环境与基线配置先交代一下这次实践的部署环境架设方便大家对照自己的场景做调整。硬件方面用的是 Atlas 800T A2 训练服务器实际用到的是 2 张昇腾 910B 芯片。软件栈为 CANN 8.0 配套版本、Python 3.10、PyTorch 2.1vLLM 侧使用的是昇腾适配的 vllm-ascend 插件分支。部署模型选择的是 Qwen2.5-14B-Instruct权重为 BF16 格式实际推理精度足够没有必要上 FP8 或 INT8。由于 14B 模型的权重加上一层 KV Cache 的占用控制单卡显存大约 64GB 的场景下使用张量并行切到两卡每卡约 32GB留给 KV Cache 的余量还算宽裕。服务启动参数大致是这样vllm serve Qwen2.5-14B-Instruct \ --tensor-parallel-size 2 \ --dtype bfloat16 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 64 \ --block-size 16 \ --enable-chunked-prefill压测时使用固定请求脚本模拟在线推理场景输入文本长度控制在 512 tokens 附近输出设置为 256 tokens并发请求数为 16。首字延迟TTFT基线约 780ms单请求端到端延迟约 1.9s整体吞吐大约 1350 tokens/s。这个基线数据并不算差但我通过 Profiler 发现其中存在大量等待浪费优化空间非常可观。2. Ascend Profiler 的采集与准备2.1 Profiler 工具链与两种主流采集方式在昇腾环境上做性能分析有两套思路可以走。第一套是使用 CANN 自带的 msprof 工具直接对整个进程打点采集算子耗时、AI Core 利用率、HBM 带宽数据。第二套是在 PyTorch 层通过 torch_npu 接入 Profiler 接口得到与 PyTorch 事件对齐的时间线数据。两套工具并不冲突我的习惯是先用 msprof 做全局扫描再用 torch_npu Profiler 锁定具体某一轮迭代内部的细节。这里给一个 torch_npu Profiler 的采集示例基于常见实践补充具体接口和版本请以你使用的 CANN 版本为准import torch import torch_npu from torch_npu.contrib import transfer_to_npu # 通过 context manager 方式打点 with torch.npu.profile(profile_path/root/profiler_output) as prof: # 在这里执行若干次推理循环 for _ in range(10): outputs model.generate( input_ids, max_new_tokens256, use_cacheTrue )采集完成后输出目录里会有一堆 JSON 和 protobuf 格式的文件时间线数据可以加载到 Perfetto 或 Chrome Tracing 工具里查看。msprof 的命令行方式更简单直接在启动 vLLM 服务时附加采集即可msprof --application./start_vllm_server.sh \ --output/root/msprof_output \ --ai-coreon \ --runtime-apion \ --task-timeon这种方式适合做全进程范围的采样同时也方便采集启动阶段初始化过程。需要注意带上 runtime-api 采集后对耗时会有轻微放大影响建议正式压测前先跑一次小流量对比开启与关闭采集下的耗时差异确认影响在可接受范围内。2.2 关键性能指标与视图怎么读拿到 profile 文件之后新手很容易一头扎进算子耗时列表里把 Sum of Duration 排一下就开始看自己最眼熟的算子。但实际上第一步应该是建立整体视图我习惯按下面这几个维度逐层去看。第一是看时间线概览图确认每个推理 step 内 Host 侧与 Device 侧的执行区间。将鼠标停在整个 step 的空隙上能看到Device 空转的区间。如果发现 Device 空闲时间段占比明显偏高说明问题大概率在 Host 侧准备与下发环节。第二是看 AI Core 利用率曲线。当 AI Core 利用率长时间处于较低水平但 HBM 带宽指标却跑得很高这是典型的访存密集场景瓶颈不在算力而在数据搬运。如果 AI Core 利用率和带宽同时都很低多半是任务下发不够连续调度或同步受阻。第三是看通信耗时。多卡场景下张量并行引入集合通信。若遇到 allreduce 或 nccl对应昇腾是 HCCL耗时占比偏高需要区分到底是通信算法效率差还是数据切分不合理导致的通信量偏大。第四是看采样中每个算子的 Wait 状态。在昇腾的 profiling 输出里一个算子从 Host 下发到 Device 执行之间通常会有时间差。大量算子的 start 时间相近但执行时间稀疏说明 Host 下发能力不足算子排队等待严重。反之如果算子排队很整齐但每个算子内部耗时过大则说明瓶颈在算子实现自身。3. 端到端瓶颈定位的实操过程3.1 第一步先看整体链路后看算子面对复杂系统我建议先抓大方向再有针对性地深入细节。拿到第一份 profile 后我先不着急看算子细节而是把压测中采集到的时间线按阶段划分请求排队阶段、Prefill 阶段、Decode 循环阶段、采样返回阶段。逐个统计耗时占比。从时间线统计结果来看当前 780ms 的 TTFT 里Prefill 本身只占大约 300ms剩下的 480ms 分布在调度等待、tokenizer、KV Cache 分配等环节。这组数据本身就给出了明确信号优先优化掉等待中的时间浪费比试图压缩 Prefill 算子耗时更有价值。Decode 阶段的瓶颈同样呈现类似规律。每个 step 接近 90ms但其中算子的纯执行时间只占了一半剩余时间被内存拷贝、算子下发等待和同步占据了。也就是说即便把所有算子的执行时间优化到零端到端延迟也只能缩短大约一半。优化操作的优先级排序就十分清晰了先解决等待浪费再优化执行阶段。3.2 第二步从 Host 侧找到下发阻塞点排查 Host 侧下发阻塞我会先看 Runtime API 的时间线标志位。把 Ascend Profiler 采集到的 runtime-api 数据加载到时间线视图后重点观察两类事件同步等待事件和内存拷贝事件。在一个典型 decode step 中大量小算子需要逐个下发到 Device。vLLM 在昇腾上的执行路径里每次 forward 调用都会触发一些同步等待。观测数据里出现了大量 NPU 上的空闲时间间隙每段间隙大约 30 到 80 微秒这部分时间消耗在等待前一个算子完成的同步上。虽然单次间隙不大但在 decode 循环里一个 step 要执行几十个算子累加后的损耗非常可观。另外还发现了一处比较隐蔽的 Host 侧问题。PagedAttention 的实现流程里每个序列的 KV Cache 地址需要先由 Host 整理成地址列表再传给 Device 上的 kernel。如果序列很多这个地址整理操作会消耗较长 CPU 时间导致 Device 侧已经完成上一个 step下一轮的 kernel 却迟迟没有下发。这就是典型的 Host 侧准备成为瓶颈的情况。对这些间隙做数据统计后发现在 16 并发请求下一个 decode step 中 Device 实际空闲等待时间约占总耗时的 32%。如果能把这部分等待时间压缩掉一半吞吐提升潜力非常可观。3.3 第三步锁死设备侧的计算与访存瓶颈Host 侧问题梳理清楚后再把视角切到 Device 侧。通过 Profiler 的 AI Core 视图观察 Decode 阶段的算子执行特征发现两个典型现象第一Decode 阶段的 MatMul 算子执行效率偏低。小 batch 下 GEMM 的 AI Core 利用率只有不到 30%大量的计算单元处于空闲状态。第二Attention 相关算子的 HBM 带宽利用率接近上限。KV Cache 的大小与序列长度、batch 数直接相关。在多并发场景下需要读取的 KV Cache 数据量很大而单个 token 的 query 只需要很少的计算量整个算子被数据搬运卡住。将 AI Core 利用率和 HBM 带宽两个指标关联起来看问题就很明显了这个模型在 Decode 阶段并不是算力不够而是数据访问效率拖了后腿。对于这种典型访存瓶颈就算把算子的计算部分改成更高效的指令序列收益也有限。真正有效的手段是减少数据搬运量更充分地利用数据和缓存结构。4. 定位到的瓶颈与优化手段落地4.1 瓶颈一PagedAttention 的 KV Cache 访问效率sorted by code我的优化从 PagedAttention 的显存布局开始。vLLM 默认将 KV Cache 划分成固定大小的 block每个 block 存储一定数量 token 的 key 和 value。通过 Profiler 的数据发现在 block_size 设置为默认值的时候Attention 算子对 HBM 的读取量中有效利用的比例并不高大量带宽消耗在读取无用或重复的 KV 数据上。我搭建了几组对比实验将 block_size 分别设为 8、16、32观察 Attention 算子的带宽利用率和端到端延迟变化。实验发现在当前 512 输入、256 输出的场景下block_size 16 时 Attention 算子的带宽效率最优继续增大到 32 后反而因为单 block 内数据量变大导致 cache 命中率下降、带宽压力回升。调整 block_size 之外也确认了开启 chunked-prefill 对减少 KV Cache 分片碎片有明显帮助。分块预填充将长 prompt 按块切分避免一次性占用大量连续 block也让 prefill 与 decode 能更好地在同一个 step 中混跑整体调度更饱满。4.2 瓶颈二Host 侧同步等待与地址整理开销针对 Host 侧空闲时间过长的问题我的优化思路是减少同步等待和压缩 CPU 端准备工作的耗时。vLLM 的昇腾适配环境中同步等待的一部分来自 torch_npu 的算子下发方式这部分不容易直接改变。但我可以改变算子的执行顺序尽量把需要依赖前一个算子结果的 CPU 端操作延后或合并减少 Host 等待的频次。第二个方向是优化 KV Cache 地址整理。通过 vLLM 提供的调度接口把 KV Cache 的 block 表维护逻辑调整成批处理方式一次性为多个请求批量构造地址列表再统一传给 kernel。这个改动不涉及模型结构只改造了调度准备阶段的实现方式代码量不大但效果很明显。实测下来 Host 侧的等待间隙从平均 50 微秒降到 20 微秒左右同时多请求并发下的整体吞吐明显提升。4.3 瓶颈三连续批处理的调度波动观察 Decode step 的序列耗时分布曲线时还发现一个规律某些 step 的执行时间明显比相邻 step 长。排查这些异常 step 后发现问题出在调度器处理新请求进入批次的节点。当一个新请求需要执行 Prefill 计算时如果调度策略不得当会拖慢整个 step 的完成速度。vLLM 的 continuous batching 机制天然支持 Prefill 与 Decode 混跑但调度器选择混跑的策略对实际效果影响颇大。如果每轮 step 都允许未完成的 Prefill 与 Decode 任务混排会使得部分 step 的负载量波动剧烈导致整体延迟不稳定。我调整了最大并发序列数设置将 max_num_seqs 从 64 压缩到 48减少单 step 内的负载波动。同时打开了 vLLM 的 preemption 重计算模式设置让显存压力过大时优先重算 Prefill 而不是直接驱逐请求避免请求端到端超时。4.4 优化后效果对比与复测方法全部优化落地后我重新跑了一次完整的压测验证。稳定并发 16 请求、输入 512 tokens、输出 256 tokens 的固定场景下TTFT 从 780ms 下降到了 610ms单请求端到端延迟从 1.9s 降到 1.5s整体吞吐从 1350 tokens/s 提升到 1820 tokens/s。这个提升幅度在纯软件调优方案里已经相当可观。复测时特别要注意一个操作习惯修改任何配置后必须清空 KV Cache 并重启服务再执行足够次数的预热请求。性能数据要在预热 50 个请求之后再开始统计否则显存池尚未填满、block 分配策略尚未稳定数据会明显失真。我这边踩过这个坑第一次优化后测试数据非常漂亮但直接切换到生产流量后效果打了不少折扣。5. 常见问题与排查技巧实录5.1 常见问题速查表下面把这次实践过程中遇到的几类典型问题整理成速查表方便后续排查时快速定位方向。现象可能原因排查路径优化方向TTFT 偏高但 Prefill 算子耗时不高调度等待、KV Cache 分配环节耗时过长看时间线中 Prefill 前各阶段耗时占比开启 chunked-prefill调整调度策略Decode step 时间曲线抖动明显新请求 Prefill 混入导致 step 负载波动对比异常 step 与正常 step 的请求数压缩 max_num_seqs调整 preemption 策略AI Core 利用率低但 HBM 带宽接近上限访存密集场景算力冗余关联 AI Core 利用率与 HBM 带宽指标优化 KV Cache 布局调整 block_sizeDevice 空闲间隙多Host 侧同步等待或地址整理耗时偏高观察 runtime-api 时间线中算子间空隙减少 CPU 端同步批量构造地址列表多卡场景吞吐不随卡数线性提升集合通信耗时占比高统计 HCCL 通信耗时在 step 中的占比调整 tensor parallel 配置增大单卡 batch5.2 独家避坑技巧再分享几个实际操作中不太容易从文档里获得的小经验。第一Ascend Profiler 采集时不要全程开着 runtime-api 选项。这个选项记录所有算子下发的 Host 侧信息日志量巨大同时会造成明显的性能损失。我习惯先跑一次完整采集拿到整体概貌后续针对怀疑的环节再开详细采集。第二分析时间线时最好把 DevMalloc 事件也标注出来。在 vLLM 的运行过程中显存分配事件如果出现在推理循环内部而不是预处理阶段说明显存池配置或 preemption 策略有问题。显存的反复申请释放往往比算子执行更消耗时间。第三多模型部署场景下的 profiling 需要按模型分别采集。如果你的环境像热词里提到的那样需要同时部署多个模型那么建议把每个模型的推理请求分流到不同的端口再分别用 Profiler 采集。多个模型混跑同一个进程时调度器的时间和空间占用会叠加对单个模型的性能分析结果会产生干扰。第四纯 CPU 模式与昇腾 NPU 模式的 profiling 分析方法完全不同。如果在纯 CPU 场景下调优关注核心数是主要的瓶颈点但 NPU 场景下核心关注点变成了 host 与 device 的协同效率。不要沿用 CPU 推理时代靠统计纯算子耗时来定位问题的习惯需要把时间线的空隙也当成一种可优化的资源来看待。6. 工具选型解析与实践建议6.1 为什么选择 Ascend Profiler 而非其他工具在昇腾平台上调试 vLLM很多人会问能不能直接用 PyTorch Profiler 或者 vLLM 自带的 profiling 接口。结论是可以最好搭配使用。PyTorch Profiler 对算子级别的视图更友好而 Ascend Profiler 针对昇腾底层硬件的指标采集更精细。真正遇到 AI Core 利用率、HBM 带宽、HCCL 通信耗时这些硬件层面的问题时PyTorch Profiler 给不了足够信息。和基于 CUPTI 生态的 Nsight 工具相比Ascend Profiler 的界面和交互确实朴素一些但它的 AI Core 视图、HBM 带宽视图、Device 同步等待视图能更直接地对应到昇腾的硬件特征。特别是算子等待状态的统计在昇腾上排查 host-device 同步问题非常有用。6.2 什么样的场景适合照搬这套方案很多朋友来问这套端到端 profiling 方案是不是只能用于昇腾环境。实际上分析方法论完全通用。即便是 NVIDIA 环境下同样的流程也成立先花 20 分钟看时间线概貌确认瓶颈在 Host 侧还是 Device 侧再用 CMU 类似指标判断是算力问题还是带宽问题最后做具体优化。只是工具不同对应指标名称和视图区域有变化。如果你使用的是 LM Studio 这类发工具或 bionic 版本环境本文的端到端思维同样适用但 profiling 粒度会粗很多。这些工具通常只提供整体延迟和 token 速度统计无法定位到单算子级别。建议在开发调试阶段使用 vLLM 配合 Ascend Profiler 或 NVIDIA Nsight 做全链路打点找到瓶颈后再把最终配置迁移到目标环境中。个人体会是推理性能优化能力的提升靠的不是读多少文档而是多翻 profile 文件。第一次看到一堆时间线时会觉得眼花缭乱不要着急先把 Host 侧、Device 侧的执行区间标出来再把空闲区间的百分比算出来顺着这些数据往下追你会发现自己很快就能从数据里读出故事来。最后再分享一个小技巧做完一次优化后保留原始 profile 文件和优化后的文件后续出现性能回退时对比两份文件能极大缩短排查时间。