diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98b14508a9..be6aa9114b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,14 +7,54 @@ permissions: on: pull_request: branches: [main] + workflow_dispatch: + inputs: + base_sha: + description: "Base SHA for manual runs; defaults to merge-base with origin/main." + required: false + type: string + head_sha: + description: "Head SHA for manual runs; defaults to the checked-out commit." + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: + diff-refs: + runs-on: ubuntu-latest + outputs: + base_sha: ${{ steps.refs.outputs.base_sha }} + head_sha: ${{ steps.refs.outputs.head_sha }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Resolve diff refs + id: refs + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + base_sha='${{ github.event.pull_request.base.sha }}' + head_sha='${{ github.event.pull_request.head.sha }}' + elif [[ -n "${{ github.event.inputs.base_sha }}" && -n "${{ github.event.inputs.head_sha }}" ]]; then + base_sha='${{ github.event.inputs.base_sha }}' + head_sha='${{ github.event.inputs.head_sha }}' + else + git fetch --no-tags origin main + base_sha=$(git merge-base origin/main HEAD) + head_sha='${{ github.sha }}' + fi + + echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT" + echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" + # ---------- Pre-commit hooks (format, lint, clang-tidy) ---------- pre-commit: + needs: [diff-refs] runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -53,7 +93,7 @@ jobs: - name: Run pre-commit uses: pre-commit/action@v3.0.0 with: - extra_args: --from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }} + extra_args: --from-ref ${{ needs['diff-refs'].outputs.base_sha }} --to-ref ${{ needs['diff-refs'].outputs.head_sha }} # ---------- Packaging matrix: 5 install paths × 4 entry points, on macOS + Ubuntu ---------- # Smoke-checks that every supported install path exposes the four user entry points. @@ -164,14 +204,18 @@ jobs: st-sim-a2a3: needs: [detect-changes, pre-commit] if: needs.detect-changes.outputs.a2a3_changed == 'true' - runs-on: ${{ matrix.os }} + runs-on: ${{ fromJSON(matrix.runner) }} timeout-minutes: 30 env: SIMPLER_SCHEDULER_TIMEOUT_MS: "5000" strategy: + fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] - python-version: ['3.10'] + include: + - runner: '["self-hosted", "cpu"]' + python-version: '3.10' + - runner: '["macos-latest"]' + python-version: '3.10' steps: - name: Checkout repository @@ -179,25 +223,47 @@ jobs: - name: Set up C++ compiler run: | if [[ "${{ runner.os }}" == "Linux" ]]; then - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test - sudo apt-get update - sudo apt-get install -y ninja-build - sudo apt-get install -y g++-15 || sudo apt-get install -y g++ - if ! command -v g++-15; then sudo ln -s $(which g++) /usr/local/bin/g++-15; fi - # graphviz: required by the dep_gen smoke (deps_viewer -> dot). - # Both a2a3 and a5 sim jobs run dep_gen, so install it in both. - sudo apt-get install -y graphviz + runner_spec='${{ matrix.runner }}' + if [[ "$runner_spec" == *self-hosted* ]]; then + command -v ninja + command -v g++ + command -v dot + if ! command -v g++-15 >/dev/null 2>&1; then + mkdir -p "$RUNNER_TEMP/bin" + ln -sf "$(command -v g++)" "$RUNNER_TEMP/bin/g++-15" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + fi + else + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update + sudo apt-get install -y ninja-build + sudo apt-get install -y g++-15 || sudo apt-get install -y g++ + if ! command -v g++-15; then sudo ln -s $(which g++) /usr/local/bin/g++-15; fi + # graphviz: required by the dep_gen smoke (deps_viewer -> dot). + # Both a2a3 and a5 sim jobs run dep_gen, so install it in both. + sudo apt-get install -y graphviz + fi else brew install ninja brew install gcc@15 || brew install gcc brew install graphviz fi - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} (GitHub-hosted) + if: ${{ !contains(matrix.runner, 'self-hosted') }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} (self-hosted) + if: ${{ contains(matrix.runner, 'self-hosted') }} + run: | + python3 -m venv --system-site-packages .venv + source .venv/bin/activate + pip install --upgrade pip + pip install torch --index-url https://download.pytorch.org/whl/cpu + pip install '.[test]' + - name: Cache pip packages uses: actions/cache@v5 with: @@ -207,13 +273,18 @@ jobs: ${{ runner.os }}-pip- - name: Install dependencies + if: ${{ !contains(matrix.runner, 'self-hosted') }} run: | pip install torch --index-url https://download.pytorch.org/whl/cpu pip install '.[test]' - name: Run pytest scene tests (a2a3sim) run: | - pytest examples tests/st --platform a2a3sim --device 0-15 -v \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest examples tests/st --platform a2a3sim --device 0-15 -v \ --pto-session-timeout 600 --require-pto-isa # DFX per-feature smokes — the default pytest above passes no --enable-* @@ -222,7 +293,11 @@ jobs: # CI step rather than buried in a combined run. - name: dep_gen smoke run: | - pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ --platform a2a3sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ --require-pto-isa --enable-dep-gen @@ -233,39 +308,59 @@ jobs: # children take xdist args that `-p no:xdist` has disabled. - name: dep_gen smoke (host_build_graph) run: | - pytest tests/st/a2a3/host_build_graph/dfx/dep_gen/test_dep_gen.py \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest tests/st/a2a3/host_build_graph/dfx/dep_gen/test_dep_gen.py \ --platform a2a3sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ --require-pto-isa --enable-dep-gen - name: l2_swimlane smoke run: | - pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ --platform a2a3sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ --require-pto-isa --enable-l2-swimlane --enable-dep-gen - name: PMU smoke run: | - pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ --platform a2a3sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ --require-pto-isa --enable-pmu 2 - name: args_dump smoke run: | - pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ --platform a2a3sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ --require-pto-isa --dump-args st-sim-a5: needs: [detect-changes, pre-commit] if: needs.detect-changes.outputs.a5_changed == 'true' - runs-on: ${{ matrix.os }} + runs-on: ${{ fromJSON(matrix.runner) }} timeout-minutes: 30 env: SIMPLER_SCHEDULER_TIMEOUT_MS: "5000" strategy: + fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] - python-version: ['3.10'] + include: + - runner: '["self-hosted", "cpu"]' + python-version: '3.10' + - runner: '["macos-latest"]' + python-version: '3.10' steps: - name: Checkout repository @@ -273,25 +368,47 @@ jobs: - name: Set up C++ compiler run: | if [[ "${{ runner.os }}" == "Linux" ]]; then - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test - sudo apt-get update - sudo apt-get install -y ninja-build - sudo apt-get install -y g++-15 || sudo apt-get install -y g++ - if ! command -v g++-15; then sudo ln -s $(which g++) /usr/local/bin/g++-15; fi - # graphviz: required by the dep_gen smoke (deps_viewer -> dot). - # Both a2a3 and a5 sim jobs run dep_gen, so install it in both. - sudo apt-get install -y graphviz + runner_spec='${{ matrix.runner }}' + if [[ "$runner_spec" == *self-hosted* ]]; then + command -v ninja + command -v g++ + command -v dot + if ! command -v g++-15 >/dev/null 2>&1; then + mkdir -p "$RUNNER_TEMP/bin" + ln -sf "$(command -v g++)" "$RUNNER_TEMP/bin/g++-15" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + fi + else + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update + sudo apt-get install -y ninja-build + sudo apt-get install -y g++-15 || sudo apt-get install -y g++ + if ! command -v g++-15; then sudo ln -s $(which g++) /usr/local/bin/g++-15; fi + # graphviz: required by the dep_gen smoke (deps_viewer -> dot). + # Both a2a3 and a5 sim jobs run dep_gen, so install it in both. + sudo apt-get install -y graphviz + fi else brew install ninja brew install gcc@15 || brew install gcc brew install graphviz fi - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} (GitHub-hosted) + if: ${{ !contains(matrix.runner, 'self-hosted') }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} (self-hosted) + if: ${{ contains(matrix.runner, 'self-hosted') }} + run: | + python3 -m venv --system-site-packages .venv + source .venv/bin/activate + pip install --upgrade pip + pip install torch --index-url https://download.pytorch.org/whl/cpu + pip install '.[test]' + - name: Cache pip packages uses: actions/cache@v5 with: @@ -301,13 +418,18 @@ jobs: ${{ runner.os }}-pip- - name: Install dependencies + if: ${{ !contains(matrix.runner, 'self-hosted') }} run: | pip install torch --index-url https://download.pytorch.org/whl/cpu pip install '.[test]' - name: Run pytest scene tests (a5sim) run: | - pytest examples tests/st --platform a5sim --device 0-15 -v \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest examples tests/st --platform a5sim --device 0-15 -v \ --pto-session-timeout 600 --require-pto-isa # DFX per-feature smokes — the default pytest above passes no --enable-* @@ -316,25 +438,41 @@ jobs: # CI step rather than buried in a combined run. Mirrors st-sim-a2a3. - name: dep_gen smoke run: | - pytest tests/st/a5/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest tests/st/a5/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ --platform a5sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ --require-pto-isa --enable-dep-gen - name: l2_swimlane smoke run: | - pytest tests/st/a5/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest tests/st/a5/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ --platform a5sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ --require-pto-isa --enable-l2-swimlane --enable-dep-gen - name: PMU smoke run: | - pytest tests/st/a5/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest tests/st/a5/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ --platform a5sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ --require-pto-isa --enable-pmu 2 - name: args_dump smoke run: | - pytest tests/st/a5/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ + PYTHON=python + if [[ '${{ matrix.runner }}' == *self-hosted* ]]; then + PYTHON=.venv/bin/python + fi + "$PYTHON" -m pytest tests/st/a5/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ --platform a5sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ --require-pto-isa --dump-args @@ -728,6 +866,7 @@ jobs: # ---------- Detect platform-specific changes (runs on GitHub server) ---------- detect-changes: + needs: [diff-refs] runs-on: ubuntu-latest outputs: a2a3_changed: ${{ steps.check.outputs.a2a3_changed }} @@ -741,7 +880,7 @@ jobs: - name: Check file changes id: check run: | - FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}) + FILES=$(git diff --name-only ${{ needs['diff-refs'].outputs.base_sha }}...${{ needs['diff-refs'].outputs.head_sha }}) NON_CODE='^(docs/|\.docs/|\.claude/|\.gitignore$|\.pre-commit-config\.yaml$)|\.md$' diff --git a/docs/ci.md b/docs/ci.md index fc35d33aa9..acb6161a75 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -3,13 +3,14 @@ ## Overview The CI pipeline maps test categories (st, ut-py, ut-cpp) × hardware tiers to GitHub Actions jobs. See [testing.md](testing.md) for full test organization and hardware classification. + Design principles: 1. **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`). 2. **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`). 3. **`--platform` is the only filter** — pytest uses `--platform` + the `requires_hardware` marker; ctest uses label `-LE` exclusion. No `-m st`, no `-m "not requires_hardware"`. -4. **sim = no hardware** — `a2a3sim`/`a5sim` jobs run on github-hosted runners alongside unit tests. +4. **sim = no hardware** — `a2a3sim`/`a5sim` keep the macOS leg on GitHub-hosted runners, while the Linux leg can be routed to the local `cpu` self-hosted pool to escape the 4-vCPU ceiling. 5. **Skip irrelevant platforms for scene tests** — `detect-changes` gates `st-sim-*` and `st-onboard-*` so pure-a5 PRs skip a2a3 scene-test runs and vice versa. **UT jobs (`ut`, `ut-a2a3`, `ut-a5`) are not gated by platform** — unit tests cover shared contracts and the cost of a falsely-skipped regression outweighs the savings. 6. **Markdown-only PRs run pre-commit and nothing else** — `detect-changes` sets `docs_only` when *every* changed file ends in `.md`. That is the one case where skipping the UT jobs carries no risk: there is no code delta to regress, and markdownlint inside pre-commit is the only check that reads the files at all. @@ -30,8 +31,8 @@ PullRequest ├── packaging-matrix (ubuntu + macOS) — [skipped iff docs_only] ├── ut (ubuntu + macOS) — Python + C++ UT, no hardware [skipped iff docs_only] ├── detect-changes (ubuntu-latest) — outputs a{2a3,5}_changed + docs_only - ├── st-sim-a2a3 (ubuntu + macOS) — gated by a2a3_changed - ├── st-sim-a5 (ubuntu + macOS) — gated by a5_changed + ├── st-sim-a2a3 (cpu/Linux + macOS) — gated by a2a3_changed + ├── st-sim-a5 (cpu/Linux + macOS) — gated by a5_changed ├── ut-a2a3 (a2a3 self-hosted) — Python + C++ UT, a2a3 hardware [skipped iff docs_only] ├── st-onboard-a2a3 (a2a3 self-hosted) — gated by a2a3_changed ├── ut-a5 (a5 self-hosted) — Python + C++ UT, a5 hardware [skipped iff docs_only] @@ -41,8 +42,8 @@ PullRequest | 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` | +| `st-sim-a2a3` | `self-hosted [cpu]`, `macos-latest` | `pytest examples tests/st --platform a2a3sim` | +| `st-sim-a5` | `self-hosted [cpu]`, `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 --platform a2a3 --device ...` | | `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) | @@ -101,16 +102,18 @@ profiling-vs-parallelism trade-off. ### Sim jobs on CPU-constrained runners -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 jobs (`st-sim-a2a3`, `st-sim-a5`) are no-hardware jobs. The macOS leg still +runs on GitHub-hosted runners, whose standard Linux equivalent is the +4-vCPU `ubuntu-latest` box; the Linux leg is better pointed at a local `cpu` +self-hosted runner if you want to escape that ceiling. `--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))` — so the runner's actual core count determines the +parallelism budget. `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. ```bash -# Sim: --max-parallel auto resolves to 4 on a standard ubuntu-latest runner +# Sim on a 4-vCPU runner: --max-parallel auto resolves to 4 pytest examples tests/st --platform a2a3sim --device 0-15 # Throttle further on a CPU-starved runner: 4 concurrent cases (each forking @@ -127,7 +130,7 @@ not need `--max-parallel` manually. ### Scheduling constraints -- Sim scene tests and no-hardware unit tests run on github-hosted runners (no hardware). +- Sim scene tests and no-hardware unit tests run on no-hardware runners; the sim Linux leg is now pointed at the `cpu` self-hosted pool when available, while macOS stays GitHub-hosted. - `detect-changes` computes three flags (`a2a3_changed`, `a5_changed`, `docs_only`) from the PR diff. Each flag is `false` only when *every* changed file is in the opposite platform's tree (`src/{arch}/`, `examples/{arch}/`, `tests/{st,ut/cpp}/{arch}/`) or in the `NON_CODE` set (`docs/`, `.docs/`, `.claude/`, `.gitignore`, `.pre-commit-config.yaml`, and any `*.md` file anywhere). 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 workflow files (`.github/`) — flips both flags to `true`. - **Gated jobs (scene tests only):** `st-sim-{a2a3,a5}`, `st-onboard-{a2a3,a5}` run iff their platform's flag is `true`. - **Platform-independent jobs (all UT + packaging):** `ut`, `ut-a2a3`, `ut-a5`, `packaging-matrix` ignore 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. The `tests/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. @@ -139,7 +142,7 @@ Three hardware tiers, applied to all test categories. See [testing.md](testing.m | Tier | CI Runner | Job examples | | ---- | --------- | ------------ | -| No hardware | `ubuntu-latest` | `ut`, `st-sim-*` | +| No hardware | `ubuntu-latest`, `self-hosted [cpu]` | `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` | diff --git a/simpler_setup/runtime_compiler.py b/simpler_setup/runtime_compiler.py index 30f0e32703..2f4fb19567 100644 --- a/simpler_setup/runtime_compiler.py +++ b/simpler_setup/runtime_compiler.py @@ -25,6 +25,8 @@ _WORKSPACE_TRUTHY = {"1", "ON", "TRUE", "YES"} +# Keep this in sync with the CI bootstrap docs for self-hosted sim runners. + def _sdma_workspace_enabled() -> bool: """Whether the a5 PTO SDMA workspace overlay is opted in.