Fix: cap torch threads for goldens, and put qwen back in the scene-test sweep - #1601
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe Qwen3-14B decode test now separates functional PR validation from full golden comparison. CI runs the functional case, while a new scheduled or manually triggered nightly workflow runs numeric validation on self-hosted a2a3 hardware. Documentation records the execution split and measured costs. ChangesQwen3-14B golden gating
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant PullRequest
participant CI
participant Nightly
participant Qwen3Decode
PullRequest->>CI: trigger functional validation
CI->>Qwen3Decode: run skip_golden case
Nightly->>Qwen3Decode: run --manual only golden case
Qwen3Decode-->>Nightly: return numeric comparison result
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
.github/workflows/nightly.yml (1)
25-35: 🩺 Stability & Availability | 🔵 TrivialGive the nightly numeric gate an explicit failure route.
Add a failure notification/issue step, or document an owned alerting path. Otherwise this sole 40-layer numeric guard can fail silently despite the documentation requiring active monitoring.
🤖 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 @.github/workflows/nightly.yml around lines 25 - 35, Update the qwen-golden-a2a3 nightly job to provide an explicit failure route: add a notification or issue-creation step that runs when the job fails, or document an owned alerting path within the workflow. Ensure failures of this sole 40-layer numeric guard cannot remain unreported.
🤖 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.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 627-629: Measure golden-path overhead as an estimate rather than
an exact real cost: in .github/workflows/ci.yml lines 627-629, remove the
reversed subtraction or explicitly label it approximate; in
examples/a2a3/tensormap_and_ringbuffer/qwen3_14b_decode/README.md lines 201-203,
use the correct subtraction direction and avoid calling it the “real cost”; and
in .github/workflows/nightly.yml lines 62-66, describe the comparison as an
estimate unless adding direct compute_golden timing.
In `@docs/ci.md`:
- Around line 61-64: Update the design-principle statement in docs/ci.md to
qualify that --platform is the only hardware/platform filter, or explicitly
limit the claim to the PR matrix; keep the documented --manual only nightly path
and its behavior unchanged.
In `@examples/a2a3/tensormap_and_ringbuffer/qwen3_14b_decode/README.md`:
- Around line 174-177: Update the “StressBatch16Seq3500” entry in the README
table to qualify “every PR” as every relevant PR, specifically when a2a3_changed
and st_affected select the st-onboard-a2a3 scene-test job; leave the nightly
golden-case description unchanged.
---
Nitpick comments:
In @.github/workflows/nightly.yml:
- Around line 25-35: Update the qwen-golden-a2a3 nightly job to provide an
explicit failure route: add a notification or issue-creation step that runs when
the job fails, or document an owned alerting path within the workflow. Ensure
failures of this sole 40-layer numeric guard cannot remain unreported.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dfae4394-969e-42b3-b713-748de054b3d1
📒 Files selected for processing (6)
.claude/skills/testing/SKILL.md.github/workflows/ci.yml.github/workflows/nightly.ymldocs/ci.mdexamples/a2a3/tensormap_and_ringbuffer/qwen3_14b_decode/README.mdexamples/a2a3/tensormap_and_ringbuffer/qwen3_14b_decode/test_qwen3_14b_decode.py
75a2a4e to
84efa02
Compare
…st sweep
`qwen3_14b_decode` held a device for 406 s (ci run 30507320146, job
90761014912) while the device itself was busy for tens of milliseconds. It was
excluded from the general a2a3 sweep with a `--ignore` and given a step and a
1800 s budget of its own, because at that size it would have eaten the sweep's
600 s session budget — the budget that makes the sweep a hang detector.
Decomposed on this repo's a2a3 box, one device:
kernel compilation, 36 incores + 1 orchestration 59 s
generate_inputs, the 38 GiB fixture 13 s
compute_golden, 40 layers at 320 torch threads 359 s
------
~431 s vs 406 s measured
The golden was 80-85% of it, and none of that was arithmetic. Its reference
implementation walks 3584 small slice operations per layer, and torch sizes its
intra-op pool from the core count, so on a 320-core host every one of those
calls forks and joins hundreds of threads to move a few KiB:
threads 320 64 16 8 4 2 1
per layer 6.35s 1.31s 1.13s 1.07s 1.05s 1.59s 1.82s
Five to eight times the cost of the work, flat from 4 to 16, turning back up
below 4. Nothing in `simpler_setup/`, `conftest.py` or the workflows had ever
set a thread count, so every golden paid this on every many-core host — all
three under `simpler_setup/goldens/` are built from per-tile Python loops, qwen
is merely the densest.
So the cap goes where all of them pass, around the two `compute_golden` call
sites in `scene_test.py`, rather than into any one golden. It only ever lowers
the limit and restores the previous value on the way out, exception included.
Results are unaffected: the same fixture golden-computed at 320 and at 8 threads
is bit-identical across `out`, `k_cache` and `v_cache`.
That takes qwen's golden to ~38-43 s and the whole case to ~115 s, which is an
ordinary member of a sweep whose own wall is ~505 s across four dies. The
`--ignore`, the dedicated step and its 1800 s budget are all deleted; the case
is nothing special now and runs like every other one.
`.claude/skills/testing/SKILL.md` loses the `HEAVY_IGNORE` quarantine row and
the command that carried it, plus three `ci.yml:<line>` references that this
change would have invalidated anyway. The README's cost section is rewritten
around the measurements; it had reported "~5 min wall, ~49 s golden" from an
idle die, which is the 4-16-thread figure and does not describe CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`qwen3_14b_decode` was the one scene test excluded from the a2a3 sweep, with a `--ignore` and a step of its own. hw-native-sys#1601 removed the exclusion by capping torch threads around goldens; this records how that root cause was found, because none of the intuitive answers was right and the next person will reach for them in the same order. The measured decomposition of the 406 s the case held a device: compile 59 s, fixture 13 s, golden 359 s, device busy for tens of milliseconds. None of the golden's 359 s was arithmetic — its reference walks 3584 small slice operations per layer and torch sizes its intra-op pool from the core count, so a 320-core host paid 6.35 s per layer against 1.05 s at 4 threads. Four hypotheses were measured and killed first: that the 38 GiB fixture dominated (13 s — the 38 GiB is `torch.cat([w] * 40)` replication, only ~2 GB is generated), that compilation dominated (59 s, real but never the majority), that per-case worker startup dominated (a 4.1 s floor is real but 57 cases summing to 615 s cannot explain a 1482 s step), and that the 320-thread golden figure was a local artifact — half true and the most instructive, since summing the independently measured phases closes to 6% of the CI window, which only works if CI's golden is the slow one too. Also records the method, which was the hard part: per-step wall clock does not separate queueing from work, but the `npu-lock` timestamps in the job log do — that is how 52% of the job turned out to be waiting for dies. `task-submit` localises those messages, so the recipe matches their ASCII parts and reads the three line shapes off `)...`, `(pid=NNNN)` and neither, which keeps it working regardless of locale and inside the repo's English-only lint. And it records why golden caching, golden vectorisation, a nightly golden split and `skip_golden` were each considered and dropped, so those are not re-derived. Indexed in docs/investigations/README.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…1606) `qwen3_14b_decode` was the one scene test excluded from the a2a3 sweep, with a `--ignore` and a step of its own. #1601 removed the exclusion by capping torch threads around goldens; this records how that root cause was found, because none of the intuitive answers was right and the next person will reach for them in the same order. The measured decomposition of the 406 s the case held a device: compile 59 s, fixture 13 s, golden 359 s, device busy for tens of milliseconds. None of the golden's 359 s was arithmetic — its reference walks 3584 small slice operations per layer and torch sizes its intra-op pool from the core count, so a 320-core host paid 6.35 s per layer against 1.05 s at 4 threads. Four hypotheses were measured and killed first: that the 38 GiB fixture dominated (13 s — the 38 GiB is `torch.cat([w] * 40)` replication, only ~2 GB is generated), that compilation dominated (59 s, real but never the majority), that per-case worker startup dominated (a 4.1 s floor is real but 57 cases summing to 615 s cannot explain a 1482 s step), and that the 320-thread golden figure was a local artifact — half true and the most instructive, since summing the independently measured phases closes to 6% of the CI window, which only works if CI's golden is the slow one too. Also records the method, which was the hard part: per-step wall clock does not separate queueing from work, but the `npu-lock` timestamps in the job log do — that is how 52% of the job turned out to be waiting for dies. `task-submit` localises those messages, so the recipe matches their ASCII parts and reads the three line shapes off `)...`, `(pid=NNNN)` and neither, which keeps it working regardless of locale and inside the repo's English-only lint. And it records why golden caching, golden vectorisation, a nightly golden split and `skip_golden` were each considered and dropped, so those are not re-derived. Indexed in docs/investigations/README.md.
The SDMA quarantine was a pair of `--ignore=<path>` arguments. `pytest --ignore` on a path that no longer exists **exits 0 and says nothing** — measured — so moving either demo directory would have dropped the quarantine silently and landed both tests back beside the `aicore_op_timeout` fault-injection test. That collision is the whole hazard: provisioning the SDMA workspace creates 48 device-only STARS streams that sit in the device fault domain, so a later AICore fault on that device costs ~306 s instead of ~0.3 s (hw-native-sys#1425, contained in hw-native-sys#1406). `@pytest.mark.sdma` replaces the path matching, and carries the whole consequence rather than one arbitrary piece of it. One declaration now drives three things: - **The Worker is built with `enable_sdma=True`.** Both pytest construction sites in `conftest.py` and both standalone sites in `scene_test.py` read it, the latter from `cls.pytestmark` so `python test_x.py` behaves like the pytest path. - **The L2 Worker pool stops mixing capabilities.** The key gains the flag and reuse tests it, because an enable_sdma Worker holds its STARS streams for life: handing it to a test that did not ask for them would spread the teardown hazard to every later L2 case on that device. The existing same-device retire loop then performs the swap, so no new teardown path. - **SDMA sorts last.** `sort_key` gains a term, keyed off the marker rather than the class because the fault-injection tests are plain functions with no `_st_level`. Fault injection therefore always runs on a device that has never provisioned — which is what actually addresses the interaction, rather than merely quarantining it. With the capability expressible, `prefetch_async_demo` becomes an ordinary L2 `@scene_test` class. It was a hand-rolled Worker only because `CASES` had no channel to `Worker.__init__`, and it paid for that by forfeiting golden comparison, case parametrization, `--rounds`, `--case` and the dispatcher's device allocation. 160 lines become 89, and its verification — `torch.equal(out, src)` — is now a one-line `compute_golden`. The framework's default orchestration includes turned out to be sufficient, so no new passthrough was needed there. `sdma_async_completion_demo` keeps its hand-rolled L3 Worker for now and takes the marker for ordering and CI selection only; converting it means expressing a comm domain through `CASES`, which is a larger change and independent of this one. CI selects with `-m sdma` / `-m "not sdma"`, so the sweep and the dedicated step can no longer drift apart. **The dedicated step stays until hw-native-sys#1425 is fixed**: in-session ordering separates the cases, but a fault on a device that has already provisioned still costs minutes, so they must not share a device. Separately, `docs/capability-survey.md` carried four `ci.yml:<line>` citations, three already invalidated by this session's edits — `:607` lands on a blank line, `:884` on a `cmake --build`, `:628-643` on a dep_gen comment, after the qwen step was deleted in hw-native-sys#1601 and `detect-changes` reworked in hw-native-sys#1589 / hw-native-sys#1590 / hw-native-sys#1607. Line numbers into a file this active cannot be maintained, so all four become greppable anchors. `grep -rIn 'ci\.yml:[0-9]'` is now empty repo-wide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SDMA quarantine was a pair of `--ignore=<path>` arguments. `pytest --ignore` on a path that no longer exists **exits 0 and says nothing** — measured — so moving either demo directory would have dropped the quarantine silently and landed both tests back beside the `aicore_op_timeout` fault-injection test. That collision is the whole hazard: provisioning the SDMA workspace creates 48 device-only STARS streams that sit in the device fault domain, so a later AICore fault on that device costs ~306 s instead of ~0.3 s (hw-native-sys#1425, contained in hw-native-sys#1406). `@pytest.mark.sdma` replaces the path matching, and carries the whole consequence rather than one arbitrary piece of it. One declaration now drives three things: - **The Worker is built with `enable_sdma=True`.** Both pytest construction sites in `conftest.py` and both standalone sites in `scene_test.py` read it, the latter from `cls.pytestmark` so `python test_x.py` behaves like the pytest path. - **The L2 Worker pool stops mixing capabilities.** The key gains the flag and reuse tests it, because an enable_sdma Worker holds its STARS streams for life: handing it to a test that did not ask for them would spread the teardown hazard to every later L2 case on that device. The existing same-device retire loop then performs the swap, so no new teardown path. - **SDMA sorts last.** `sort_key` gains a term, keyed off the marker rather than the class because the fault-injection tests are plain functions with no `_st_level`. This is what will make merging the dedicated CI step back into the sweep safe once hw-native-sys#1425 is fixed; today `-m` already separates them, so it matters for local full runs. With the capability expressible, `prefetch_async_demo` becomes an ordinary L2 `@scene_test` class. It was a hand-rolled Worker only because `CASES` had no channel to `Worker.__init__`, and it paid for that by forfeiting golden comparison, case parametrization, `--rounds`, `--case` and the dispatcher's device allocation. 160 lines become 89, and its verification — `torch.equal(out, src)` — is now a one-line `compute_golden`. The framework's default orchestration includes turned out to be sufficient, so no new passthrough was needed there. The conversion also moves it from the Resource phase to the L2 phase, which runs after it, so it provisions on a device the fault-injection cases have already finished with. `sdma_async_completion_demo` keeps its hand-rolled L3 Worker for now and takes the marker for ordering and CI selection only; converting it means expressing a comm domain through `CASES`, which is a larger change and independent of this one. CI selects with `-m sdma` / `-m "not sdma"`, so the sweep and the dedicated step can no longer drift apart. **The dedicated step stays until hw-native-sys#1425 is fixed**: the two phases run their jobs in parallel across devices, so only separate device pools keep an SDMA provisioning away from a fault injection. `.claude/skills/testing/SKILL.md` moves with the mechanism throughout — not only the mirror-CI command, but the two instructions that told readers to extract `--ignore` sets from `ci.yml` and to grep it when a test passes alone and fails in the sweep. Both now name the marker; following the skill reproduces CI rather than a mechanism that no longer exists. Separately, `docs/capability-survey.md` carried four `ci.yml:<line>` citations, three already invalidated by this session's edits — `:607` lands on a blank line, `:884` on a `cmake --build`, `:628-643` on a dep_gen comment, after the qwen step was deleted in hw-native-sys#1601 and `detect-changes` reworked in hw-native-sys#1589 / hw-native-sys#1590 / hw-native-sys#1607. Line numbers into a file this active cannot be maintained, so all four become greppable anchors. `grep -rIn 'ci\.yml:[0-9]'` is now empty repo-wide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SDMA quarantine was a pair of `--ignore=<path>` arguments. `pytest --ignore` on a path that no longer exists **exits 0 and says nothing** — measured — so moving either demo directory would have dropped the quarantine silently and landed both tests back beside the `aicore_op_timeout` fault-injection test. That collision is the whole hazard: provisioning the SDMA workspace creates 48 device-only STARS streams that sit in the device fault domain, so a later AICore fault on that device costs ~306 s instead of ~0.3 s (hw-native-sys#1425, contained in hw-native-sys#1406). `@pytest.mark.sdma` replaces the path matching, and carries the whole consequence rather than one arbitrary piece of it. One declaration now drives three things: - **The Worker is built with `enable_sdma=True`.** Both pytest construction sites in `conftest.py` and both standalone sites in `scene_test.py` read it, the latter from `cls.pytestmark` so `python test_x.py` behaves like the pytest path. - **The L2 Worker pool stops mixing capabilities.** The key gains the flag and reuse tests it, because an enable_sdma Worker holds its STARS streams for life: handing it to a test that did not ask for them would spread the teardown hazard to every later L2 case on that device. The existing same-device retire loop then performs the swap, so no new teardown path. - **SDMA sorts last.** `sort_key` gains a term, keyed off the marker rather than the class because the fault-injection tests are plain functions with no `_st_level`. This is what will make merging the dedicated CI step back into the sweep safe once hw-native-sys#1425 is fixed; today `-m` already separates them, so it matters for local full runs. With the capability expressible, `prefetch_async_demo` becomes an ordinary L2 `@scene_test` class. It was a hand-rolled Worker only because `CASES` had no channel to `Worker.__init__`, and it paid for that by forfeiting golden comparison, case parametrization, `--rounds`, `--case` and the dispatcher's device allocation. 160 lines become 89, and its verification — `torch.equal(out, src)` — is now a one-line `compute_golden`. The framework's default orchestration includes turned out to be sufficient, so no new passthrough was needed there. The conversion also moves it from the Resource phase to the L2 phase, which runs after it, so it provisions on a device the fault-injection cases have already finished with. `sdma_async_completion_demo` keeps its hand-rolled L3 Worker for now and takes the marker for ordering and CI selection only; converting it means expressing a comm domain through `CASES`, which is a larger change and independent of this one. CI selects with `-m sdma` / `-m "not sdma"`, so the sweep and the dedicated step can no longer drift apart. **The dedicated step stays until hw-native-sys#1425 is fixed**: the two phases run their jobs in parallel across devices, so only separate device pools keep an SDMA provisioning away from a fault injection. `.claude/skills/testing/SKILL.md` moves with the mechanism throughout — not only the mirror-CI command, but the two instructions that told readers to extract `--ignore` sets from `ci.yml` and to grep it when a test passes alone and fails in the sweep. Both now name the marker; following the skill reproduces CI rather than a mechanism that no longer exists. Separately, `docs/capability-survey.md` carried four `ci.yml:<line>` citations, three already invalidated by this session's edits — `:607` lands on a blank line, `:884` on a `cmake --build`, `:628-643` on a dep_gen comment, after the qwen step was deleted in hw-native-sys#1601 and `detect-changes` reworked in hw-native-sys#1589 / hw-native-sys#1590 / hw-native-sys#1607. Line numbers into a file this active cannot be maintained, so all four become greppable anchors. `grep -rIn 'ci\.yml:[0-9]'` is now empty repo-wide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#1609) The SDMA quarantine was a pair of `--ignore=<path>` arguments. `pytest --ignore` on a path that no longer exists **exits 0 and says nothing** — measured — so moving either demo directory would have dropped the quarantine silently and landed both tests back beside the `aicore_op_timeout` fault-injection test. That collision is the whole hazard: provisioning the SDMA workspace creates 48 device-only STARS streams that sit in the device fault domain, so a later AICore fault on that device costs ~306 s instead of ~0.3 s (#1425, contained in #1406). `@pytest.mark.sdma` replaces the path matching, and carries the whole consequence rather than one arbitrary piece of it. One declaration now drives three things: - **The Worker is built with `enable_sdma=True`.** Both pytest construction sites in `conftest.py` and both standalone sites in `scene_test.py` read it, the latter from `cls.pytestmark` so `python test_x.py` behaves like the pytest path. - **The L2 Worker pool stops mixing capabilities.** The key gains the flag and reuse tests it, because an enable_sdma Worker holds its STARS streams for life: handing it to a test that did not ask for them would spread the teardown hazard to every later L2 case on that device. The existing same-device retire loop then performs the swap, so no new teardown path. - **SDMA sorts last.** `sort_key` gains a term, keyed off the marker rather than the class because the fault-injection tests are plain functions with no `_st_level`. This is what will make merging the dedicated CI step back into the sweep safe once #1425 is fixed; today `-m` already separates them, so it matters for local full runs. With the capability expressible, `prefetch_async_demo` becomes an ordinary L2 `@scene_test` class. It was a hand-rolled Worker only because `CASES` had no channel to `Worker.__init__`, and it paid for that by forfeiting golden comparison, case parametrization, `--rounds`, `--case` and the dispatcher's device allocation. 160 lines become 89, and its verification — `torch.equal(out, src)` — is now a one-line `compute_golden`. The framework's default orchestration includes turned out to be sufficient, so no new passthrough was needed there. The conversion also moves it from the Resource phase to the L2 phase, which runs after it, so it provisions on a device the fault-injection cases have already finished with. `sdma_async_completion_demo` keeps its hand-rolled L3 Worker for now and takes the marker for ordering and CI selection only; converting it means expressing a comm domain through `CASES`, which is a larger change and independent of this one. CI selects with `-m sdma` / `-m "not sdma"`, so the sweep and the dedicated step can no longer drift apart. **The dedicated step stays until #1425 is fixed**: the two phases run their jobs in parallel across devices, so only separate device pools keep an SDMA provisioning away from a fault injection. `.claude/skills/testing/SKILL.md` moves with the mechanism throughout — not only the mirror-CI command, but the two instructions that told readers to extract `--ignore` sets from `ci.yml` and to grep it when a test passes alone and fails in the sweep. Both now name the marker; following the skill reproduces CI rather than a mechanism that no longer exists. Separately, `docs/capability-survey.md` carried four `ci.yml:<line>` citations, three already invalidated by this session's edits — `:607` lands on a blank line, `:884` on a `cmake --build`, `:628-643` on a dep_gen comment, after the qwen step was deleted in #1601 and `detect-changes` reworked in #1589 / #1590 / #1607. Line numbers into a file this active cannot be maintained, so all four become greppable anchors. `grep -rIn 'ci\.yml:[0-9]'` is now empty repo-wide.
test_a5_aicpu_topology_fallback compiles src/a5/platform/onboard/host/ aicpu_topology_probe.cpp, which now includes common/acl_hal_device.h, but the target never gained src/common/platform/include. ut, ut-a2a3 and ut-a5 all fail at "fatal error: common/acl_hal_device.h: No such file or directory". The a2a3 sibling target already carries the directory; add it to the a5 one. Also drop the "Qwen3-14B 40-layer decode (a2a3)" ci.yml step, which the rebase reintroduced. hw-native-sys#1601 removed it deliberately when qwen went back into the general scene-test sweep, so keeping it runs the 38 GiB fixture twice per a2a3 onboard job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_a5_aicpu_topology_fallback compiles src/a5/platform/onboard/host/ aicpu_topology_probe.cpp, which now includes common/acl_hal_device.h, but the target never gained src/common/platform/include. ut, ut-a2a3 and ut-a5 all fail at "fatal error: common/acl_hal_device.h: No such file or directory". The a2a3 sibling target already carries the directory; add it to the a5 one. Also drop the "Qwen3-14B 40-layer decode (a2a3)" ci.yml step, which the rebase reintroduced. hw-native-sys#1601 removed it deliberately when qwen went back into the general scene-test sweep, so keeping it runs the 38 GiB fixture twice per a2a3 onboard job.
The chip-init path makes direct driver calls that bypass ACL (halMemCtl / halGetDeviceInfo* on a2a3, halResMap / halGetDeviceInfo* / dsmi_get_device_info on a5), so they index the driver-visible space and must translate through acl_to_hal_device_id. Nothing exercised that translation: no CI job sets ASCEND_RT_VISIBLE_DEVICES, so every job ran the identity path and a call site that forgot to translate stayed green. tests/st/vis_isolation runs dummy_task in a subprocess with the granted card exposed as logical 0. init_aicore_register_addresses and probe_aicpu_topology run on every onboard bring-up, so one scene test covers every translated site on both arches. The subprocess is load-bearing: the variable must be set before ACL init, and mutating it in-process would leak into the session's pooled workers. Carrying this as a scene test rather than the two ci.yml steps keeps the selection in the test tree, so the sweep and the check cannot drift apart -- the direction hw-native-sys#1601 and hw-native-sys#1609 took for qwen and SDMA. The onboard jobs already run `pytest examples tests/st`, and the platforms marker keeps it off the sim runners, where a driver-level remap has nothing to verify.
What was wrong
qwen3_14b_decodeheld a device for 406 s (ci run 30507320146, job 90761014912) while the device itself was busy for tens of milliseconds. It was excluded from the general a2a3 sweep with a--ignoreand given a step and an 1800 s budget of its own, because at that size it would have eaten the sweep's 600 s session budget — the budget that makes the sweep a hang detector.Decomposed on this repo's a2a3 box, one device:
generate_inputs, the 38 GiB fixturecompute_golden, 40 layers at 320 torch threadsThe golden was 80–85% of it, and none of that was arithmetic.
Root cause
The reference implementation walks 3584 small slice operations per layer, and torch sizes its intra-op pool from the core count. On a 320-core host every one of those calls forks and joins hundreds of threads to move a few KiB:
Five to eight times the cost of the work, flat from 4 to 16, turning back up below 4.
Nothing in
simpler_setup/,conftest.pyor the workflows had ever set a thread count, so every golden paid this on every many-core host. All three undersimpler_setup/goldens/are built from per-tile Python loops — qwen is merely the densest (8for-range loops against 2 each).Change
The cap goes where all three goldens pass — around the two
compute_goldencall sites inscene_test.py— rather than into any one golden. It only ever lowers the limit (a caller who asked for fewer keeps theirs) and restores the previous value on the way out, exception included.That takes qwen's golden to ~38–43 s and the whole case to ~115 s: an ordinary member of a sweep whose own wall is ~505 s across four dies. So the
--ignore, the dedicated step and its 1800 s budget are all deleted — the case is nothing special now and runs like every other one.Testing
out,k_cacheandv_cache(torch.equaltrue,max|diff| = 0). Checked rather than assumed, since thread count can reorder float reductions.KernelCompiler— 35 ordinary incores at 1.1–1.3 s each, orchestration 6.6 s, and the vendor FAI kernel as the outlier at 8.4 s / 1330 KiB for its AIV variant.yaml.safe_loadonci.yml;ast.parseonscene_test.py;markdownlint-cli2cleanst-onboard-a2a3should stay well inside its 600 s budget with the case included.Docs
.claude/skills/testing/SKILL.mdloses theHEAVY_IGNOREquarantine row and the command carrying it, plus threeci.yml:<line>references this change would have invalidated. The README's cost section is rewritten around the measurements — it reported "~5 min wall, ~49 s golden" from an idle die, which is the 4–16-thread figure and does not describe CI.