NPU上跑vLLM:部署、调试与性能优化实战 跑大模型的人基本都绕不开 vLLM 这个开源推理框架。但大多数教程默认建立在 CUDA 生态上一旦换了硬件平台很多经验直接作废。社区里有个面向国产 NPU 的适配分支大家通常叫它 vLLM-Ascend目标是把 vLLM 的 PagedAttention、continuous batching 这些核心机制搬到 NPC 平台让同一套 API 和部署方式继续工作。这篇文章只讲三件事在 NPU 上怎么跑起来、出问题怎么查、性能怎么压。不管你是刚把模型从 GPU 上搬过来的新手还是已经跑通但想再榨性能的团队都可以按自己的进度看。1. 先弄清 NPU 上 vLLM 到底动了什么1.1 vLLM 的核心机制PagedAttention 与 continuous batchingvLLM 的效率核心是 continuous batching。传统推理框架会等一个批次的所有请求结束后才换下一批中间只要有请求完成计算资源就会空转。vLLM 则不一样它在每一步解码后立刻腾出完成请求的位置把新请求直接塞进当前批次加速卡的利用率自然就上去了。PagedAttention 是另一个关键设计。它把 KV 缓存拆成固定大小的物理块逻辑上每个请求是连续的物理上却可以分散存放在很多不连续块里类似操作系统内存分页。这种机制能大幅降低显存碎片把有限的加速卡内存压榨得更干净。这两点都依赖底层加速器提供两个能力一是能高效访问散落在各处的内存块二是能在计算过程中动态修改显存分配。CUDA 生态里这些都顺手国产 NPU 的指令体系、内存申请方式都不一样所以不能直接把代码平移过来必须在算子库和适配层重新实现。vLLM-Ascend 做的事情就是保留上层调度逻辑把底层访存、分块分配、算子实现全部换成 NPU 版本。1.2 NPU 与 CUDA 的差异是踩坑的源头实际开发排障时最容易翻车的一点是假设两边完全一样。我在迁移过程中至少感受到了三个层面的差异。工具链不同。NPU 有自己的软件栈包括算子库、深度学习加速库、编译工具链而 vLLM 默认只认一套标准算子接口。适配分支需要在算子层做很多封装把原生 CUDA 算子的调用映射到 NPU 算子实现上。这个映射并不总是百分百等价偶尔会遇到某个算子在 NPU 上没有对应实现的情况。内存模型不同。CUDA 环境有一套相对透明的显存地址空间和统一寻址写起来比较直接NPU 上经常要把数据切到不同的内存区域有的区域适合大块搬运有的区域适合算子直接读取。KV cache 的物理块怎么摆放、要不要对齐直接决定吞吐表现。我见过有人把 GPU 上的--block-size原样搬到 NPU结果因为内存对齐问题多出不少拷贝性能反而掉。精度和溢出表现不同。NPU 上某些算子中间结果的溢出判断和 CUDA 不完全一致求和顺序变化也会带来微小误差。这不是 Bug是浮点数计算本身的特性但会让你在调试的时候困惑很久。1.3 定位你要拿 vLLM-Ascend 解决什么问题在动手之前先想清楚你的场景属于哪类。是内部工具快速出结果还是对外服务要求稳定延迟还是离线批量跑大量 prompt不同的目标决定了你后面怎么调参数。如果是内部工具用默认参数把模型跑起来就行。如果是对外服务那就要关心 TTFT 和 ITL也就是首 token 延迟和 token 间延迟。如果是离线批处理就要把吞吐量放在第一位宁可牺牲单请求延迟也要把设备打满。我习惯把这个问题写在需求文档第一行后面调试卡住的时候回头看看这个目标能省下很多瞎折腾的时间。2. 环境准备与版本匹配比想象中更重要2.1 先把依赖关系理清NPU 适配版本的环境安装最忌直接照搬 GitHub 上的命令。CUDA 生态基本是驱动加 CUDA Toolkit 加 PyTorch 就完事NPU 这边多了一层厂商软件栈而且每一层之间都有严格的版本匹配关系。我推荐按固定顺序安装不要跳步安装底层驱动和固件这是所有软件运行的前提。安装厂商提供的加速软件栈通常包含算子库、运行时、编译工具三件套。创建干净的 Python 虚拟环境避免系统环境被污染。安装与加速软件栈版本匹配的 PyTorch 和 torch_npu。最后安装 vLLM 的 NPU 适配分支。最后一步放在最后是有原因的。如果先装 vLLM再装底层依赖你很可能在导入阶段莫名报错到时候很难判断是哪个库冲突了。推荐做法注意事项驱动/固件按厂商文档刷到稳定版本不要乱追最新固件容易引入兼容问题加速软件栈用与驱动配套的厂商版本版本号前后两位要对齐小版本差异也会报错Python 环境独立 venv 或 conda 环境不要用系统 Python 直接干活torch_npu与 PyTorch 版本严格配对装错版本会在导入时报非法指令vLLM 适配分支用与 torch_npu 发布周期接近的版本新版本的 vLLM 接口改动大适配可能滞后2.2 环境变量和冒烟测试安装完成后先做一次五分钟冒烟测试别急着启动大模型。npu-smi info这条命令用于确认加速卡能被系统识别类似 NVIDIA 环境下的 nvidia-smi。正常能看到设备型号、使用率、温度等信息。然后是基础 Python 接口验证python - EOF import torch import torch_npu x torch.randn(4, 4).npu() print(x) EOF能打印出张量说明 PyTorch 到 NPU 的链路是通的。再执行下面这条确认 vLLM 的命令行入口可用python -m vllm.entrypoints.openai.api_server --help看到帮助信息就算过了第一关。我见过不少人在这一步卡住报错提示找不到算子库。这种问题九成是环境变量问题你在当前 shell 里没有 source 厂商软件栈的环境变量或者虚拟环境路径不对。建议把环境变量导出写进~/.bashrc或者启动脚本每次开新终端省得重复设置。2.3 常见环境类报错速查报错现场原因处理办法导入 torch_npu 时进程直接崩溃PyTorch 和 torch_npu 版本不匹配卸载重装匹配版本启动时提示找不到 so 文件厂商软件栈路径未生效重新 source 环境变量报设备无法初始化驱动与固件不匹配按厂商官方组合重刷驱动出现非法指令或段错误编译器版本与 torch 不兼容换 GCC 版本或重建虚拟环境这些是第一天最容易遇到的坎。跨过去之后才能真正开始搞模型服务。3. 在 NPU 上跑通一个推理服务3.1 准备一个测试权重建议第一轮先用小模型跑通全流程我一般用 1B 到 7B 级别的开源对话权重做验证。模型可以从公开模型仓库下载也可以直接选一个你之前已经在 CUDA 上验证过的权重这样后续对比性能时控制变量更方便。拿 7B 模型举例。权重下载好后目录结构大概是这样的model_repo/ ├── config.json ├── model.safetensors.index.json ├── model-00001-of-00003.safetensors ├── ... └── tokenizer.jsonvLLM 会读取这个目录作为--model参数的值。你不需要转换格式官方适配分支支持直接加载这些权重。3.2 启动参数怎么定下面是我在 NPU 上验证过的一组可复现配置单卡 32GB 显存、7B 模型、上下文长度 8192export ASCEND_VISIBLE_DEVICES0 python -m vllm.entrypoints.openai.api_server \ --model ./model_repo \ --dtype bfloat16 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 64 \ --port 8000逐个解释关键参数--dtype bfloat16是在 NPU 上比较稳的精度选择兼顾速度和显存占用。--max-model-len 8192限制上下文长度。如果设太大KV cache 会占满显存导致可用 batch 变小。--gpu-memory-utilization 0.9表示允许使用最多 90% 的加速卡内存。剩下的 10% 留给权重加载、激活值和碎片空间。--max-num-seqs 64控制单个批次最多处理多少请求。设太大会显存溢出设太小吞吐上不去。启动日志里如果出现Starting vLLM API server基本就成功了。然后发一个请求验证链路curl http://127.0.0.1:8000/v1/completions \ -H Content-Type: application/json \ -d {model:/your/model_repo, prompt:你好请用三句话介绍你自己, max_tokens:512}能正常返回 token说明推理链路没有问题。3.3 服务化与脚本化怎么选vLLM 支持两种调用方式在 Python 脚本里用LLM类直接推理或者启动 API server 走 HTTP 接口。我建议业务线上直接走 API server。它自带连续批处理、请求排队、流式输出和内置指标这些能力自己重新实现一遍成本太高。脚本模式更适合做单元测试和离线批量评测。如果你要做性能压测可以在脚本模式下手动控制请求列表也可以直接对着 API server 发并发请求。两种方式各有用途我一般是先脚本模式验证效果再 API server 模式做并发验证。4. 调试方法日志、报错与数值问题4.1 先看日志再猜原因NPU 侧的日志通过环境变量控制。最常用的调试级别是ASCEND_GLOBAL_LOG_LEVEL3能看到算子调度和内存申请细节。vLLM 侧可以开--verbose增加运行日志输出。遇到异常退出我习惯同时开两个终端一个看 vLLM 业务日志一个看 NPU 运行日志。日志级别不要一上来就开最细的 debug。在 7B 模型上全量 debug 日志一天能产生几十 GB 文件磁盘很快写满。正确做法是先默认级别跑一遍确认稳定后再针对具体任务调高。4.2 高频报错解析我整理了一张速查表基本覆盖在 NPU 上调试 vLLM 遇到的高频问题错误现象可能原因排查与解决算子未实现提示 not implemented for NPU适配分支未覆盖该算子简化模型结构或升级适配分支版本shape mismatch维度对不上NPU 算子的维度顺序要求不同显式做 permute/transpose 后重试显存分配失败out of memoryKV cache 占用过高调低--gpu-memory-utilization或--max-num-seqs非法指令直接崩溃编译环境不匹配检查依赖版本重建虚拟环境输出 token 全是重复或乱码数值溢出或精度问题换 bfloat16 为 float16或关掉贪心采样测试进程挂起无响应通信库初始化失败或超时检查设备间通信配置和链路状态遇到报错别急着搜错误串先确认错误出现在哪个阶段是模型加载、前缀缓存分配还是具体某一步算子的执行。错误信息中一般带有阶段线索把日志往上翻十行左右经常能看到前置原因。4.3 数值问题的三板斧最难查的一类问题是不报错但输出结果不对。这类情况在 NPU 上我曾经遇到过一次排查用了两天最后发现是某个算子对特殊值的处理逻辑和 CUDA 版本不一致。三板斧排查法第一板斧固定输入。用一个固定的 prompt、固定的随机种子反复跑三次确认问题是否可复现。不可复现的问题多半和内存踩踏或异步执行有关。第二板斧对比输出。把同一个模型分别在 CPU 和 NPU 上跑一次逐 token 对比 logits。误差在精度范围内就不用管如果某一层开始差异突然变大问题就锁定在这一层附近的算子。第三板斧加入同步和中间打印。NPU 上执行是异步的报错可能延迟发生。在可疑位置插入同步调用然后打印中间结果张量的均值、方差和绝对值最大值。数值异常的分布通常能给出直观线索。4.4 调试时的性能损耗控制调试和性能优化天然有冲突。我的建议是默认定级跑线上发现问题后单独起一个调试实例日志重定向到文件不跟正式实例混在一起。另外不要把调试日志等级写在启动脚本里固定住用环境变量动态控制这样线上出现问题也能临时开细粒度日志。5. 性能优化把吞吐和延迟压到极致5.1 先测基线别凭感觉调参性能调优的第一原则是量化。不要一上来就改参数先固定一组测试数据记录优化前的三项指标吞吐量生成 token 数/秒、TTFT首 token 延迟、ITL每 token 生成间隔。我用公开的测试集构造三组不同输入短问题平均 50 token、中等输入256 token、长输入1024 token每组混入不同的输出长度要求。测出来的数据能反映真实业务的复杂情况。以下是记录指标时重点关注的内容单请求吞吐评价模型在 NPU 上的执行效率。多请求并发吞吐评价框架调度和连续批处理能力。不同输入长度下的 TTFT评价 prefill 阶段是否成为瓶颈。长尾请求的完成时间评价系统在压力下的稳定性。调试的过程中每改一个参数至少跑三轮测试取中位数避免抖动误导判断。5.2 关键参数如何影响性能参数作用NPU 上调优经验--max-num-seqs控制并发序列数逐步增大直到显存利用率接近上限且延迟仍在容忍范围--max-num-batched-tokens控制单步总 token 数减小可以降延迟增大可以提吞吐--block-sizeKV cache 物理块大小默认值通常够用改大改小都要测试不同硬件适配度不同--max-model-len决定 KV cache 总量设过小影响长输入设过大会挤占 batch 容量--enable-chunked-prefill切分长 prompt 的 prefill长输入场景建议开启能减少 prefill 阻塞 decode一个常见误区是盲目调大并发数。并发数变大后显存占用上升KV cache 会先被占满后续请求只能排队。排队时间过长会拉高整体延迟。我见过有人把--max-num-seqs从 64 调到 128结果吞吐没提升平均延迟反而翻了倍。5.3 开启图模式调度开销降下来在 CUDA 生态里很多人习惯用 CUDA Graph 把一系列小算子变成一个捕获图减少内核启动开销。NPU 上也有类似的机制适配分支会提供对应的开关。图模式的原理是把固定 shape 的计算流程预先编译和捕获运行时直接来回放。图模式的收益在小 batch、延迟敏感场景下非常明显。我曾经在一个 7B 模型服务上实测开启图模式后单请求 TTFT 下降约三成代价是首次启动时捕获时间变长并且请求 shape 变化频繁时效果会打折。[!NOTE]图模式不是万能的。如果你的业务是长 prompt 高并发图模式的收益会被 prefill 阶段的大量计算稀释。我建议先测基线再决定是否开启。5.4 量化精度换吞吐是否值得量化是在有限显存上增大吞吐最直接的手段。NPU 上有不少算子已经做了 INT8 支持但表现没有 CUDA 生态那么统一。如果模型权重能接受一定精度损失用高压缩率量化通常能显著增加 KV cache 的余量。我建议的流程是在原有权重上跑一遍标准评测集记录准确率。量化后再跑一遍同样数据。对比结果确保下降幅度在业务可接受范围。做一次完整压测确认吞吐提升效果。你还会遇到量化算子不支持的情况。这种时候不要硬上先看算子清单或者考虑给关键模块保留 bfloat16其余层量化。5.5 用 profiling 定位真正的瓶颈性能不上来的时候不要只盯参数要学会看硬件状态。NPU 有对应的性能分析工具能输出算子级别的耗时、读写带宽、指令吞吐数据。没有图形界面的时候就用命令行工具定时采样设备利用率、内存占用、温度和功耗。我看到过几种典型情况加速卡利用率高、显存占用高、吞吐未达预期一般是算子本身效率问题需要看性能剖析结果定位到耗时最大的算子。加速卡利用率低、内存带宽高说明数据搬运占了大多数时间考虑算子融合或减少反复拷贝。单个请求吞吐可以并发后掉得厉害多半是调度或锁竞争问题优先检查上下文切换和通信逻辑。profiling 数据一定是在优化后才开不要在调参过程中频繁采集否则环境被干扰数据没有参考价值。5.6 一个调优案例的思路我在自己负责的某内部问答服务上做过一次完整优化。初始状态是 7B 模型、单卡、默认参数压测 32 并发时 TTFT 偏高显存使用率始终上不去。第一步开启图模式TTFT 降下来一截。第二步开启长输入切分长 prompt 请求不再阻塞短请求。第三步把--max-num-seqs从 32 逐步提升到 64同时观察显存利用率和延迟曲线最终停在 64 这个拐点。第四步把 KV cache 物理块大小从默认值 16 调整为适配分支推荐的 32内存分配碎片明显减少。最终吞吐提升了约 60%TTFT 保持在规定阈值内。6. 多卡扩展与生产环境避坑6.1 单机多卡先把并行维度分清楚服务量上去之后单卡终究不够用。多卡扩展前先分清楚三种并行方式。张量并行把每一层的权重切到多张卡上多个卡同时处理同一个请求适用单请求延迟要求高的场景。倒过来数据并行是每张卡独立跑一个完整模型通过前层负载均衡把请求分散开适合吞吐优先的场景。流水线并行则把不同层放到不同卡上延迟和吞吐居中但实现复杂度高。vLLM 的适配分支对张量并行的支持相对成熟一般通过--tensor-parallel-size参数控制。后两种并行在很多场景下可以借助外部负载均衡器实现不一定要在框架层做。6.2 多卡场景的坑很多与通信相关多卡推理最容易出的问题不是显存是通信初始化失败。这类报错通常在启动阶段出现检查顺序如下确认多张卡都被系统识别。确认通信库版本与驱动匹配。确认进程间通信的物理链路没有异常。多进程并发启动时确认是否共用环境变量目录。还有一个容易被忽略的问题是功耗和散热。多卡满载时机箱温度会迅速上升触发降频后性能掉得非常快。我建议给多卡设备配单独的散热监控基线测试要在稳定温度下完成不要刚开机就记录性能数据。6.3 生产化建议故障要可观测恢复要可预期模型服务上了生产环境就不只是调参的问题还要考虑稳定性。下面是我在实际运营中沉淀下来的配置清单服务启动脚本加上健康检查接口定期请求一次检测响应是否正常。请求接入层设置超时和重试策略避免后端积压后雪崩。监控三组指标硬件状态、框架内部指标、业务延迟指标。显存利用率、KV cache 使用率、排队请求数要打到监控系统超过阈值自动告警。做好模型热更新方案新权重在独立实例上验证后再切流量。vLLM 内置了一些统计指标可以直接接入监控系统。建议至少关注请求运行数、缓存使用率、排队数量和 token 吞吐变化。这些指标结合 hardware 监控基本能覆盖绝大部分运维场景。6.4 长上下文场景的额外处理长上下文是当前大模型应用的热门方向。上下文越长KV cache 占用越大显存压力越明显。一个简单经验KV cache 占用可以用公式估算实际在 NPU 上还需要考虑对齐开销所以留的余量要比 CUDA 环境更大。长上下文场景下我建议单独开一个容量验证用例用最长上下文的 prompt 连续压测确认不会在运行中段突然 OOM。如果你做不到就把--max-model-len调小一些稳定优先。6.5 集群化部署的取舍机器数量上来之后集群管理成为新的复杂度来源。很多团队以为用容器化平台能解决一切但 NPU 环境的容器化并不像 CUDA 那么顺手底层固件、驱动、加速软件栈都要透传进容器版本管理难度不小。我见过最稳妥的做法是先在一台物理机上验证所有功能再封装成统一镜像最后再谈集群调度。顺序反了会白白浪费排查时间。最后再分享一点个人经验从头到尾踩过一遍之后我最大的体会是vLLM 适配分支的价值已经不在跑通模型而在于它把 CUDA 生态下成熟的推理能力迁移到了另一套硬件上。这个过程中你牺牲的是一些惯性比如默认参数、工具习惯、调试思维得到的却是硬件选择的自由度。如果你现在正打算在 NPU 上搭建服务我建议先不要追求极限性能。花半天时间把环境、版本、冒烟测试这些基本功固化下来再谈优化。真实业务里稳定跑一个月不出问题比压测数据好看十个百分点重要得多。最后再分享一个小技巧把改过的参数都记录在文件里标明测试日期、硬件状态、请求规模、优化结果。参数优化这件事记忆是不可靠的只有历史记录能帮你复现结果。我自己就是凭着这些记录才能在无数次失败后找到稳定又高效的一档配置。