前言

写昇腾代码的时候有两类报错最让人头疼:

NPU 掉线:程序跑着跑着突然 RuntimeError: NPU is not available,一看 npu-smi 发现卡的状态从 Healthy 变成了 Error。

内存分配失败:显存够,但分配的时候报 OutOfMemoryError 或者 MemoryError,百思不得其解。

这两类问题往往不是应用层的问题,要往驱动层定位。

NPU 掉线的常见原因

NPU 掉线的本质是硬件或者驱动层面的异常。驱动检测到 NPU 状态不对,会把它标记为 Error 状态,禁止后续操作。

常见原因:

1. ECC 错误(最常见)

ECC 是显存纠错机制。昇腾 910 的 HBM 显存支持 ECC,当检测到单位错误时可以自动纠正,但当错误积累到一定阈值,驱动就会把 NPU 标记为不可用。

# 查看 NPU 状态
npu-smi info -i 0

# 看 ECC 相关错误计数
npu-smi info -i 0 -v | grep -i ecc

ECC 错误计数如果一直在涨,可能是硬件问题,也可能是内存带宽被压到极限导致误码率上升。跑大模型推理的时候如果散热不好,显存温度过高,ECC 错误率会明显上升。

2. 驱动崩溃

驱动本身也有 Bug,有时候跑着跑着内核模块 panics 了。查系统日志:

dmesg | grep -i ascend
# 或者
journalctl -k | grep -i ascend

驱动崩溃的日志一般会包含 BUG:Oops 或者 npu 相关的模块名。如果看到 npu driver timeout 之类的信息,说明驱动检测到某个操作超时了,可能是因为计算本身跑太久,也可能是因为 PCIe 链路问题。

3. PCIe 链路不稳定

多卡训练的时候,如果主板上同时插了 GPU 和 NPU,PCIe 带宽可能不够。查 PCIe 状态:

lspci -vv -d "1a2e:"  # 昇腾的 vendor ID 是 1a2e
# 看 LnkSta 字段,确认是不是在 Gen3 x16 全速运行

Current Speed: 8GT/s 是 Gen3,Speed: 5GT/s 是 Gen2。如果显示 Gen2 x8,说明带宽降级了,可能是因为槽位问题或者线缆松动。

内存分配失败的驱动层排查

显存分配失败(明明有空余显存但分配不了)是另一个高频问题。

先确认是真的显存不足还是分配器的问题:

import subprocess

def check_npu_memory():
    # 查每张卡的显存使用情况
    result = subprocess.check_output(
        "npu-smi query -t memory -i 0",
        shell=True
    ).decode()

    print(result)
    # 输出类似:
    # Device[0] Memory Total: 32768 MiB
    # Device[0] Memory Used: 8192 MiB
    # Device[0] Memory Free: 24576 MiB

check_npu_memory()

如果 Memory Free 还有很多但分配失败,问题往往出在显存碎片上。

昇腾的显存分配器用的是伙伴系统和 slab 的混合方案。长时间跑推理服务的时候,频繁分配和释放不同大小的 tensor,会导致显存碎片化——总空闲量够,但最大连续块不够。

一个典型的碎片化场景:

# 假设显存总共 32GB,目前使用了 20GB
# 这 20GB 里可能有几百个小块分布在各处

# 分配一个大 tensor(需要 10GB 连续空间)
# 虽然 20GB 空闲,但碎片化之后最大连续块可能只有 2GB
# 分配失败,报 OutOfMemory

# 解决办法:预先分配一块大的,然后用 pointer arithmetic 切分
# 或者跑一轮显存整理(有些版本支持)
import acl

# 触发显存整理(如果有这个接口)
try:
    acl.rt.reset_device(0)  # 重置设备,清理碎片
except:
    pass

acl.rt.reset_device 会清空当前进程的显存分配池,相当于重启一下显存分配器。但要注意,这之前分配的所有 tensor 都会失效,所以最好在服务启动阶段就做一次。

驱动层的调试工具

昇腾驱动提供了一些底层调试接口,普通开发者可能不知道:

# 查看驱动层的 trace 事件
cat /sys/kernel/debug/tracing/trace | grep ascend

# 或者开启 ascend 的 trace
echo 1 > /sys/kernel/debug/tracing/tracing_on
echo nop > /sys/kernel/debug/tracing/current_tracer
echo 'ascend_*' > /sys/kernel/debug/tracing/set_event
# 然后跑你的程序
cat /sys/kernel/debug/tracing/trace > trace_output.txt

驱动层的 trace 信息很详细,能看到每次 NPU 调用在内核里发生了什么,什么时候发了中断,什么时候回来。但信息量巨大,要结合具体问题筛选。

一个实战案例

之前帮一个团队定位推理服务的 NPU 掉线问题。现象是:服务跑大概 8 小时之后,某张卡会掉线。重启服务之后能恢复,但再跑 8 小时又掉。

排查过程:

# 查掉线前后的系统日志
dmesg | tail -1000 | grep -B5 -A5 "npu"

# 发现错误信息
# [12345.678901] ascend 0000:3b:00.0: NPU timeout, reset triggered
# [12345.679012] ascend 0000:3b:00.0: ECC error count exceeded threshold

ECC 错误计数超阈值。结合他们跑的模型看,是一个超分辨率模型,对显存带宽的压力很大。查 npu-smi 的历史数据,发现掉线前显存温度一直在 85 度以上,而正常应该稳定在 70 度左右。

最后定位到是散热问题:服务器的风扇转速配置不对,部分卡片的气流被堵住了。调整了风扇配置之后,显存温度降到 72 度,再没出过掉线。

这个案例说明,有时候 NPU 掉线的根因不在代码里,在机房的散热系统里。

内存分配失败的一个隐蔽原因

还有一个很容易被忽略的问题:多进程共享 NPU 时,每个进程的显存分配是独立的,但驱动层面的显存统计有时候不准。

比如两个推理服务进程各分配了 8GB 显存,驱动显示总共用了 16GB,但实际上有 2GB 是重叠统计的(共享权重那部分)。当第三个进程想分配 10GB 的时候,驱动会报显存不足,但 npu-smi 看到的空闲量其实是够的。

这种情况要看 npuinfo 里的 memory.used 是进程级的还是系统级的,有些版本的工具显示的是进程级分配,没有考虑共享部分。

遇到这种问题,可以让进程逐个启动,逐个确认显存分配情况,把共享权重部分单独处理。

仓库在 https://atomgit.com/cann/driver,有驱动层的一些工具和诊断脚本可以参考。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐