Skip to content

Fix: avoid remote teardown after fatal AICore errors - #1664

Open
sunkaixuan2018 wants to merge 1 commit into
hw-native-sys:mainfrom
sunkaixuan2018:skx/fix-issue-1425-teardown
Open

Fix: avoid remote teardown after fatal AICore errors#1664
sunkaixuan2018 wants to merge 1 commit into
hw-native-sys:mainfrom
sunkaixuan2018:skx/fix-issue-1425-teardown

Conversation

@sunkaixuan2018

@sunkaixuan2018 sunkaixuan2018 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Preserve the existing stream error-code and DFX contract for ordinary a2a3/a5 runs; apply ACL_STOP_ON_FAILURE only to the a2a3 per-run stream when a real SDMA workspace is provisioned.
  • Publish fatal state before completion so every teardown thread observes the poisoned-device state, with one thread owning the emergency shutdown.
  • On fatal teardown, stop host collectors, force-reset the card, and abandon device-backed handles without destroy/free/unregister calls that can block in DEV_RUNNING_DOWN.
  • Keep the full resource-release path unchanged for healthy teardown, and add focused regression coverage for ordering, reset budget, and no-device-touch cleanup.

The a2a3 SDMA fatal path waits max(10 seconds, configured op-execute timeout + 5 seconds) before reset. This is an empirical workaround for CANN 9.0.0 and driver 26.0.rc1, not a portable runtime completion fence; it is deliberately limited to workers that actually provision the SDMA workspace.

Reset budget follows the stream population

force_reset_device() drains the card before resetting it and returns 0 only when its post-reset probe confirms a usable generation, so a second pass runs against a settled card and can recover a poison the first could not. Ordinary poison therefore keeps a bounded three-attempt budget on both a2a3 and a5.

A Worker holding the 48 CP-process SDMA streams is the exception and gets a single attempt: there a non-confirming reset has already blocked on the driver's 150/300-second remote-event timeout, so a retry multiplies that wait without adding a completion condition.

Testing

  • Passed all pre-commit checks; Windows skipped clang-tidy because the onboard sources require Linux/CANN headers, then the changed files passed the repository clang-tidy runner on myserver.
  • Built all a2a3/a5 host_build_graph and tensormap_and_ringbuffer runtimes with CANN 9.0.0.
  • Passed the non-hardware C++ unit tests, including the fatal publication, reset-budget, arena-abandon, and allocator-abandon tests.
  • Passed a2a3 hardware AICore-timeout regression with DFX enabled:
    • ordinary non-SDMA worker: passed in 8.4 s and retained the non-507015 error contract;
    • real-SDMA worker: passed in 19.2 s, including the scoped 10 s workaround, with bounded Worker.close().
  • Passed a2a3 hardware diagnostic-contract cases for scheduler timeout (sub_class=S1) and tensor wait timeout (orch_error_code=8).
  • a5 hardware coverage is provided by the PR's st-onboard-a5 CI job; local myserver hardware is a2a3, while the a5 runtime build and a5 C++ unit suite passed there.

Scope

Mitigates #1425 — this bounds fatal teardown to roughly 10–30 s and keeps ordinary Workers on the fast path. It does not restore SDMA-Worker fault recovery to ~0.3 s: the root cause (CANN exposes no retirement fence for CP-process SDMA streams) is unresolved and remains deferred pending a CANN runtime-and-driver fix, so #1425 should stay open against that dependency.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 970084de-57d1-42ef-8f82-d8adfd453bac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The PR adds stop-on-failure stream creation and centralizes stream setup. It adds non-blocking AICore exit signaling and guards repeated emergency shutdown. Fatal device teardown now retries force reset, abandons host-side resource handles, and skips unsafe per-resource runtime destruction.

Fatal device recovery and stream lifecycle

Layer / File(s) Summary
Stop-on-failure stream lifecycle
src/common/platform/..., src/a2a3/platform/onboard/host/device_runner.*, tests/ut/cpp/hierarchical/test_run_stream_slots.cpp
Stream creation applies ACL_STOP_ON_FAILURE. Failed configuration destroys the stream. Abandoned stream slots clear handles without destroying streams.
Fatal AICore shutdown signaling
src/a2a3/platform/{include,shared}/aicpu/platform_regs.*, src/a2a3/runtime/.../scheduler/*
Emergency shutdown latches a state, signals initialized AICores without acknowledgement waits, and resets the state during deinitialization.
Abandoned fatal-device teardown
src/common/aicpu_loader/host/*, src/common/platform/include/host/*, src/common/platform/onboard/host/device_runner_base.*, src/a2a3/platform/onboard/host/device_runner.cpp
Fatal finalization retries force reset up to three times, abandons loader, allocator, kernel-argument, stream, and DMA ownership, and preserves normal teardown for usable devices.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeviceRunner
  participant Scheduler
  participant AICore
  participant DeviceResources
  DeviceRunner->>Scheduler: begin emergency shutdown
  Scheduler->>AICore: write exit signal without acknowledgement wait
  DeviceRunner->>DeviceRunner: wait for device-down handoff
  DeviceRunner->>DeviceResources: force reset and confirm recovery
  DeviceRunner->>DeviceResources: abandon invalidated handles
  DeviceRunner->>DeviceRunner: clear unusable device state
Loading

Possibly related PRs

  • hw-native-sys/simpler#1650: Changes DeviceRunner, DeviceRunnerBase, and RunStreamSlots for a different stream lifecycle concern.

Poem

A rabbit saw the fault lights glow,
Then signaled cores to quit and go.
Streams stayed safe, handles cleared,
Reset retries disappeared.
“Clean recovery!” the rabbit cheered.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #1425 by avoiding prolonged poisoned-device teardown while preserving healthy teardown and recovery behavior.
Out of Scope Changes check ✅ Passed The changes are focused on fatal AICore teardown, device recovery, resource abandonment, and supporting tests.
Title check ✅ Passed The title clearly summarizes the main change: preventing remote teardown after fatal AICore errors.
Description check ✅ Passed The description directly explains fatal teardown, device reset, resource abandonment, timeout behavior, and validation for the changeset.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/common/platform/onboard/host/device_runner_base.cpp (2)

116-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the duplicated stop-on-failure stream creation logic.

Both functions perform the same three steps: rtStreamCreate, aclrtSetStreamFailureMode(..., ACL_STOP_ON_FAILURE), and destroy-and-clear the handle on configuration failure. Keeping two copies risks the two diverging on a future fix (for example, an added retry or a changed failure mode).

  • src/common/platform/onboard/host/device_runner_base.cpp#L116-L133: keep create_stop_on_failure_stream, but change its signature to take void **stream (or a small template) so DeviceRunner::create_run_stream can call it directly instead of reimplementing the same three steps.
  • src/a2a3/platform/onboard/host/device_runner.cpp#L531-L552: replace the body of create_run_stream with a call to the shared helper from device_runner_base.cpp, passing a "run" name for logging.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/platform/onboard/host/device_runner_base.cpp` around lines 116 -
133, Consolidate the duplicated stream setup by updating
create_stop_on_failure_stream in
src/common/platform/onboard/host/device_runner_base.cpp:116-133 to accept void
**stream (or an equivalent small template) while preserving creation,
ACL_STOP_ON_FAILURE configuration, and cleanup behavior. Replace
DeviceRunner::create_run_stream in
src/a2a3/platform/onboard/host/device_runner.cpp:531-552 with a direct call to
the shared helper using the "run" log name.

1174-1252: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fatal-teardown correctness relies on an unenforced call order.

In the abandon path, mem_alloc_.free() (Line 1200), free_tensor() (Line 1247), and mem_alloc_.finalize() (Line 1252) run unconditionally. They only avoid touching device memory because mem_alloc_.abandon_after_device_failure() (Line 1183) already cleared ptr_size_map_ earlier in the same function. Nothing in this loop or in MemoryAllocator prevents a later refactor from reordering these calls, which would silently reintroduce real rtFree() calls against a poisoned/reset device — the exact hang this PR removes.

Guard these calls explicitly with abandon_device_resources, the same way the stream and DMA-workspace releases above are already guarded, so the abandon behavior does not depend on call order.

🛡️ Proposed fix to make the abandon path order-independent
     for (auto &kv : chip_callable_buffers_) {
-        mem_alloc_.free(reinterpret_cast<void *>(kv.second.chip_dev));
+        if (!abandon_device_resources) {
+            mem_alloc_.free(reinterpret_cast<void *>(kv.second.chip_dev));
+        }
         if (!abandon_device_resources) {
             LOG_DEBUG(
                 "Freed chip callable buffer: chip_dev=0x%lx, size=%zu, hash=0x%lx", kv.second.chip_dev,
                 kv.second.total_size, kv.first
             );
         }
     }
@@
     if (device_wall_dev_ptr_ != nullptr) {
-        free_tensor(device_wall_dev_ptr_);
+        if (!abandon_device_resources) {
+            free_tensor(device_wall_dev_ptr_);
+        }
         device_wall_dev_ptr_ = nullptr;
     }
-
-    // Free all remaining allocations (including handshake buffer and binGmAddr)
-    mem_alloc_.finalize();
+    // Free all remaining allocations (including handshake buffer and binGmAddr).
+    // Skipped in the abandon path: ptr_size_map_ was already cleared above.
+    if (!abandon_device_resources) {
+        mem_alloc_.finalize();
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/platform/onboard/host/device_runner_base.cpp` around lines 1174 -
1252, Guard the device-memory cleanup calls in the teardown flow with
abandon_device_resources: skip chip buffer mem_alloc_.free(), device_wall
free_tensor(), and mem_alloc_.finalize() when abandoning device resources, while
retaining them for normal teardown. Update the related cleanup loops around
chip_callable_buffers_ and device_wall_dev_ptr_ so fatal teardown cannot issue
device frees regardless of call order.
src/a2a3/platform/onboard/host/device_runner.cpp (1)

1086-1119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dead code: this poisoned-device retry block is unreachable.

The new fatal branch at Lines 959-1029 always returns whenever device_unusable_ is true at function entry, since nothing in this function sets device_unusable_ back to true between the two checks (it is only mutated by recover_device_or_mark_unusable(), called from run()). So the if (device_unusable_) { ... } block at Lines 1087-1119 — which retries force_reset_device() up to kMaxResetAttempts times — can never execute; it duplicates the retry loop already added at Lines 984-999.

Remove this block (and its now-always-true reset_rc == 0 follow-up at Lines 1126-1128 can be simplified accordingly), so a future reader does not assume this retry path still runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a3/platform/onboard/host/device_runner.cpp` around lines 1086 - 1119,
Remove the unreachable device_unusable_ force-reset retry block from finalize,
including its kMaxResetAttempts loop and related logging. Simplify the
subsequent reset_rc == 0 follow-up because the retry is already handled by the
fatal branch earlier in finalize; preserve the existing recovery behavior from
that branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/a2a3/platform/onboard/host/device_runner.cpp`:
- Around line 1086-1119: Remove the unreachable device_unusable_ force-reset
retry block from finalize, including its kMaxResetAttempts loop and related
logging. Simplify the subsequent reset_rc == 0 follow-up because the retry is
already handled by the fatal branch earlier in finalize; preserve the existing
recovery behavior from that branch.

In `@src/common/platform/onboard/host/device_runner_base.cpp`:
- Around line 116-133: Consolidate the duplicated stream setup by updating
create_stop_on_failure_stream in
src/common/platform/onboard/host/device_runner_base.cpp:116-133 to accept void
**stream (or an equivalent small template) while preserving creation,
ACL_STOP_ON_FAILURE configuration, and cleanup behavior. Replace
DeviceRunner::create_run_stream in
src/a2a3/platform/onboard/host/device_runner.cpp:531-552 with a direct call to
the shared helper using the "run" log name.
- Around line 1174-1252: Guard the device-memory cleanup calls in the teardown
flow with abandon_device_resources: skip chip buffer mem_alloc_.free(),
device_wall free_tensor(), and mem_alloc_.finalize() when abandoning device
resources, while retaining them for normal teardown. Update the related cleanup
loops around chip_callable_buffers_ and device_wall_dev_ptr_ so fatal teardown
cannot issue device frees regardless of call order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1779fe14-fdfc-43ca-9328-dda944db23ee

📥 Commits

Reviewing files that changed from the base of the PR and between b5261a7 and ce65e3d.

📒 Files selected for processing (14)
  • src/a2a3/platform/include/aicpu/platform_regs.h
  • src/a2a3/platform/onboard/host/device_runner.cpp
  • src/a2a3/platform/onboard/host/device_runner.h
  • src/a2a3/platform/shared/aicpu/platform_regs.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h
  • src/common/aicpu_loader/host/load_aicpu_op.cpp
  • src/common/aicpu_loader/host/load_aicpu_op.h
  • src/common/platform/include/host/memory_allocator.h
  • src/common/platform/include/host/run_stream_slots.h
  • src/common/platform/onboard/host/device_runner_base.cpp
  • src/common/platform/onboard/host/device_runner_base.h
  • src/common/platform/onboard/host/device_runner_helpers.h
  • tests/ut/cpp/hierarchical/test_run_stream_slots.cpp

@sunkaixuan2018
sunkaixuan2018 force-pushed the skx/fix-issue-1425-teardown branch 6 times, most recently from aab86b1 to ac6781f Compare August 4, 2026 03:14
@ChaoZheng109

Copy link
Copy Markdown
Collaborator

审查意见 —— 合并前请处理

整体方向、机制与测试覆盖都很扎实,投资记录(investigation)写得尤其好。合并前有四点需要澄清:

1. Fixes #1425 会自动关闭一个本 PR 只"收敛"而非"修复"的 issue。 #1425 要求把 SDMA Worker 的故障恢复恢复到 ~0.3s;本 PR 交付的是有界的 ~10–30s 拆卸,加上普通 Worker 保持快路径。根因(CANN 对 CP-process SDMA 流没有退休栅栏)仍未解决——投资记录自己也写明完整恢复"deferred pending a CANN runtime-and-driver fix"。建议把 Fixes 改为 Contains/Mitigates #1425 并保留 issue 开启(挂上残留的 CANN 依赖),或在 PR 中明确说明这只是收敛方案。

2. 普通(非 SDMA)中毒路径的强制复位从最多 3 次降为 1 次——请确认对普通分支是刻意的。 记录在案的单次复位理由(重试会叠加 ~300s remote-event 超时)是 SDMA 专属的;普通故障下 force_reset_device() 失败并无此代价,而被删掉的 3 次重试有独立理由("poison occasionally needs a drain-then-reset cycle;5/5 验证")。我追查了上层兜底:覆盖率不会丢(#1110 dispatcher 会在全新子进程里重跑被 poison-skip 的类),在 a2a3 上,原本能靠第 2/3 次就地复位救回、留在同子进程继续的卡,现在更容易被隔离、退到"整进程重启"这条更慢的恢复路径(对 a5 无影响,a5 本就无进程内恢复)。请确认:普通分支收敛到单次复位是刻意权衡过的,并接受 a2a3 就地恢复变弱的代价;否则建议只在 SDMA 分支用单次、非 SDMA 分支保留多次重试。

3. 针对 #1425 的核心回归测试 test_sdma_worker_aicore_fault_teardown_is_bounded@pytest.mark.sdma 门控、默认不选中,而本 PR 未在 CI 中接入 -m sdma 也就是说,验证 10s sleep + 有界 close 的那个测试在常规 CI 里是休眠的(普通 enable_sdma=False 变体确实在跑)。请确认已有某个 CI job 在独占设备上执行 -m sdma,否则这条回归护栏永远不会运行。

4. a2a3/a5 双树不对称。 a2a3 把 device_unusable_ 升级为 std::atomic<bool>(acquire/release),a5 仍是普通 bool;但两者的 can_accept_run() 都从 c_api_shared.cpp 读取该标志(可能与 run() 的写入方不在同一线程)。按 codestyle 规则 10,近重复的双树应保持同步——请确认 a5 确实无需原子化,或在 a5 一并应用。此外 a5 并未获得 publish_fatal_shutdown 发布栅栏(它只存在于 a2a3 的 tmr 调度器);请确认这是 a5 调度模型使然、而非遗漏。

次要(可考虑):attempt_fatal_reset_once 是个只调用一次 callable 的模板包装,除了标注"一次"的语义外无逻辑。保持现状也可,但直接调用 force_reset_device() 再加一条注释是等价的。

@sunkaixuan2018
sunkaixuan2018 force-pushed the skx/fix-issue-1425-teardown branch from ac6781f to cfd17c4 Compare August 4, 2026 10:37
@sunkaixuan2018

Copy link
Copy Markdown
Contributor Author

@ChaoZheng109 感谢细致的审查 —— 第 2 点直接命中了本 PR 的一个真实回归,CI 也独立印证了它。逐条回复:

1. Fixes #1425 → 已改为 Mitigates 你的判断正确:本 PR 交付的是有界(~10–30s)收敛,根因(CANN 对 CP-process SDMA 流没有退休栅栏)仍未解决。PR 描述已去掉自动关闭关键字,新增 "Scope" 段落写明这一点并说明 #1425 应挂着残留的 CANN 依赖保持开启。

2. 普通路径复位 3→1 是回归,已修复 —— 并且它正是本次 CI 失败的原因。

不是刻意权衡,是我漏掉的。你指出的"a2a3 就地恢复变弱"在这次 st-onboard-a2a3 上已经真实发生了。失败的 5 个 task_timing 用例与故意注入故障的用例是同设备前后相邻关系:

设备 前一个用例(故意毒化) 后一个用例 结果
2 dep_pool_overflow test_distinct_slots_emit_markers FAIL 507018
7 flow_control_deadlock test_duplicate_slot_merges_window FAIL
5 heap_ring_deadlock test_hbg_distinct_slots_emit_markers FAIL 507046
3 scope_deadlock test_mix_task_aggregates_across_subtasks FAIL
6 require_sync_start_invalid test_spmd_task_aggregates_across_threads FAIL

即:单次复位未能确认干净的卡,把毒化状态留给了下一个进程。task_timing 本身没问题(#1681#1675 同一 job 通过)。

修复按你的建议执行:非 SDMA 分支恢复 3 次有界重试(a2a3 与 a5 同步),仅 SDMA 分支保留单次。理由现在写在代码与 investigation 里:force_reset_device() 每次调用都会先 drain 再 reset,并以 post-reset 探测(stream create/destroy + rtMalloc/rtFree)确认,所以第二次是在已沉降的卡上运行 —— 确实存在新的完成条件,原先"重试无新完成条件"的说法只对持有 48 条 CP-process 流的 SDMA 卡成立。

3. -m sdma 在 CI 里是接入了的。 .github/workflows/ci.yml 有独立的 "SDMA pytest (a2a3)" step:

SDMA_TESTS="examples tests/st -m sdma"

按 marker 选择且覆盖 tests/st,所以 tests/st/aicore_op_timeout/ 里新增的 test_sdma_worker_aicore_fault_teardown_is_bounded 会被它选中,在独占设备上跑。本次 run 的日志里看不到它,是因为前一个 sweep step 先失败导致 job 中止,该 step 未执行 —— 不是护栏休眠。第 2 点修好后应能看到它执行。

4. a5 原子化已补齐;publish_fatal_shutdown 的不对称确认是遗漏,但本 PR 不扩围。

  • device_unusable_ 在 a5 已改为 std::atomic<bool>(acquire/release),8 处读写全部同步,与 a2a3 一致。你的分析正确:can_accept_run() 的读取方与 run() 的写入方可能不同线程。

  • publish_fatal_shutdown 的缺失不是 a5 调度模型使然。我核对了四棵树,a2a3/host_build_grapha5/host_build_grapha5/tensormap_and_ringbuffer 三者的 emergency_shutdown 与 a2a3/tmr 修改前逐字节相同,都仍走等待应答的 platform_deinit_aicore_regs,因此同样存在该挂起风险。

    本 PR 只改了 [Performance] Provisioning the PTO-ISA async-SDMA workspace degrades AICore fault recovery ~1000x (0.3s → ~306s) on a2a3 #1425 复现的 a2a3/tmr。扩到另外三棵树需要同时给 a5 增加 platform_signal_aicore_exit,并改动三个当前工作正常的调度器,而我这边没有 a5 硬件可验证。倾向于单独开 issue 跟进而不是在这个已经因行为变更而红过一次的 PR 里扩围 —— 如果你认为应该一并落地,我照办。

次要项: attempt_fatal_reset_once 已被 attempt_fatal_reset(reset, max_attempts) 取代,现在承载真实的重试预算逻辑,不再是空包装。

@sunkaixuan2018

Copy link
Copy Markdown
Contributor Author

@coderabbitai Thanks — all three nitpicks were reviewed against ce65e3d2; each was already resolved by a later commit on the branch, verified against HEAD:

  1. Consolidate duplicated stop-on-failure stream creation — no duplication exists. aclrtSetStreamFailureMode / ACL_STOP_ON_FAILURE appear at exactly one site (src/a2a3/platform/onboard/host/device_runner.cpp:562), and there is no create_stop_on_failure_stream in device_runner_base.cpp to share with. The mode is applied only when the Worker actually provisioned an SDMA workspace, which is a2a3-specific, so hoisting it to the common base would misplace it.

  2. Guard the abandon path with abandon_device_resources — already guarded explicitly, not relying on call order. In finalize_common_impl, the chip_callable_buffers_ loop, free_tensor(device_wall_dev_ptr_), and mem_alloc_.finalize() each sit behind if (!abandon_device_resources), which is the shape your proposed diff asked for.

  3. Dead kMaxResetAttempts retry block in finalize() — the block was removed; grep -rn kMaxResetAttempts src/ returned nothing at the reviewed commit.

Separately, the reset budget did change in the latest push, in the opposite direction from that dead-code removal: ordinary (non-SDMA) poison now keeps a bounded three-attempt budget via attempt_fatal_reset(reset, max_attempts), because force_reset_device() drains before resetting and confirms with a post-reset probe, so a retry does add a completion condition. Only an SDMA-provisioned card is held to a single attempt. Collapsing both onto one attempt was a real regression — it left fault-injected cards poisoned for the next process and failed five task_timing tests in st-onboard-a2a3.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== stop-on-failure configuration sites =='
rg -n -C 4 'aclrtSetStreamFailureMode|ACL_STOP_ON_FAILURE|create_stop_on_failure_stream' \
  src/a2a3/platform/onboard/host/device_runner.cpp \
  src/common/platform/onboard/host/device_runner_base.cpp \
  src/common/platform/onboard/host/device_runner_base.h || true

printf '%s\n' '== abandon-device-resource guards =='
rg -n -C 8 'finalize_common_impl|abandon_device_resources|chip_callable_buffers_|device_wall_dev_ptr_|mem_alloc_\.finalize' \
  src/common/platform/onboard/host/device_runner_base.cpp

printf '%s\n' '== reset attempt symbols and call sites =='
rg -n -C 6 'kMaxResetAttempts|attempt_fatal_reset|force_reset_device|sdma' \
  src/a2a3/platform/onboard/host/device_runner.cpp \
  src/a2a3/platform/onboard/host/device_runner.h \
  src/a5/platform/onboard/host/device_runner.cpp \
  src/a5/platform/onboard/host/device_runner.h 2>/dev/null || true

Length of output: 58058


@sunkaixuan2018 Verified at the current PR HEAD.

  1. aclrtSetStreamFailureMode(stream, ACL_STOP_ON_FAILURE) has one call site. The call is conditional on dma_workspace_handle_ != nullptr. The common base does not contain an equivalent helper.

  2. DeviceRunnerBase::finalize_common_impl(bool) explicitly guards the chip-callable cleanup, free_tensor(device_wall_dev_ptr_), and mem_alloc_.finalize() with !abandon_device_resources.

  3. The obsolete kMaxResetAttempts block is absent. Both a2a3 and a5 use attempt_fatal_reset. SDMA-provisioned Workers use one reset attempt. Non-SDMA Workers use the three-attempt budget. force_reset_device() includes the post-reset stream and allocation probes, so a retry has a defined recovery condition.

These three findings are resolved. 🐇


✏️ Learnings added
Learnt from: sunkaixuan2018
URL: https://github.com/hw-native-sys/simpler/pull/1664

Timestamp: 2026-08-04T10:39:00.235Z
Learning: In `src/a2a3/platform/onboard/host/device_runner.cpp` and `src/a5/platform/onboard/host/device_runner.cpp`, fatal reset uses `attempt_fatal_reset`: SDMA-provisioned Workers use one attempt because repeated resets can multiply the CANN remote-event timeout, while non-SDMA Workers retain three attempts because `force_reset_device()` drains and then verifies recovery with stream create/destroy and HBM allocation/free probes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@sunkaixuan2018
sunkaixuan2018 force-pushed the skx/fix-issue-1425-teardown branch 3 times, most recently from 164ad18 to cdfba86 Compare August 4, 2026 13:59
@sunkaixuan2018

Copy link
Copy Markdown
Contributor Author

Follow-up: the first fix was necessary but not sufficient. Second root cause found and fixed.

Where it stood. Restoring the three-attempt reset budget took st-onboard-a2a3 from 5 task_timing failures to 3 — real progress, but not green.

What the remaining 3 were. The surviving failures were on devices whose preceding test was a pool/allocator-deadlock fault injection (dep_pool_overflow and heap_ring_deadlock failed in both runs). Two things in the logs ruled out the reset budget as the remaining cause:

  • Only 3 fatal teardowns ran in the whole job, and they belonged to the failing task_timing tests, not to their predecessors. The fault-injection tests recover in place and never take the force-reset path at all.
  • Every one of those force resets confirmed clean on the first attempt (aclrtResetDeviceForce(N) established a usable device generation (probe confirmed)), so no amount of retry budget was going to matter.

The actual cause. This PR made emergency shutdown fire-and-forget. platform_deinit_aicore_regs did three things — signal exit, wait for the core to acknowledge, then quiesce its register block (dispatch register back to idle, fast path closed) — and the PR kept only the signal. The per-thread shutdown() path also no-ops once fatal shutdown starts, so on a fatal run nothing waited for the cores any more. The AICPU op returned while AICores were still running, with the fast path left open, and the card stayed poisoned past the host's device reset. The next process on that device then failed at launch with 507018/507046.

The control that isolates it: st-onboard-a5 passes with the identical host-side teardown changes. a5's schedulers were untouched and kept their acknowledgement wait; only the a2a3 tensormap_and_ringbuffer scheduler lost it. Four sibling PRs also pass this job today, so it is not a pre-existing flake.

The fix. Emergency shutdown now signals every handshake'd core first, then joins them — rather than signal-and-wait one core at a time, which is what the original serial platform_deinit_aicore_regs loop did. Cores drain concurrently, a dead core's timeout no longer serializes behind the cores ahead of it, and each core's register block is quiesced once it confirms it stopped. platform_finish_aicore_exit() is split out of platform_deinit_aicore_regs() for this; the latter is now signal + finish and behaves exactly as before for its existing callers.

Worth being explicit that this does not reintroduce the #1425 hang: the acknowledgement is an on-device poll of the core's COND register, bounded by the variant deinit timeout. It is not a host or remote operation and has nothing to do with the CANN CP-process SDMA stream teardown that the rest of this PR contains.

The publish_fatal_shutdown fence is unchanged and still does its job — it keeps a healthy per-thread shutdown() from racing the emergency broadcast over the same cores.

@sunkaixuan2018
sunkaixuan2018 force-pushed the skx/fix-issue-1425-teardown branch 3 times, most recently from 447fac5 to 07c8032 Compare August 4, 2026 15:50
A fatal AICore error left teardown walking device resources that the
poisoned card could no longer retire, so Worker.close() blocked in the
driver's remote-event timeout.

Fatal teardown now stops host collectors, force-resets the card, and then
forgets the failed generation's handles without per-resource destroy, free,
or unregister calls. cleanup_active_run() takes the same branch, so an
enqueue rollback or drain on a poisoned card drops host-side ownership
instead of issuing frees and stream destroys the driver cannot retire.
Healthy teardown keeps the full release path.

The reset budget follows the stream population. force_reset_device() drains
before it resets and returns 0 only when its post-reset probe confirms the
card, so a second pass runs against a settled card and can recover a poison
the first could not; ordinary poison therefore keeps a bounded three-attempt
budget on both a2a3 and a5. A Worker holding the 48 CP-process SDMA streams
gets a single attempt, because there a non-confirming reset has already
blocked on the driver's 150/300-second remote-event timeout and a retry only
multiplies that wait. Collapsing both populations onto one attempt leaves a
fault-injected card poisoned for the next process that lands on it.

Scheduler threads publish fatal state before completion, so no thread enters
the healthy per-thread shutdown path for a fatal run and races the emergency
broadcast for the same cores, and one thread owns that broadcast.

Emergency shutdown signals every handshake'd core before joining any of them,
instead of signalling and waiting one core at a time, and the join takes one
deadline for the whole group rather than one timeout per core. The join is
load-bearing: returning while cores still run leaves the card poisoned past
the host's device reset, so the next process on that device fails at launch.
The shared deadline is what keeps it affordable — the onboard deinit timeout
is 1 second, so a per-core deadline would cost a second per unresponsive core
on a fatal run where every core is dead. It is an on-device register poll, so
it adds no host or remote operation.

ACL_STOP_ON_FAILURE applies only to an a2a3 run stream whose Worker actually
provisioned the SDMA workspace. Ordinary a2a3 and all a5 streams keep their
existing error and diagnostic contract, and take no added handoff delay.

Mitigates hw-native-sys#1425.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sunkaixuan2018
sunkaixuan2018 force-pushed the skx/fix-issue-1425-teardown branch from 07c8032 to 90eafb5 Compare August 4, 2026 16:10
@sunkaixuan2018

Copy link
Copy Markdown
Contributor Author

CI is fully green — all 18 checks pass, including both hardware jobs (st-onboard-a2a3 8m0s, st-onboard-a5 10m52s) and ut-a2a3 / ut-a5.

st-onboard-a2a3 was the job that had been red since the first push. Getting it green took three distinct fixes, each found from a different piece of evidence:

Failures Cause
5 Reset budget collapsed from 3 attempts to 1 on the ordinary poison path — the regression @ChaoZheng109 predicted in review point 2.
3 Emergency shutdown dropped the AICore exit acknowledgement, so the AICPU op returned with cores still running and the fast path open, leaving the card poisoned past the host device reset.
1 Restoring the join cost one deinit timeout per core (1s onboard), pushing an AICore-timeout run past its 10s budget. Fixed by sharing one deadline across the whole core group.
1 Rebase fallout: CallConfig.enable_l2_swimlane was renamed to enable_chip_swimlane by #1681. The test file merged cleanly but was semantically stale.
0

The last run before this one finished 12-green / 3-red, where all three red jobs failed on infrastructure rather than code — Cloning pto-isa ... timed out, Could not find a version that satisfies the requirement scikit-build-core (PyPI unreachable), and a failed Checkout target. A single retrigger on an identical tree cleared all three, which confirms they were transient.

The branch has also been rebased onto current main, which required resolving 13 conflicts against #1683 (enqueue/drain split) and #1681 (layered naming). Two notes on that:

  • Refactor: split device enqueue from completion drain #1683 moved per-run cleanup into cleanup_active_run(), so the poisoned-card guarding was re-applied there. Without it, a fatal enqueue rollback or drain would have gone back to issuing the frees and stream destroys this PR exists to avoid.
  • device_unusable_ is now atomic on a5 upstream, so review point 4's atomicity half is resolved by main rather than by this PR, and the claim has been dropped from the commit message.

Still open for your call, unchanged from the earlier comment: whether to fold the emergency-shutdown fix into the three sibling scheduler trees (a2a3/host_build_graph, a5/host_build_graph, a5/tensormap_and_ringbuffer), which still carry the byte-identical acknowledgement-waiting emergency_shutdown and would need platform_signal_aicore_exit / platform_finish_aicore_exit added to a5's platform_regs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants