
A:模型上线了,推理延迟 280ms,比预期高了一倍。
B:280ms 是什么模型?输入多大?
A:BERT-base,序列长度 512,batch=1。
B:单请求 280ms 明显偏高。BERT-base 在 910 上,序列 512,理论延迟是 80-100ms。差了 3 倍,先排查瓶颈在哪。
A:怎么排查?
B:看三个指标:AICore 利用率、HBM 带宽利用率、HCCL 通信时间(如果多卡)。用 npu-smi info -t usages 监控,推理的时候看实时数据。
A:AICore 利用率 45%,HBM 带宽利用率 60%,HCCL 不涉及(单卡)。
B:45% 太低了,正常应该 >70%。带宽利用率 60% 也偏低。说明瓶颈不在计算,在数据搬运。你的预处理在哪做的?
A:预处理在 CPU 上,用 transformers 库的 tokenizer。
B:tokenizer 本身不慢,但每次推理都要把 tokenized 结果从 CPU 搬到 NPU。PCIe 的带宽是 16GB/s,搬一个 (1, 512) 的 int64 tensor,大小是 4KB,理论耗时 0.25us。但实际搬运开销远大于这个------CPU 要准备数据、NPU 要接收、中间还有一次同步。batch=1 的时候,这个开销占比很大。
A:那预处理挪到 NPU 上?
B:对。昇腾有 ops-adv 仓库,里面有文本预处理的算子(分词、转 token ID)。但 tokenizer 的逻辑比较复杂,ops-adv 目前只支持基础操作(比如 lookup table)。如果模型用的是 BPE tokenizer,可能要自己写一个轻量版本。
A:写一个 tokenizer 算子工作量不小。有没有更快的方案?
B:有。把预处理和推理合成一个请求,减少 CPU-NPU 的交互次数。现在的流程是:CPU preprocess → 搬到 NPU → NPU inference → 搬回 CPU → CPU postprocess。改成:CPU preprocess → 搬到 NPU → NPU inference + postprocess(后处理也在 NPU 上做)→ 搬回 CPU。后处理就是取 argmax 和 softmax,很简单,用 Vector Unit 一行代码就能做。
A:改一下试试。
(半小时后)
A:改完了,延迟从 280ms 降到 180ms。还是比预期高。
B:180ms,进步了。AICore 利用率现在多少?
A:62%。带宽利用率 75%。
B:利用率上去了,但还是不够。再看计算图,BERT 的 Attention 算子有没有融合?
A:怎么看?
B:跑一遍推理,把 GE 的图导出来:export GENGINE_GRAPH_SAVE_PATH=./ge_graphs。然后用 Netron 打开,找 Attention 相关的节点。
A:看了一下,Attention 还是分开的:MatMul → Softmax → MatMul。没有合成 FlashAttention。
B:问题找到了。FlashAttention 没生效。ATC 编译的时候,模型要用 NPU 设备加载,GE 才能识别出 Attention 模式。你的模型是怎么加载的?
A:先在 CPU 上加载,然后 .npu()。
B:这就是问题。CPU 加载之后,GE 看到的是一个普通的 PyTorch 模型,不认为是 Attention。改成:
python
with torch.npu.device(0):
model = BertModel.from_pretrained("bert-base")
这样 GE 在编译时能识别出 Attention 模式,自动合成 FlashAttention。
A:改一下,重新编译。
(编译后)
A:延迟从 180ms 降到 95ms。AICore 利用率 78%,带宽利用率 45%。
B:95ms 接近预期了。带宽利用率降到 45%,说明瓶颈转移到计算了,这是好事。还有优化空间吗?
A:跑 AOE 调优试试?
B:BERT 的调优收益通常在 10-20%。跑一下 GA,50 轮就够了。记得保存调优结果,下次编译直接加载。
A:调完之后延迟 82ms。满足预期了。
总结一下这次调优的关键:
- 预处理/后处理搬到 NPU,减少 CPU-NPU 数据搬运
- 模型用 NPU 设备加载,让 GE 识别出 Attention 模式,触发 FlashAttention 融合
- 跑 AOE 调优,进一步压榨 AICore 性能
从 280ms 到 82ms,提升 3.4 倍。单卡推理慢的问题基本解决了。