The CI pipeline maps test categories (st, ut-py, ut-cpp) × hardware tiers to GitHub Actions jobs. See testing.md for full test organization and hardware classification.
Design principles:
- Merge by runner, not by language — Python and C++ unit tests share setup cost and run as steps within a single job per runner tier (
ut,ut-a2a3,ut-a5). - Runner matches hardware tier — no-hardware tests run on
ubuntu-latest; platform-specific tests run on self-hosted runners with the matching label (a2a3,a5). --platformis the only filter — pytest uses--platform+ therequires_hardwaremarker; ctest uses label-LEexclusion. No-m st, no-m "not requires_hardware".- sim = no hardware —
a2a3sim/a5simjobs run on github-hosted runners alongside unit tests. - Skip irrelevant platforms, and irrelevant suites —
detect-changesgatesst-sim-*andst-onboard-*by platform, so pure-a5 PRs skip a2a3 scene-test runs and vice versa. UT jobs are still not gated by platform — unit tests cover shared contracts and the cost of a falsely-skipped regression outweighs the savings. They are gated by test category: a diff confined totests/st/orexamples/cannot break a unit test, and one confined totests/ut/cannot break a scene test, because the two suites execute disjoint trees (pytest examples tests/stvspytest tests/utplus the C++ ctest) and no unit test readsexamples/ortests/st/. Shared test infrastructure — the rootconftest.py,pyproject.toml,simpler_setup/,tests/lint/— belongs to neither category and runs both. - Non-code PRs run pre-commit and docs, and nothing else —
detect-changessetsnon_code_onlywhen no changed file falls outside theNON_CODEset. Skipping the UT jobs is riskless there because nothing in that set can change what the code does, and each member already has its own gate: markdownlint insidepre-commitreads the markdown,docs.yml(unconditional on every PR) builds the site with--strict, andpre-commititself is ungated so a.pre-commit-config.yamlchange is fully exercised by it.
The complete test-type × hardware-tier matrix. Empty cells have no tests yet; only non-empty jobs exist in ci.yml.
| Category | github-hosted (no hardware) | a2a3 runner | a5 runner |
|---|---|---|---|
| ut (py + cpp) | ut |
ut-a2a3 |
ut-a5 |
| st | st-sim-a2a3, st-sim-a5 |
st-onboard-a2a3, st-pod-onboard-a2a3 |
st-onboard-a5 |
ci.yml and ci-self-cpu.yml are now thin topology callers: they own
triggers, needs:, gate if: expressions, runner/setup inputs, and matrix
shape. The executable job bodies live in reusable workflows:
_detect-changes.yml, _pre-commit.yml, _ut-no-hardware.yml,
_packaging.yml, _profiling-flags-smoke.yml, _st-sim-a2a3.yml,
_st-sim-a5.yml, _ut-npu-a2a3.yml, _ut-npu-a5.yml,
_st-npu-a2a3.yml, _st-npu-a5.yml, and _st-pod.yml. The scene-test and
NPU unit-test bodies are split one workflow per architecture so each job
renders only its own steps. Shared step scaffolding that is safe to run
after checkout lives in composite actions under .github/actions/
(cache-pip, setup-venv, and the three pod-* actions).
PullRequest
├── pre-commit (ubuntu-latest)
├── packaging-matrix (ubuntu + macOS) — [needs !examples_only && !tests_only]
├── profiling-flags-smoke (ubuntu-latest) — (a2a3_changed || a5_changed) && !examples_only && !tests_only
├── ut (ubuntu + macOS) — Python + C++ UT, no hardware [needs ut_affected]
├── detect-changes (reusable; ubuntu-latest in ci.yml) — outputs non_code_only, a{2a3,5}_changed, {st,ut}_affected, examples_only, tests_only
├── st-sim-a2a3 (ubuntu + macOS) — a2a3_changed && st_affected
├── st-sim-a5 (ubuntu + macOS) — a5_changed && st_affected
├── ut-a2a3 (a2a3 self-hosted) — Python + C++ UT, a2a3 hardware [needs ut_affected]
├── st-onboard-a2a3 (a2a3 self-hosted) — a2a3_changed && st_affected
├── st-pod-onboard-a2a3 (a2a3pod pair) — a2a3_changed && st_affected
├── ut-a5 (a5 self-hosted) — Python + C++ UT, a5 hardware [needs ut_affected]
└── st-onboard-a5 (a5 self-hosted) — a5_changed && st_affected
| Job | Runner | What it runs |
|---|---|---|
ut |
ubuntu-latest, macos-latest |
pytest tests/ut + ctest -LE requires_hardware |
st-sim-a2a3 |
ubuntu-latest, macos-latest |
pytest examples tests/st --platform a2a3sim |
st-sim-a5 |
ubuntu-latest, macos-latest |
pytest examples tests/st --platform a5sim |
ut-a2a3 |
a2a3 self-hosted | pytest tests/ut --platform a2a3 + ctest -L "^requires_hardware(_a2a3)?$" --resource-spec-file ... + build tools/cann-examples/query and run query version (no device) + build tools/cann-examples/aicpu-device-query and tools/cann-examples/aicpu-kernel-launch (host + cross-compiled device SO, link smoke only) |
st-onboard-a2a3 |
a2a3 self-hosted | pytest examples tests/st -m "not sdma" --platform a2a3 --device ..., then a separate -m sdma step, then the DFX per-feature smokes |
ut-a5 |
a5 self-hosted | pytest tests/ut --platform a5 + ctest -L "^requires_hardware(_a5)?$" + build tools/cann-examples/query and run query version (no device) + build tools/cann-examples/aicpu-device-query and tools/cann-examples/aicpu-kernel-launch (link smoke only) |
st-onboard-a5 |
a5 self-hosted | pytest examples tests/st --platform a5 --device ...; x86_64 runners add -m "not sdma" |
st-pod-onboard-a2a3 |
a pair of a2a3pod machines |
the L4 mixed local/remote examples, one L3 per machine |
st-pod-onboard-a2a3 is the only job spanning two machines. The runner it
lands on becomes the L4 parent and drives its peer entirely over ssh; the peer
runs no workflow code at all. Everything that identifies either machine —
addresses, the device split, ports, the staging root, proxies — comes from a
.env the runner carries, so _st-pod.yml holds no machine-specific value.
Adding or re-addressing a machine is an edit to that file. Only a machine
hosting a runner needs one.
Its body splits by what is per-run and what is per-example:
| Action | Called | What it does |
|---|---|---|
pod-stage |
once | rsync this run's tree onto the peer and build it there |
pod-run-example |
once per example | start the peer's L3 daemon, run the example's parent, stop the daemon and pull its logs |
pod-teardown |
once, if: always() |
remove the run's tree from the peer |
Staging and the peer-side build are the job's whole cost, and every example runs against that same tree and venv, so they happen once; only the daemon and the parent repeat. A job-level matrix over examples would instead repeat the staging and both venvs per branch.
Examples run with continue-on-error and a summary step decides the result:
one round holds two machines, so learning about only the first failure wastes
the second half of it. A step reporting green there has not necessarily
passed — continue-on-error rewrites a failed step's conclusion to success
and leaves the truth in outcome, which only the summary step reads.
Each example's logs go to output/pod-ci-<run>-<attempt>/<example>/ and the
whole directory is uploaded as one artifact. Reach for it first on a
device-side failure: the host traceback only says the peer's scheduler gave
up, and the sub-class saying why is printed on the device.
Writing an example — the files, the entry module, the environment variables
pod-run-example sets — is covered in
examples/workers/README.md.
A separate workflow, sanitizers.yml,
runs on a nightly schedule — kept out of ci.yml so the cron fires only the
sanitizer jobs, never the PR/self-hosted pipeline. Its
sanitizer-sim job builds the sim runtime + kernels with ASAN or TSAN
(pip install --config-settings=cmake.define.SIMPLER_SANITIZER=...) and runs a
scoped subset under the matching LD_PRELOAD (a2a3sim/a5sim, ubuntu-only).
dlopen_count tests are excluded everywhere (they assert exact dlopen accounting
that the sanitizers perturb by interposing dlopen). The full suite is avoided
because ASAN/TSAN slow the sim enough that oversubscription-heavy cases livelock
on a 4-vCPU runner — so the scope is parallelism-limited per sanitizer:
- ASAN (~1.7x):
prepared_callable+dynamic_register(where present),--max-parallel 2, skippingparallel_broadcast. - TSAN (~5-15x): livelocks the chip-fork L3 cases even when run serially, so it
runs only the light
prepared_callableL2 tests,--max-parallel 1, withTSAN_OPTIONS=halt_on_error=0:exitcode=0(report races without aborting or failing the job — TSAN's defaultexitcode=66would otherwise redden the cell on every race; the job gates on hang/crash, triaging the reported races into a suppressions file is a follow-up).
Both sanitizer jobs gate (no continue-on-error). Not a PR gate; see
sanitizers.md for the design + usage.
For self-hosted jobs with multiple NPUs, pass a --device range (and
optionally pytest's -x for fail-fast) to get the full dispatcher
benefit — device bin-packing for L3, xdist fanout for L2, and a shared
ChipWorker per (runtime, device):
# Recommended CI invocation — a2a3 deselects the SDMA marker, as the job does,
# and runs it as a second pass afterwards
pytest examples tests/st -m "not sdma" --platform a2a3 --device 4-7 -x
pytest examples tests/st -m sdma --platform a2a3 --device 4-5 -x
# A5 ARM64 runners run the full corpus
pytest examples tests/st --platform a5 --device 0-7 -x
# A5 x86_64 runners deselect SDMA tests
pytest examples tests/st -m "not sdma" --platform a5 --device 0-7 -x-x (--exitfirst) is appropriate for CI, where aborting on first
failure saves runner minutes. Local development usually wants the opposite
(let every failure surface) — just drop the flag. The short form is the
same in both pytest and standalone on purpose; see
testing.md §CLI Design Principles.
pytest-xdist is pulled in via the test extra. See
testing.md §Parallel Test Execution
for the full hierarchy, fail-fast semantics, and the
profiling-vs-parallelism trade-off.
Sim jobs (st-sim-a2a3, st-sim-a5) run on ubuntu-latest, whose standard
GitHub-hosted runner currently has 4 vCPUs. --device 0-15 is still the
right choice for the pool size (some L3 cases need several virtual ids), but
the default --max-parallel auto caps the in-flight subprocess count to
min(nproc, len(--device)) — on a 4-core runner that becomes 4. Note
os.cpu_count() reports the host's logical CPUs and ignores any cgroup CPU
quota, so this is the true core count, not a container limit.
# Sim: --max-parallel auto resolves to 4 on a standard ubuntu-latest runner
pytest examples tests/st --platform a2a3sim --device 0-15
# Throttle further on a CPU-starved runner: 4 concurrent cases (each forking
# several chip subprocesses with many threads) can oversubscribe 4 cores and
# trigger the sim handshake/deinit failures in
# troubleshooting/sim-oversubscription-hang.md. --max-parallel 2 trades
# throughput for stability.
pytest examples tests/st --platform a2a3sim --device 0-15 --max-parallel 2On hardware jobs the auto default is len(--device) because each subprocess
is device-bound (host CPU mostly waits on the NPU), so hardware runners do
not need --max-parallel manually.
-
Sim scene tests and no-hardware unit tests run on github-hosted runners (no hardware).
-
detect-changesis implemented once in.github/workflows/_detect-changes.ymland computes four axes from the PR diff — non-code (non_code_only), architecture (a2a3_changed/a5_changed), test category (st_affected/ut_affected), and corpus (examples_only/tests_only) — all of them derived from oneNON_CODEset:docs/,.docs/,.claude/,mkdocs.yml,.github/workflows/docs.yml,.gitignore,.pre-commit-config.yaml, and any*.mdfile anywhere. Membership follows a file's effect, not its path —mkdocs.ymlanddocs.ymlare docs tooling that happens to live outsidedocs/. An arch flag isfalseonly when every changed file is in the opposite platform's tree (src/{arch}/,examples/{arch}/,tests/{st,ut/cpp}/{arch}/) or inNON_CODE. Anything else — shared C++ (src/common/), Python (python/,simpler_setup/), build files (CMakeLists.txt,pyproject.toml), shared test infra (tests/ut/py/,tests/lint/), tooling (tools/), or any CI implementation workflow (.github/workflows/ci.yml,.github/workflows/ci-self-cpu.yml,.github/workflows/_*.yml) — flips both flags totrue. CI implementation workflows are deliberately excluded fromNON_CODE: a change to the gates or reusable job bodies must run everything, including whatever it just switched off. -
Test-category axis:
ST_ONLY='^(tests/st/|examples/)'andUT_ONLY='^tests/ut/', applied in the same shape as the arch patterns — a category is unaffected only when every changed file is exclusively the other's.st_affectedgates the four scene-test jobs;ut_affectedgatesut,ut-a2a3,ut-a5. Anything belonging to neither (rootconftest.py,pyproject.toml,simpler_setup/,tests/lint/) flips both, so shared infrastructure always runs both suites. -
Corpus axis:
EXAMPLES_ONLY='^examples/'andTESTS_ONLY='^(tests/)', same shape again, one per side of the product jobs' payload.packaging-matrixandprofiling-flags-smokebuild and install the product and then exercise it with a fixed, tiny payload — one entry-point script (atests/st/file) and onevector_example— so neither reads either suite as a corpus, and a diff confined toexamples/or totests/cannot reach them:wheel.packagesis["simpler_setup", "python/simpler"], so both partitions are provably absent from the product. A payload file changed under either is still exercised by the scene-test job that reads the same corpus. -
Gated jobs (scene tests):
st-sim-{a2a3,a5},st-onboard-{a2a3,a5}run iff their platform's flag andst_affectedaretrue. -
Platform-independent jobs (all UT + packaging):
ut,ut-a2a3,ut-a5,packaging-matrixignore the platform flags — unit tests exercise shared contracts (nanobind bindings, RuntimeBuilder, ring buffers, etc.) and the risk of silently skipping a regression outweighs the CI minutes saved. Thetests/ut/cpp/{arch}/entry in the gating regex only attributes an arch-specific C++ UT change to that platform (so it does not spuriously flip the other arch's scene-test flag); it does not gate the UT jobs themselves. The three UT jobs do respectut_affected, which is a statement about test category rather than silicon.packaging-matrixrespects the corpus axis, which is a statement about which corpus a job reads: neitherexamples/nortests/is inwheel.packages, so a diff confined to either partition cannot change what the packaging job builds. -
non_code_onlyis the sameNON_CODEset, not a narrower one. It istruewhen no changed file falls outside it. Nothing in the set can change what the code does, and no workflow consumes any of it beyond its own gate:pre-commitis ungated so it always exercises.pre-commit-config.yaml,docs.ymlis unconditional on every PR so it always exercisesmkdocs.yml/docs/, and no workflow invokes anything under.claude/(grep -rn '\.claude' .github/workflows/finds only comments). An empty diff short-circuits the whole step: attribution is impossible, so a single guard setsnon_code_only=false, every arch and category flagtrue, and both corpus flags (examples_only,tests_only)false, then returns — running the full matrix, packaging and the profiling smoke included. That guard is deliberately one place; testing emptiness per flag is what previously leftnon_code_onlyfalse while both arch flags also came out false, running UT and packaging but skipping every scene test.The arch flags subtract
NON_CODEbefore deciding, so a non-code-only change already makes bothfalse. An arch-gated job therefore needs no separate non-code check. See.claude/rules/ci-change-detection.mdfor the invariants these gates must keep. -
SDMA tests run as their own step inside
st-onboard-a2a3. The sweep deselects them with-m "not sdma"and a later step runs-m sdma. Ordering is what the two paths share: the SDMA step is always second, so no fault-injection case can land on a device that has already provisioned SDMA. Device acquisition differs by host arch — on aarch64 the SDMA step takes its owntask-submit --device auto --device-num 2, so the two steps are disjoint in devices as well; on x86_64 there is notask-submitand both steps use the same${DEVICE_RANGE}, leaving ordering as the only separation. Provisioning the SDMA workspace creates device-only STARS streams that live in the device fault domain, so an AICore fault on a device that has provisioned SDMA costs minutes instead of milliseconds — the sweep'saicore_op_timeoutfault injection must therefore never share a device with them (#1425). Selection is by marker on both sides, so the two cannot drift apart; the split can be dropped once #1425 is fixed. Nothing outsidest-onboard-a2a3filters on the marker, so a localpytest examples tests/ststill runs everything.
When GitHub-hosted runners are congested, a repo admin can validate a PR on the repo-level self-hosted runners via the emergency lane, bypassing GitHub-hosted queueing entirely:
- Trigger: comment
/run-cpuon the PR (ci-self-cpu-button.yml), orgh workflow run ci-self-cpu.yml -f repository=<repo> -f ref=<sha>manually (covers forks and arbitrary SHAs). The button gate ispermission(commenter) == 'admin'only (getCollaboratorPermissionLevel).issue_commentfrom fork PRs runs with a read-only token, so the permission check must work under it — verify on first fork-PR use. - What it runs: checks out
repository@ref(the PR head), then T1 — the no-hardware Linux jobs (pre-commit,ut,packaging,profiling-flags-smoke,st-sim-{a2a3,a5}) on[self-hosted, cpu]— and T3 — the NPU jobs (ut-a2a3,st-onboard-a2a3,ut-a5,st-onboard-a5) on[self-hosted, a2a3/a5]. T2 (macOS) is intentionally absent. The lane calls the same reusable job-body workflows asci.yml, passingsetup_variant=self-cpu,repository,ref, and self-hosted runner labels where the main CI passessetup_variant=githuband GitHub-hosted runners. Gate outputs still come from the canonicaldetect-changesworkflow, executed on[self-hosted, cpu]in this lane. - cpu runner contract: dnf-installed
cmake ninja-build gcc-c++ clang-tools-extra graphviz gtest-devel python3-devel, plus a pip-installable torch aarch64 CPU wheel;g++-15is a symlink stand-in for the ubuntu-toolchain ppa g++. On the agentsg++resolves to a conda GCC 15 prefix rather than/usr/bin/g++, so the lane's sim artifacts are built with GCC 15 andcompile_commands.jsonnames that prefix's<triple>-g++;tests/lint/clang_tidy.pydrops the triple before replaying a command, without which clang-tidy adopts it as a target and resolves no C++ standard library at all.ci.ymllints with clang-tidy 18 and HCE 2.0 packages only LLVM 12, so an agent additionally provides 18 onPATHasclang-tidy-18, installed together with its clang builtin headers — a clang-tidy whose prefix carries nolib/clang/<major>/includeresolves no resource dir and fails every#include <stddef.h>. Thepre-commitjob shadows the distroclang-tidywith it when present. - The lane run is standalone — it attaches no checks to the PR; results are read from the run.
Three hardware tiers, applied to all test categories. See testing.md for the full table including per-category mechanisms (pytest markers, ctest labels, folder structure).
| Tier | CI Runner | Job examples |
|---|---|---|
| No hardware | ubuntu-latest |
ut, st-sim-* |
| Platform-specific (a2a3) | [self-hosted, a2a3] |
ut-a2a3, st-onboard-a2a3 |
| Platform-specific (a5) | [self-hosted, a5] |
ut-a5, st-onboard-a5 |
On a self-hosted runner, every step that touches an NPU — pytest and ctest
alike — must hold its devices exclusively while it runs. There are two a2a3
runner pools, branched at run time on the host arch (uname -m):
- ARM64 a2a3 runners share the host with interactive users, so the step
runs through
task-submit --device <list> --run "...", whose per-device lock keeps a CI job from colliding with someone's local run (and vice versa). - X64 a2a3 runners do not use
task-submit— their cards are exclusive to the runner — so the step runspytest/ctestdirectly with--device ${DEVICE_RANGE}.
a5 runners always use task-submit. The x86_64 A5 scene-test sweep deselects
the sdma marker; ARM64 runs the full corpus, including SDMA tests. Steps that
only build (cmake, RuntimeBuilder, the
cann-examples smokes) take no lock on either arch. The same device-lock rule
applies to local onboard work — see
.claude/rules/running-onboard.md.
The two onboard scene-test jobs (st-onboard-a2a3, st-onboard-a5) compile
their selected test batch before entering task-submit. The lock-free warm-up
populates the build/cache/kernels/ cache; the subsequent pytest invocation
keeps the existing batch-level device allocation and reconstructs each
ChipCallable from that cache. The warm-up does not acquire one lock per case,
and runners with exclusive devices continue to execute pytest directly.
Compilation is serial by default; both onboard jobs pass --compile-workers 8,
which assumes the runner's CPU is theirs alone — it starts eight ccec
processes at once. The sim jobs run on ephemeral GitHub-hosted runners with no
restored cache, so they compile cold every time and get no warm-up step.
A warm-up that cannot compile a class reports it and keeps going: the pass only fills a cache, so the locked pytest run that follows is what recompiles the class and attributes the failure to the case that owns it, instead of one unbuildable kernel costing the whole batch its results.
actions/checkout cleans ignored files before each job, so the onboard jobs
restore and save build/cache/kernels/ through actions/cache. Cache keys are
partitioned by target architecture, runner OS/architecture, and PTO-ISA pin.
The artifact's own key covers the contents of its orchestration, incore, and
transitively included sources, the compiler identities and effective fixed
flags, a digest of the modules that decide artifact bytes
(kernel_compiler.py, toolchain.py, elf_parser.py), the binding's
serialized-callable ABI, and a manual schema constant. It also carries the
owning test class's qualified name, so two scene tests built from identical
sources keep separate entries rather than sharing one.
Entries are content-addressed and therefore never overwritten, so a run prunes
entries whose last use is more than 14 days old before it exits. Without that,
the directory grows by one entry per kernel change forever and the saved
actions/cache archive — one new entry per push, restored by prefix — would
crowd the repository's 10 GB cache budget and evict the pip and cmake caches
the other jobs depend on.
Python unit tests. Run via pytest, filtered by --platform + requires_hardware marker.
| File | Content | Hardware? |
|---|---|---|
test_task_interface.py |
nanobind extension API tests | No |
test_runtime_builder.py (mocked classes) |
RuntimeBuilder discovery, error handling, build logic | No |
test_runtime_builder.py::TestRuntimeBuilderIntegration |
Real compilation across platform × runtime | Yes (@pytest.mark.requires_hardware) |
GoogleTest-based tests for pure C++ modules. Run via ctest, filtered by label -LE exclusion.
| Runner | Command |
|---|---|
| No hardware | ctest --test-dir tests/ut/cpp/build -LE requires_hardware |
| a2a3 | ctest --test-dir tests/ut/cpp/build -L "^requires_hardware(_a2a3)?$" |
| a5 | ctest --test-dir tests/ut/cpp/build -L "^requires_hardware(_a5)?$" |
Small, fast examples that run on both simulation and real hardware. Organized as examples/{arch}/{runtime}/{name}/. Discovered and executed by pytest via each example's test_*.py (@scene_test format).
Large-scale, feature-rich hardware tests. Too slow or using instructions unsupported by the simulator. Organized as tests/st/{arch}/{runtime}/{name}/. Platform compatibility is declared per test via @scene_test(platforms=[...]).
Both examples/ and tests/st/ cases follow the same layout:
{name}/
test_{name}.py # @scene_test class (generate_args, compute_golden)
kernels/
orchestration/*.cpp
aic/*.cpp # optional
aiv/*.cpp # optional
Cases are discovered by pytest via test_*.py files. Each test module ends with if __name__ == "__main__": SceneTestCase.run_module(__name__) so it can also run standalone as python test_*.py -p <platform>.
A single --platform flag controls hardware/non-hardware splitting across all three categories.
@pytest.mark.requires_hardware # any hardware
class TestRuntimeBuilderIntegration:
...
@pytest.mark.requires_hardware("a2a3") # a2a3 specifically
class TestA2A3Feature:
...Selection:
# No hardware (no-hw tests run, requires_hardware tests skip)
pytest tests/ut
# Hardware (no-hw tests skip, hw + platform-specific tests run)
pytest tests/ut --platform a2a3# any hardware
set_tests_properties(test_runtime_integration PROPERTIES LABELS "requires_hardware")
# a2a3-specific
set_tests_properties(test_a2a3_feature PROPERTIES LABELS "requires_hardware_a2a3")Selection uses -LE (label exclude) on no-hw runner and -L (label include) on device runners:
ctest -LE requires_hardware # no-hardware runner: only unlabeled
ctest -L "^requires_hardware(_a2a3)?$" # a2a3 runner: hw + a2a3-specific
ctest -L "^requires_hardware(_a5)?$" # a5 runner: hw + a5-specific@scene_test(level=2, platforms=["a2a3sim", "a2a3"], runtime="tensormap_and_ringbuffer")
class TestVectorExample(SceneTestCase):
...--platform |
Behavior |
|---|---|
a2a3sim |
Run if "a2a3sim" in platforms |
a2a3 |
Run if "a2a3" in platforms |
| (none) | Auto-parametrize over all *sim entries in platforms |
No --platform means "run all sims" — tests with no sim in their platforms list are skipped. No additional markers are used.
- macOS libomp collision: on macOS, the root
conftest.pysetsKMP_DUPLICATE_LIB_OK=TRUEbeforeimport pytestto work around a duplicate-libomp abort triggered by homebrew numpy and pip torch coexisting in one Python process (see troubleshooting/macos-libomp-collision.md). Standalonepython test_*.pybypasses conftest — rely on the env var being exported by the shell ortools/verify_packaging.sh. - sim hangs /
rc=-1under CPU oversubscription: on a few-vCPU runner, high--max-parallel(or many concurrent sim cases) oversubscribes the host CPUs, where sim's busy-spin handshake can livelock (hang →rc=124) or the deinit timeout can false-trip (simpler_run failed with code -1). Mitigate with--max-parallel 2; onboard is unaffected (see troubleshooting/sim-oversubscription-hang.md). - local runs time out more slowly than CI: compiled defaults are lenient for serving workloads, while CI sets tighter
SIMPLER_*_TIMEOUT_*env values to fail fast. Use the same env values locally when debugging suspected hangs (see troubleshooting/local-timeout-defaults.md). st-onboard-a2a3mass 507899 is not OOM: a whole-suite collapse of507899/507018/register_callable -1is an AICPU device-fault cascade (simpler_aicpu_execexception), not memory exhaustion. Diagnosis recipe and the per-device preinstall-name fix are in troubleshooting/a2a3-507899-aicpu-shared-so-fault.md.