做了几年推理服务,被人问得最多的问题就是:你这个场景到底用GPU还是NPU?这个问题背后往往是预算有限、功耗受限、又要按期交付的普遍焦虑。我最初也死磕这个二选一,结果两次都踩了坑:一次是纯GPU方案被电费和散热账单教育了,另一次是纯NPU方案被算子和精度问题拖到临近上线才临时补救。后来我把思路反转,先问自己"为什么不能先看负载长什么样,再决定硬件阵容",慢慢才形成今天想分享的这套东西。这篇文章要聊的是GPU和NPU之外的第三个答案:按负载切片、异构协同、统一调度。它不是某个硬件,而是一套让不同算力引擎各跑一段的工程方法。适合正在做模型上线、推理服务优化,或者刚拿到一块NPU评估板准备做验证的同学参考。
1. 两个热门选项的真实底细
1.1 GPU:算力高光掩不住三笔隐形账单
GPU为什么这么火?因为它的架构天生适合大规模并行矩阵运算。核心数多、访存带宽大、软件生态成熟,主流框架基本都优先支持GPU。这三点加在一起,让它成为模型推理默认的"标准答案"。
但生产环境不是跑个验收报告。GPU有至少三笔账单,评测指标里看不见。第一笔是功耗与散热。高端加速卡的标称功耗动辄几百瓦,跑起来加上服务器整体散热,一个机柜的功率密度很快就到上限。有些机房条件差的团队,卡还没买先得解决供电和空调,这不是段子。第二笔是采购成本与折旧。GPU价格贵,而且迭代快,两三年就要考虑淘汰换新,折旧摊到每个请求上,小业务的成本压力很大。第三笔是利用率问题。推理服务的流量有明显波峰波谷,白天在线API高峰,夜里批量任务低迷,GPU很难始终满负荷。我见过不少团队,GPU平均利用率不到30%,但还是得按满配电力预留资源。
我不是说GPU不好,而是说如果只看峰值算力就下单,后续会有很多超出预算的"意外支出"。这也是很多人开始看NPU的契机:能不能用更低的功耗、更便宜的价格把同样的模型跑起来。NPU确实能,但它有另一套问题。
1.2 NPU:能效漂亮,但"好用"和"好用"是两回事
NPU的算力逻辑和GPU不一样。它把乘法累加阵列、片上缓存和低精度计算做成专用电路,跑特定类型的算子时,能效比相当夸张。同样一个INT8卷积,NPU的每瓦性能可能做到GPU的好几倍。对于边缘盒子、摄像头这类功耗受限的设备,NPU几乎是唯一可行解。
但问题也出在"特定"两个字上。第一,算子兼容性。把模型从GPU格式转换到NPU格式时,某个算子不支持是常态。一旦遇到不支持的算子,要么改网络结构,要么把这个子图留在CPU或GPU上执行,复杂度一下就上来。第二,精度不一致。NPU集群习惯上使用INT8量化,量化误差会累积。同一个模型,在GPU上用FP16跑精度达标,转到NPU上可能某些类别掉点,需要逐层排查、校准。第三,动态Shape支持弱。NPU为了极致性能,很多内部存储结构是静态规划的,输入尺寸一变化就要重新编译。遇到检测类模型那种后处理阶段,动态逻辑一多,NPU反而施展不开。
另外,不同NPU厂商的SDK、模型格式、量化工具各搞一套,迁移成本不低。这不是某一家的毛病,是整个生态还在成熟的正常状态。所以NPU的真实定位不是"GPU的全面替代",而是"特定负载下的能效补充"。明白这一点,后面整套思路才顺理成章。
1.3 顺便说说CPU和FPGA,为什么它们也不是标准的"第三答案"
聊选型时常有人提CPU和FPGA。CPU在低延迟、小Batch场景其实很有价值,单独的CPU节点也能承担不少推理任务。但大规模矩阵运算和GPU比,性能和单位成本都不占优。业务量一大,CPU方案算下来的总成本反而更高。
FPGA则走到另一个极端:可重配置、功耗可控、时延可以做到很确定,特别适合一些专用信号处理场景。但它的开发周期和调试难度都不是普通算法团队能长期承担的。你不仅要懂硬件描述语言,还要处理时序、布局布线一堆硬件层面的问题。模型还经常要改,每次迭代都要重新综合实现,收益和成本往往不成比例。
所以,真正能作为"第三答案"的,不是某种单一的替代硬件,而是"编排"——把一次推理拆开来看,哪些阶段适合GPU、哪些阶段适合NPU、哪些阶段留在CPU,再由一层调度逻辑把它们组合起来。这套组合拳,就是我这篇文章想展开的核心思路。
2. 第三个答案的底层逻辑:负载形状决定硬件阵容
2.1 从一维算力到四维目标:延迟、吞吐、Batch和功耗
很多硬件选型评测只喜欢报一个指标,比如多少T算力。但对生产推理服务来说,光看算力等于没看。真正决定选型的是四个可量化的目标:延迟、吞吐、Batch特性和功耗预算。
先说话延迟。在线推荐、智能客服这类场景,用户等不起,P95延迟超过几百毫秒就是事故。这类服务请求往往是小Batch,甚至Batch等于1,每次请求还有动态输入长度。算力再高,如果启动开销大、流水线切不深,延迟依然上不来。再说吞吐。离线批量任务完全不同,比如一批图片要批量质检,一晚处理十万张,单个任务跑慢几秒没关系,只关心总吞吐量和每张图的成本。Batch在这里可以压得很高,对算力密集型硬件非常友好。
第三个维度是Batch分布。我接触过的很多服务,平均Batch只有2到4,但峰值能达到32以上。这种波动对GPU最不友好,因为大算力卡在低Batch下发挥不出优势;对NPU倒是相对合适,因为它的静态特性在小Batch下效率不低。第四个维度是功耗预算。机房供电上限是多少,边缘设备是电池还是太阳能,这些不是技术团队能谈判的物理约束。
把这四个维度列成一张表,很多问题的答案会自己浮现。比如:在线API,Batch小、延迟敏感,适合GPU或CPU;离线批量,Batch大、延迟无所谓,CPU加NPU组合也行,成本优先;边缘摄像头,功耗受限,NPU基本是必须项;大模型服务,Prefill和Decode分化明显,异构方案天然有优势。
2.2 把一次推理拆成流水线,让不同硬件各跑一段
大多数团队把模型当成一个不可分割的整体,要么整张卡,要么整颗NPU。实际上一次推理是由多个阶段组成的流水线,阶段之间差异很大。
拿一个典型的工业质检视觉模型举例。输入是工业相机拍的原始图像,处理流程大致是:图像解码、尺寸缩放、归一化,然后进入一个骨干卷积网络做特征提取,最后是一个检测头加一堆后处理逻辑,比如NMS。这三个阶段的负载特征完全不一样。
前处理是密集型数据搬运,图像解码对算力没要求,但对数据带宽和格式转换有要求,适合在CPU上跑,或者走专门的硬件解码通道。骨干卷积网络是规则的矩阵乘加,输入尺寸固定、网络层结构固定,最适合NPU这类专用阵列。到了检测头和后处理阶段,牵扯到坐标解码、阈值筛选、NMS,这些都是动态Shape和控制流操作,NPU最怕这种负载,GPU的灵活性在这里更有优势。
语言模型也有类似的分化。Prefill阶段要并行处理整个输入序列,计算密度高,适合算力大的部件;Decode阶段是逐Token生成,访存带宽受限,纯算力不再是唯一瓶颈,这时候硬件的功耗差异会被放大。所谓Prefill和Decode分离,本质上就是这个思路在语言模型场景的应用。当然,这个方案要求高速互联和成熟的通信实现,不然中间状态来回搬运的成本会吃掉收益,团队要有足够的系统能力才建议碰。
所以第三个答案的第一步,不是先选卡,而是先画流水线:请求进来之后,每一个阶段分别是什么负载,消耗多少时间,占多大比例。画完之后,切分方案基本就出来了。
2.3 调度层:这才是整套架构的"第三个答案"
不同引擎解决了算力问题,真正让它们协作的是一层调度逻辑。这也是很多做NPU的朋友容易忽略的点:把模型转好、性能跑起来,却缺一个"把请求分给谁"的决策者。没有调度层,异构只是一堆各自运行的孤岛。
调度的职责可以概括为三件事。第一是路由:根据当前请求的形状、优先级和排队情况,决定它进GPU、NPU还是CPU引擎。第二是配额:给每个引擎设置最大并发上限,保护引擎不被打爆,也保护整个系统不互相挤占。第三是熔断:当某个引擎排队时间超过阈值时,快速失败或者降级到备用引擎,而不是让请求无限堆积。
打个比方,这个调度层很像快递分拣中心的中控台。大件重货走干线运输,小件快件走城际专线,异常件进人工窗口,中控台负责实时调配。你不会把所有快递都塞给一种车,同样也不该让所有请求都落在一种卡上。
这套思路的好处在于,它是可生长的。今天只有GPU,就只在GPU一个引擎上做路由;明天加了NPU板卡,无非是多插一个引擎,路由规则加两行配置。硬件选型从"一次性拍板"变成了"渐进式扩展",这正是我认为的第三个答案最核心的价值。
3. 实操落地:一套异构推理服务是怎么搭起来的
3.1 第一步:给模型做一次"负载体检"
别急着转模型、买机器,先给现有服务做一次体检。我见过最典型的失败路径:拿到一款NPU开发板,跑通一个Demo,立刻立项要替换GPU,结果上线第一周就发现吞吐没上去、延迟还超了。原因很简单,Demo和真实流量根本不是一回事。
体检要收集几类数据。第一,请求量曲线。把一周的请求按小时统计,找出波峰时段、波谷时段、峰值是平均值的几倍。第二,单请求时延构成。在服务里埋点,分别统计预处理、模型推理、后处理三个阶段的耗时占比。这一步很关键,因为如果预处理占了一半时间,换再强的NPU也只能优化推理那部分。第三,Batch分布。把一周所有请求的Batch大小记录下来,看平均Batch和P99 Batch。第四,资源使用率。当前GPU的算力利用率、显存占用、功耗曲线,全部拉出来,判断瓶颈到底是算力、显存还是IO。
埋点不需要多复杂,就是几个计数器加耗时直方图,跑三到五天,数据就够用了。拿到数据后,把负载画像填进一张表格,这样后面每一步决策都有依据,而不是拍脑袋。这个体检动作本身,往往就能暴露很多"还以为不是瓶颈"的问题。
3.2 第二步:确定引擎分工和路由规则
体检完成之后,就可以画分工图了。还是用前面那个工业质检项目来说,最后定下来的分工是这样:
- CPU负责图像解码、缩放和最后的坐标结果透传;
- NPU负责骨干卷积网络,输入固定为640乘640,Batch固定为4,统一走INT8;
- GPU负责检测头、NMS和动态后处理,这部分就吃灵活性和动态Shape。
为什么这么分?骨干网络在NPU上静态编译后,性能最稳定,能效比最高。而NMS这类逻辑控制密集的操作,在NPU上要么不支持、要么性能很差,留在GPU上反而省心。这样一来,每次请求先是CPU做预处理,然后进入NPU做重计算,最后到GPU收尾,三个引擎各干各最擅长的活。
引擎分工和路由规则确定之后,最好把它们统一成一份配置文件,让基础设施团队不用每次改判断逻辑。我习惯用一种类YAML的结构来描述整条流水线,字段包含阶段、引擎类型、输入约束、超时和回退条件,比如下面这个简化示例:
pipeline: preprocess: engine: cpu decode: hw_decoder backbone: engine: npu input_info: width: 640 height: 640 batch: 4 precision: int8 timeout_ms: 120 head: engine: gpu min_batch: 1 max_batch: 8 timeout_ms: 80这个配置文件的含义是:把请求按阶段路由,预处理走CPU,骨干网络走NPU并限定输入格式,后处理走GPU。如果NPU阶段超时超过120毫秒,调度器要按下一小节设计的策略做回退。
3.3 第三步:设计回退、超时和动态Batch策略
引擎分工定好之后,最见功力的其实是异常路径设计。很多人踩坑就踩在这里:NPU排队排到崩溃,调度器把流量全甩给GPU,结果GPU也崩,整个服务雪崩。
一个稳妥的思路是设置百分比配额,而不是简单的"超过就全切"。例如,NPU队列长度超过阈值时,只把增量请求的30%切到GPU,剩下70%继续排队或者直接快速失败。这个比例要根据压测结果调。另外,每个引擎必须设置最大并发数,GPU也要设置上限,超了就直接返回降级响应,而不是让请求无限堆积。
动态Batch也要按引擎区别设置。NPU适合固定小Batch,好处是延迟稳定;GPU可以适当把小块合并成大Batch,提高吞吐。调度器可以维护一个小型合并窗口,例如攒够4个请求再发给NPU,攒够8个再发给GPU。合并窗口不能设太大,否则在线请求的延迟就上去了。
调度器本身不复杂,但回退比例、最大并发和动态Batch窗口这几个参数必须做成可配置项,而不是写死在代码里。我会用一个极简的伪代码把核心逻辑固化下来,方便后续压测时反复调整参数:
def route_request(req): if queue_len(npu) > npu_max_queue: if failover_ratio(gpu) < 0.3: return dispatch_to(gpu, flatten_batch(req)) else: return reject_with_capacity_hint(req) if req.is_dynamic_shape or req.requires_fp16: return dispatch_to(gpu, req) return dispatch_to(npu, gather_batch(req, target=npu_batch_size))这段代码里最关键的是failover_ratio,它控制回退流量比例,把雪崩的概率压到最低。这个指标要看全局,而不是单个请求,建议做成一个原子计数器,避免并发误判。
3.4 第四步:验证、灰度与压测
功能跑通后,不要直接切全量流量。先把三种方案放在同一份回放流量下对比:纯GPU、纯NPU、异构混合。用压测工具把线上录制的真实请求回放一遍,分别记录P50和P95延迟、每分钟请求数、功耗和错误率。数据出来后你会看到很有趣的结果:纯GPU方案延迟最低但不便宜,纯NPU方案能效最好但在动态请求上P95很糟糕,混合方案单看延迟不是第一名,但综合成本和稳定性往往最平衡。
灰度放量也讲究节奏。先切5%的流量到混合方案,观察一周,确认没有精度事故和时延毛刺,再逐步扩大。每一步都要对比业务侧的核心指标,这些指标只有业务方最清楚,上线窗口要留足。
还有一个必须做的环节:精度对齐。GPU跑FP16,NPU跑INT8,两边出来的结果要抽样比对。方法是对同一批测试样本,先让GPU方案输出作为基准,再让NPU方案输出,算一下各类别的误差分布。如果某个类别的指标掉得厉害,就得回查是哪一层量化出了问题,然后在配置里对该层单独保持高精度。
4. 常见问题速查与避坑实录
4.1 NPU转换阶段的"老三样"问题
把模型转到NPU的过程,绕不开三个问题,我挨个说。
第一,算子不支持。某款NPU的SDK只支持有限算子集合,模型里遇到不支持的算子,最常见的解决办法是"算子剥离":把不支持的部分拆出来,放到CPU或GPU上作为一个子图运行。听起来简单,但子图之间的张量传输是额外开销,拆得太碎反而更慢。我的经验是能改网络结构解决的,优先改网络,比如用一个更通用的算子替换自定义层。第二,动态Shape被固定。NPU编译器为了做极致优化,会把输入尺寸固定下来,你传一张不同尺寸的图,它要么报错要么重新编译,延迟直接爆炸。解决办法是校准输入分布,项目里绝大多数图落在什么尺寸区间,就固定在那个区间,其余尺寸走回退路径。第三,INT8量化精度掉落。之前说的逐层排查方法,这里具体说一下:先在测试集上跑一遍INT8和FP16的中间结果,逐层算特征分布差异,把误差放大最多的那一两层保留FP16,其余继续用INT8。这样大部分精度损失可以找回来,代价是这两层速度稍慢,但可接受。
4.2 异构调度里的时延雪崩问题
我在项目里遇到过两次比较严重的雪崩,都是同一种模式:某个引擎出现慢请求,客户端超时重试,重试又把对端压垮,然后所有引擎一起变慢。
排查路径有迹可循。先看监控图,哪个引擎的队列长度最先抬头,哪个引擎的错误率在雪崩前就开始爬升。然后检查超时配置,很多服务超时时间设成固定值,超出就重试,重试其实是最危险的,它会把一个故障放大好几倍。解决方向是:给每个阶段设置独立超时;重试要带全局的速率限制;雪崩前让队列自然溢出,而不是无限存积。调度器要主动丢弃最低优先级的请求,保证核心请求的P95稳定。这个取舍需要产品侧提前对齐,但架构上必须预留。
这里有一张问题速查表,按症状排查比较快:
| 症状 | 优先排查方向 | 常规解决手段 |
|---|---|---|
| NPU转换报算子不支持 | 日志里的算子名 | 改结构或子图回退,避免碎片化 |
| INT8精度掉点 | 逐层特征分布对比 | 误差大的层保留FP16 |
| 输入尺寸变化延迟暴涨 | 检查是否触发重新编译 | 统一固定尺寸,超范围走回退 |
| 某引擎队列无限堆积 | 队列长度和重试计数 | 设最大并发,超限快速失败 |
| 混合方案P95抖动 | 阶段间数据拷贝时间 | 共享内存或零拷贝,减少跨引擎搬运 |
4.3 成本模型,怎么算才对
最后算一笔总账。很多团队选型只看"单卡价格",这是个误区。真正的成本要摊开看:硬件采购、机房供电和散热改造、折旧周期、开发人力迁移成本、模型重新调优的维护成本。我按一套中等规模服务粗略算过,纯GPU方案和混合方案的采购价差大约15%到20%,但运行两年算上电费和迁移人力,混合方案往往能省下30%以上。这个数字会随地区电价浮动,但趋势是稳定的。
当然,规模太小的团队我不建议一上来就搞混合架构。模型只有一个、QPS不到几十,直接买一台GPU服务器是最省心的起步方式。异构协同是在你有稳定流量、明确的瓶颈和足够工程人力之后,再做减法优化成本的手段。顺序不能反。
5. 一些真实体会
5.1 关于硬件选型的三个心态转变
第一,不要迷信任何一家厂商给的Benchmark。官方评测用的是动手环境精心调过的确定性负载,你的线上流量是动态度、随机的、各式各样都有。拿真实流量回放测出来的数字才算数。
第二,异构协同的复杂度是真实存在的。每多接入一个引擎,就多一套依赖、多一群兼容性问题,需要投入的维护成本常常被低估。如果你团队只有两三个人,我建议先从一个引擎做到极致开始,等瓶颈确凿了再引入第二个引擎,不要为了技术炫酷去搞复杂架构。
第三,我踩过最大的坑是"让引擎之间互相等待"。不同引擎处理速度天然不一样,流水线设计时一定要用解耦队列把各阶段隔开,否则一个阶段抖动就会拖垮全链路。队列长度要有上限,积压时要优先处理最关键的请求。
5.2 监控先行,比硬件选型更优先
最后再分享一个小技巧:把所有引擎的监控指标统一成一个标准格式,无论GPU、NPU还是CPU,都上报"排队长度、处理时延、错误率、利用率"这四类指标。未来哪怕换芯片、加新板卡,也只是插一套监控接口的事,不用重新造轮子。
把选型问题从"该买谁"变成"怎么编排",整个系统反而更好维护。这套思路我至今还在用,希望对正在纠结要不要买NPU、或者刚被GPU账单吓到的你有帮助。