-
Notifications
You must be signed in to change notification settings - Fork 64
Device Error Codes_zh
来源:本页镜像自仓库文档
docs/troubleshooting/device-error-codes_zh.md。 请以仓库版本为准并在其中修改,wiki 页随后同步,避免两处内容漂移。
本文适用于通过
Worker.run(...)直接运行tensormap_and_ringbuffer(以下简称 trb / rt2)运行时的上层调用方。文中逐一说明每个设备侧错误码的 含义、触发场景,以及遇到该错误码时应如何定位问题。
trb 运行时将编排(orchestrator)与调度(scheduler)两个环节均放在 AICPU 上执行。
一旦设备侧出现致命情况,运行时会将对应的错误码 latch(锁存) 至设备共享内存
(SM)头部;host 侧随后在 validate_runtime_impl 收尾阶段读回并打印。因此,上层
调用方可观测到的信息分为以下两部分。
当返回码非 0 时,Worker.run(...) 内部会抛出如下异常:
RuntimeError: run failed with code <rc>
其中 <rc> 的具体含义取决于运行环境:
| 环境 |
<rc> 的含义 |
|---|---|
sim(a5sim / a2a3sim) |
直接等于运行时状态 -N,其中 N 即下表所列错误码。例如 -1 表示 SCOPE_DEADLOCK,-100 表示 SCHEDULER_TIMEOUT。 |
| onboard(真实硬件) | 常被 CANN 看门狗(op-execute 超时 / stream-sync 超时)提前触发,从而被掩盖为通用的 507018。此时 <rc> 不再等于 -N,不可直接作为错误码解读。 |
关键提示:在 onboard 环境下,
507018(run_prepared failed/aclrtSynchronizeStreamWithTimeout failed)是一个泛化的 host 侧错误码, 多种不同的设备侧机制最终都会表现为这同一个码。因此,不应仅凭507018便判定为"死锁"或"内存溢出"——真正的设备错误类别记录在下文所述的日志行中。
无论 sim 还是 onboard,只要设备侧锁存了错误码,host 日志中都会出现如下一行:
[ERROR] PTO2 runtime failed: orch_error_code=<O> sched_error_code=<S> runtime_status=<R>
-
orch_error_code非 0,表示错误属于 编排错误(1–11),详见第 3 节。 -
sched_error_code非 0,表示错误属于 调度错误(100+),详见第 4 节。 - 二者至多有一个非 0;
runtime_status为统一归并后的-N。
若 sched_error_code=100(调度器检测到无前进进展并超时),日志中还会紧接着
输出一行子分类及定位信息,使调用方无需翻阅设备日志即可判断停滞点:
[ERROR] PTO2 scheduler timeout sub_class=<S1..S5> (detail=N) completed=c/t \
running=r ready=k waiting=w orch_done=d stuck_task_id=id stuck_core=core
排查的第一步始终是:在 host 日志中检索 orch_error_code=、sched_error_code=
或 sub_class=,先完成错误归类,再对照下文的表格逐项定位。即便 onboard 环境
将异常码掩盖为 507018,上述几行日志仍会照常打印。
| 码 | 名称 | 层 | 简要含义 | 大致归属 |
|---|---|---|---|---|
| 0 | NONE | — | 无错误 | — |
| 1 | SCOPE_DEADLOCK | 编排 | 单个 scope 内的任务数达到任务窗口上限,slot 无法回收 | 用户编排 |
| 2 | HEAP_RING_DEADLOCK | 编排 | ring 的任务 slot 与 heap 双双耗尽,无法继续提交任务 | 用户编排 / 配置 |
| 3 | FLOW_CONTROL_DEADLOCK | 编排 | 任务窗口被阻塞但 heap 未满(典型场景为同一 ring 上的嵌套) | 用户编排 |
| 4 | DEP_POOL_OVERFLOW | 编排 | 依赖(fanin)溢出池无法分配到条目 | 用户编排 / 配置 |
| 5 | INVALID_ARGS | 编排 | 编排 API 收到非法参数 | 用户编排 |
| 6 | (已废弃) | — | 旧的 per-task fanin 溢出,现已并入 4 | — |
| 7 | REQUIRE_SYNC_START_INVALID | 编排 |
require_sync_start 请求的 block 数超过该类核总数,必然死锁 |
用户编排 |
| 8 | TENSOR_WAIT_TIMEOUT | 编排 | 等待 tensor 数据就绪超时(生产者未完成 / 消费者未释放) | 用户 kernel / 编排 |
| 9 | EXPLICIT_ORCH_FATAL | 编排 | 编排代码主动调用 rt_report_fatal() 上报错误 |
用户编排(主动) |
| 10 | SCOPE_TASKS_OVERFLOW | 编排 | scope 任务记录缓冲被填满(实践中 ring 通常先满,几乎不可达) | 运行时内部 |
| 11 | TENSORMAP_OVERFLOW | 编排 | tensormap 条目池被阻塞,last_task_alive 无法前进 |
运行时内部 / 极端规模 |
| 100 | SCHEDULER_TIMEOUT | 调度 | 调度器检测到无前进进展并超时(详见子分类 S1–S5) | 视子分类而定 |
| 101 | ASYNC_COMPLETION_INVALID | 调度 | 异步完成条件非法(completion_type / counter 地址错误) | 用户 kernel(async) |
| 102 | ASYNC_WAIT_OVERFLOW | 调度 | 异步等待列表被填满(在途 async 完成数超过上限 64) | 用户 kernel(async) |
| 103 | ASYNC_REGISTRATION_FAILED | 调度 | 收到非法的 async 完成消息类型(通常已被 102 提前拦截) | 运行时内部 |
编排错误(1–11)由 AICPU 编排线程检测,写入
orch_error_code,以先写入者为准。 调度错误(100+)由调度线程检测,写入sched_error_code,同样以先写入者为准。
此类错误几乎都与资源容量与编排结构之间的匹配关系有关。共同的调参入口是
CallConfig.runtime_env 中的三项 ring 配置(也可通过同名环境变量
PTO2_RING_TASK_WINDOW / PTO2_RING_HEAP / PTO2_RING_DEP_POOL 覆盖):
-
ring_task_window:每个 ring 的任务槽数(须为 2 的幂,且 ≥4)。 -
ring_heap:每个 ring 的 heap 字节数(≥1024)。 -
ring_dep_pool:每个 ring 的依赖溢出池容量(≥4)。
问题在于:这三项该调到多大,不应凭猜测。错误码本身只告诉调用方"某类资源已耗尽", 却不指出是哪一类资源、由哪个 scope 触发。下述 scope_stats 工具正是用于补齐这一 信息,进而将容量调整到合适大小。
scope_stats 是 trb 运行时专属的 opt-in DFX 功能:它为编排中的每一个 PTO2_SCOPE
区域记录四类资源——task window 槽位、heap 字节、dep pool 条目、tensormap 条目——的
高水位,从而回答"是哪一类资源逼近容量、由哪个 scope 驱动"这一问题。这恰好是错误码
1、2、4 定位所需的信息,因此三者共用同一套排查手段。
-
开启方式(Worker 直跑):在
CallConfig上置位enable_scope_stats,并指定 输出目录output_prefix:from simpler.task_interface import CallConfig cfg = CallConfig() cfg.enable_scope_stats = True cfg.output_prefix = "outputs/my_run" # 输出根目录 worker.run(callable, args, cfg)
-
输出位置:
<output_prefix>/scope_stats/scope_stats.jsonl(NDJSON 格式)。 首行为运行元数据,其中task_window_max/heap_max/dep_pool_max为各 ring 的容量上限、tensormap_max为 tensormap 容量;其余每行是某个 scope 在 begin 或 end 边界处的一次采样。 -
对失败的运行同样有效:即便本次运行以 fatal 收场,元数据行会标记
"fatal": true, 且该点之前落盘的记录仍然保留。因此可直接对触发错误码 1 / 2 / 4 的 workload 开启 scope_stats,观察究竟是哪一类资源、在哪个 scope 撞上了上限。 -
快速判读:某个 scope 的资源高水位等于该 scope 在 end 边界与 begin 边界之间 的差值(即
task_window/heap/dep_pool各自的*_end − *_start)。将其与 元数据中对应 ring 的*_max相比,占比最接近 1 的那一类资源即为瓶颈所在。 -
可视化报告:执行
python simpler_setup/tools/scope_stats_plot.py <output_prefix>/scope_stats/scope_stats.jsonl会生成自包含的 HTML 报告,其中的 Top Peaks 表直接列出每类资源最接近容量的 ring 与 scope,无需手动解析 JSONL。 -
据此调参:定位到瓶颈资源及其所在 scope 后,按下表调整
CallConfig.runtime_env中的对应项,或从结构上拆分对应的 scope:瓶颈资源 对应错误码 调整方式 task window 1 调大 ring_task_window,或将该 scope 拆分为多个小 scopeheap 2 调大 ring_heap,或减小单任务的参数 / 中间 tensor 占用dep pool 4 调大 ring_dep_pool,或减少单任务的显式依赖数量
工具的完整用法、报告字段与内部机制详见
docs/dfx/scope-stats.md。
-
触发条件:同一 scope 内提交的任务数 ≥ 任务窗口容量。由于 scope 内任务持有的
fanout 引用要到
scope_end时才会释放,slot 无法回收,导致整个 ring 死锁。 -
排查步骤:
- 统计发生停滞的 scope 内共提交了多少任务,确认其是否 ≥
ring_task_window; 开启 scope_stats(见上文"用 scope_stats 定位资源瓶颈")可直接读出各 scope 的 task window 高水位,无需人工统计。 - 或调大
ring_task_window,或将大 scope 拆分为多个小 scope,使 slot 能在scope_end处及时回收。
- 统计发生停滞的 scope 内共提交了多少任务,确认其是否 ≥
- 触发条件:ring 的任务分配器报告分配失败——任务 slot 与 heap 空间同时耗尽, 无法继续提交任务。
-
排查步骤:此类问题通常源于单任务的参数或中间 tensor 占用过大,或
ring_heap配置过小。请先查看 host 日志中打印的Ring buffer sizes: task_window=... heap=... dep_pool=...,对照单个 scope 的实际 需求,相应调大ring_heap。开启 scope_stats(见上文"用 scope_stats 定位资源 瓶颈")可进一步定位到 heap 高水位究竟落在哪个 scope,从而据实调整容量。
-
触发条件:任务窗口被阻塞、但 heap 未满(典型场景为同一 ring 上的嵌套结构:
外层任务等待内层任务,而内层任务又无法获得 slot)。ring 分配器在空转约 500ms
(由
PTO2_ALLOC_DEADLOCK_TIMEOUT_CYCLES控制)仍无法回收 slot 后,报出此码。 -
排查步骤:检查是否在同一 ring 上进行了自依赖或嵌套提交;可调大
ring_task_window,或将嵌套结构改为跨 ring 提交。此码与错误码 1 的区别在于: 错误码 1 为单个 scope 被填满,错误码 3 为流控或嵌套导致的阻塞。
- 触发条件:某个任务的显式依赖(fanin)数量过多,溢出池无法分配到条目。
-
排查步骤:定位挂载了超大
set_dependencies()列表的任务;减少单任务依赖数量, 或调大ring_dep_pool。开启 scope_stats(见上文"用 scope_stats 定位资源瓶颈") 可读出各 scope 的 dep pool 高水位,据此判断该调大容量还是精简依赖连线。
-
触发条件:编排 API 收到非法参数。常见来源包括:
-
alloc_tensors()未提供任何TensorCreateInfo,或提供了非 output 的创建信息; - 在手动 scope 内再开启自动 scope(嵌套非法);
-
set_dependencies()引用了不存在或尚未提交的 task id; -
submit_*的L0TaskArgs自带 error 标志。
-
-
排查步骤:此类错误属于编排代码缺陷,而非容量问题,调整 ring 大小无效。
应回到 orchestration
.cpp中,检查上述 API 的调用参数。
-
触发条件:任务调用了
require_sync_start(),且请求的block_num超过目标核 类型的物理核总数(对 AIV 为 AIV 总数total_aiv_count;对 MIX / AIC 为 cluster 总数total_cluster_count,即每个 cluster 一个 AIC)。判定基准是该类核在硬件上的 物理总数,而非当前空闲核数——sync-start 允许等待正在忙碌的核执行完毕后再一同 起跑,因此只要block_num不超过物理总数即为可满足;唯有当它连全部核都无法容纳时 才必然死锁,运行时据此在提交阶段直接拒绝。 -
排查步骤:将该任务的
block_num降至不超过目标核类型的物理核总数;或重新确认 是否确有 sync-start 的必要。
-
触发条件:等待某个 tensor 数据就绪超时(默认 15s,由
PTO2_TENSOR_DATA_TIMEOUT_MS控制,且已做频率缩放)。原因或为生产者任务始终未完成(kernel 卡死或执行过慢), 或为消费者未释放 fanout 引用计数。 -
排查步骤:
- 首先确认是否存在某个生产 kernel 真正 hang(可参考错误码 100 的子分类 S1)。
- 核对依赖连线是否正确:消费者确实声明了对该 tensor 的依赖,且会正常退出并释放 引用。
- 若 kernel 仅为执行缓慢而非真正死锁,可临时调大
PTO2_TENSOR_DATA_TIMEOUT_MS加以验证。
-
触发条件:编排代码主动调用了
rt_report_fatal(PTO2_ERROR_EXPLICIT_ORCH_FATAL, ...)。这是提供给编排作者的主动 上报机制,不依赖任何运行时资源的耗尽。上报后,所有后续编排 API 调用都会短路为 no-op。 -
排查步骤:此错误由编排代码主动埋设,依据
rt_report_fatal调用处的 message 自行排查即可。
- 正常情况下不应出现。错误码 10 在实践中不可达(ring 通常先满,会先报出错误码 1 或 3);错误码 11 需要 tensormap 条目池(编译期上限 65536)被海量条目填满,且存在 卡住不回收的生产者。
- 排查步骤:若确实命中,通常意味着运行时内部不变量被破坏(或属于极端规模的 workload)。请保留设备日志并联系运行时维护者——此类问题无法通过常规调参解决。
在展开错误码 100 之前,先说明 onboard 环境下的超时竞争——它决定了同一次调度
停滞最终会被观测成 -100(带 sub_class)还是被掩盖成 507018,是理解后续排查
的前提。
关于 onboard 环境下的超时竞争:调度超时(
PTO2_SCHEDULER_TIMEOUT_MS)、 STARS op-execute 超时(PTO2_OP_EXECUTE_TIMEOUT_US,默认约 45s,触发HandleTaskTimeout杀aicpu-sd)、host stream-sync 超时 (PTO2_STREAM_SYNC_TIMEOUT_MS)三者之间存在竞争,最终由最先触发的一方决定 呈现结果。最先触发的一方决定了最终观测到的是-100加sub_class,还是被掩盖 的507018。一次 45s 的 op-execute kill 并不等同于死锁,也可能仅仅是 kernel 执行时间过长。若需测量设备上的真实耗时,可调大PTO2_OP_EXECUTE_TIMEOUT_US。
这三个超时的值在
Worker.init()时即被锁定,之后每次run()都不再更新。 在 Worker 直跑通路上,一次Worker.run(...)只会在已有的持久流上重新拉起 AICPU / AICore 执行 kernel 并做一次流同步;AICPU / AICore 流以及这三个超时的 配置都是Worker.init()阶段的一次性动作:
- op-execute 超时通过
aclrtSetOpExecuteTimeOutV2在设备首次 attach(即 init)时设置一次,是 STARS 的设备级设置,后续run()不再改动。- stream-sync 超时的取值在 init 时从环境变量解析并缓存;此后每次
run()的流同步都以该缓存值为参数发起,但传入的始终是同一个固定值,并非每次重新配置。因此,在同一个 Worker 上多次
run()之间修改PTO2_OP_EXECUTE_TIMEOUT_US/PTO2_STREAM_SYNC_TIMEOUT_MS不会生效——这两个 环境变量只在init()时读取一次。若需以不同的超时取值做对比(例如借调大 op-execute 超时来区分"真正死锁"与"仅仅是执行缓慢"),必须为每个取值重新创建 Worker(重新init()),或在独立进程中运行。
-
触发条件:调度器在约
PTO2_SCHEDULER_TIMEOUT_MS的时间内检测不到任何前进 进展。这是设备停滞的汇聚性错误码;运行时会进一步将其细分为 S1–S5 并打印 定位信息。 -
首要动作:读取 host 日志中的
sub_class=一行,按下表逐项对照:
| 子分类 | 含义 | 主要原因 | 排查方式 |
|---|---|---|---|
| S1 running-stalled | 有任务在核上运行但始终无法完成 | AICore kernel hang / kernel 执行时间过长 | 查看 stuck_task_id / stuck_core,定位到具体 kernel,排查死循环或超长计算 |
| S3 ready-but-all-idle | 所有核均空闲、存在 fanin 已满足的就绪任务、却未被派发 | 派发循环或 sync-start 起跑门问题 | 多属运行时侧;检查是否使用了 sync-start 且配置异常 |
| S4 dependency-deadlock | 仅剩 WAIT 状态任务,fanin 永远无法解除 | 依赖图连线错误或成环 | 通过公开 API 几乎无法构造(详见下文);多为依赖连线缺陷 |
| S5 orchestrator-starvation | 已提交任务均已完成,但编排尚未结束,调度器空转 | 编排上游被卡住 | 检查编排线程是否在某处自旋或等待 |
| unknown | 前提或记账不变量被破坏 | 运行时内部缺陷 / 内存损坏 | 保留设备日志,上报运行时维护者 |
实践中可复现的主要为 S1 与 S3。 S4、S5、unknown 均为防御性标签:当前公开 API 无法构造出对应的活状态(例如
set_dependencies只能引用已提交任务,无法表达环; 被卡住的生产者其状态为 RUNNING,归入 S1 而非纯 WAIT)。若确实观测到 S4 / S5 / unknown,几乎可以断定为运行时内部缺陷,请予以上报。
-
S1 的典型排查:host 侧的
sub_class=一行会给出stuck_task_id与stuck_core。凭此 task id 回到编排代码,找出对应的 kernel,重点排查:死循环、 等待一个永远不会到达的信号,或单纯因计算量过大而触发超时。可临时调大PTO2_SCHEDULER_TIMEOUT_MS,以区分"真正死锁"与"仅仅是执行缓慢"。 -
借设备日志查"哪个核在跑哪个 kernel":当
stuck_task_id还不足以定位时,把 日志级别提到最详细的 V0,设备日志(AICPU log)就会打印停滞时刻的任务快照, 形如:[STALL thread=0 idle_iterations=...] TASK ring=1 task_id=42 state=RUNNING \ fanin_refcount=0/2 kernels=[aic:3 aiv0:7 aiv1:-1] \ running_on=[owner_thread=0 cores=[core=5(AIC) core=6(AIV0)]]其中
kernels=[aic aiv0 aiv1]是该任务三个子核槽位的 kernel id、cores=[...]是 正在执行它的物理核号与子核类型——由此可直接把"卡住的任务"映射到"具体哪个 kernel、在哪些核上"。设置日志级别的方式:-
Worker 直跑:在调用
worker.init()之前将 simpler logger 提级——logging.getLogger("simpler").setLevel("V0")。级别在init()时被快照并下发 设备,因此必须在init()之前设置(run()之间再改不会生效)。 -
pytest / scene test:等价地传
--log-level v0。
V0 是最详细级(
V0最详细、V9最精简、默认V5),会一并打开派发路径的其他 诊断行。设备日志默认落在共享目录~/ascend/log/debug/device-<id>/,多进程混写、 难以辨认;请先用ASCEND_PROCESS_LOG_PATH环境变量把它重定向到本次运行专属的 目录(该目录须预先创建),再去读取——详见第 5 节第 4 步与.claude/rules/running-onboard.md的 "Device logs" 一节。 -
Worker 直跑:在调用
-
通过 counters 判断:
completed=c/t(已完成 / 总数)以及running=r ready=k waiting=w三个计数,实际上正是子分类的判定依据,其优先级为running > ready > waiting > 编排未完成。例如,running=1必然对应 S1;running=0且ready>0对应 S3。
-
触发条件:异步完成条件非法——例如 COUNTER 类型的
counter_addr为空、 completion_type 非法,或 SDMA 事件记录无效。其本质是 async 完成机制注册有误。 -
排查步骤:检查 kernel 中
register_completion_condition及相关 async API 的 参数,尤其是计数器地址与 completion type 是否填写正确。
-
触发条件:异步等待列表被填满(在途 async 完成数 ≥ 上限 64,即
MAX_ASYNC_WAITS)。 通常是注册了过多 async 完成,却未及时被 poll 或回收。 -
排查步骤:减少单任务的在途 async 完成数;确认消费侧确实在轮询并推进这些完成。
另需注意:AICore 侧单任务的 condition 上限为 64(
MAX_COMPLETIONS_PER_TASK), 超出该上限时会先报出错误码 102。
- 触发条件:从 AICore 收到了非法的 async 完成消息类型(既非 CONDITION,亦非 TASK_NORMAL_DONE)。
- 正常路径下几乎不可达:正常情况下会被错误码 102(condition 数超上限)提前拦截, 错误码 103 仅在 slab 被破坏或消息内部畸形时才会出现。若确实命中,通常意味着运行时 内部缺陷,请上报并保留设备日志。
-
获取异常码:查看
RuntimeError: run failed with code <rc>。- sim 环境:
<rc> = -N,直接对照第 2 节的表格。 - onboard 环境:
<rc>为507018时,不应就此止步,需继续向下排查。
- sim 环境:
-
获取自诊断日志行:检索 host 日志
grep -E "orch_error_code=|sched_error_code=|sub_class=" <运行日志>,随后:-
orch_error_code=N(N∈1..11)→ 参见第 3 节。 -
sched_error_code=100→ 读取sub_class=一行,对照第 4 节的 S1–S5 表格。 -
sched_error_code=101/102/103→ 参见第 4 节对应小节。
-
-
区分"编排问题"与"运行时内部问题":
- 错误码 1、3、4、5、7、8、9 及 100/S1 → 大概率属于用户的编排或 kernel, 按对应小节调整。其中 错误码 1 / 2 / 4 属于资源容量问题,可用 scope_stats DFX 定位瓶颈资源与所在 scope(见第 3 节"用 scope_stats 定位资源瓶颈"),再据实 调整 ring 容量。
- 错误码 2 → 属于配置(ring 大小)问题,或单任务占用过大。
- 错误码 10、11、103 及 100/S4、100/S5、100/unknown → 属于运行时内部问题, 请保留设备日志并上报。
-
需要更深层的设备真相时:设备日志(AICPU / CCECPU log)才是 ground truth。
在 onboard 环境下,请务必先将设备日志重定向到本次运行专属的目录,避免在共享目录
中依据 pid 或时间戳猜测文件——具体参见
.claude/rules/running-onboard.md中 "Device logs" 一节(设置ASCEND_PROCESS_LOG_PATH),以及该文件中关于507018的机制分类表。
- 错误码定义:
src/{arch}/runtime/tensormap_and_ringbuffer/common/pto_runtime_status.h - host 自诊断打印:
.../host/runtime_maker.cpp(validate_runtime_impl) - 子分类逻辑:
.../runtime/scheduler/scheduler_cold_path.cpp(classify_stall_reason) - 端到端负向测试(各错误码的实际触发样例):
tests/st/runtime_fatal_codes/ - onboard
507018机制分类与设备日志定位:.claude/rules/running-onboard.md - 本地超时默认值:
docs/troubleshooting/local-timeout-defaults.md