From da7ba4c69c605deb22b4b920cf98a3b238f4865b Mon Sep 17 00:00:00 2001 From: Chao Wang <26245345+ChaoWao@users.noreply.github.com> Date: Sat, 1 Aug 2026 21:05:58 -0700 Subject: [PATCH] CI: add CPU emergency lane and /run-cpu admin button When GitHub-hosted runners are congested, a repo admin can validate a PR head on the repo-level self-hosted runners: - ci-self-cpu.yml checks out the PR head (repository + ref inputs, so fork PRs work), then runs T1 (no-hardware Linux: pre-commit, ut, packaging, profiling-flags-smoke, st-sim) on [self-hosted, cpu] and T3 (NPU jobs) on [self-hosted, a2a3/a5]; T2 (macOS) is intentionally absent. A lane-local detect-changes (same NON_CODE vocabulary as ci.yml) skips jobs a diff cannot affect. - ci-self-cpu-button.yml turns a "/run-cpu" PR comment into a lane run, gated on repo-admin permission only. It calls the lane via workflow_call: issue_comment from fork PRs runs with a read-only token, which cannot dispatch workflows. --- .github/workflows/ci-self-cpu-button.yml | 65 +++ .github/workflows/ci-self-cpu.yml | 521 +++++++++++++++++++++++ docs/ci.md | 22 + 3 files changed, 608 insertions(+) create mode 100644 .github/workflows/ci-self-cpu-button.yml create mode 100644 .github/workflows/ci-self-cpu.yml diff --git a/.github/workflows/ci-self-cpu-button.yml b/.github/workflows/ci-self-cpu-button.yml new file mode 100644 index 0000000000..3133db2a63 --- /dev/null +++ b/.github/workflows/ci-self-cpu-button.yml @@ -0,0 +1,65 @@ +name: CI Self CPU Button + +# PR comment command: a repo admin comments "/run-cpu" on a pull request and +# this calls ci-self-cpu.yml (workflow_call) to validate that PR's head on the +# cpu + NPU runners — the emergency lane for GitHub-hosted congestion. +# +# Gate: permission(commenter) == 'admin' ONLY (getCollaboratorPermissionLevel). +# Note: issue_comment from fork PRs runs with a read-only GITHUB_TOKEN; the +# permission check is a read API and must be verified working on a fork PR. + +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: read + +jobs: + gate-and-resolve: + if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/run-cpu') + runs-on: [self-hosted, cpu] + outputs: + repository: ${{ steps.resolve.outputs.repository }} + ref: ${{ steps.resolve.outputs.ref }} + pr_number: ${{ steps.resolve.outputs.pr_number }} + steps: + - name: Admin-only gate + uses: actions/github-script@v7 + with: + script: | + const user = context.payload.comment.user.login; + let perm = null; + try { + const r = await github.rest.repos.getCollaboratorPermissionLevel( + { ...context.repo, username: user }); + perm = r.data.permission; + } catch (e) { + core.info(`not a repo collaborator: ${user}`); + } + if (perm !== 'admin') { + core.setFailed( + `denied: ${user} has ${perm || 'no'} repo permission; ` + + `admin required to trigger /run-cpu`); + } + - name: Resolve PR head + id: resolve + uses: actions/github-script@v7 + with: + script: | + const pr = await github.rest.pulls.get({ + ...context.repo, + pull_number: context.payload.issue.number, + }); + core.setOutput('repository', pr.data.head.repo.full_name); + core.setOutput('ref', pr.data.head.sha); + core.setOutput('pr_number', String(pr.data.number)); + + lane: + needs: [gate-and-resolve] + uses: ./.github/workflows/ci-self-cpu.yml + with: + repository: ${{ needs.gate-and-resolve.outputs.repository }} + ref: ${{ needs.gate-and-resolve.outputs.ref }} + pr_number: ${{ needs.gate-and-resolve.outputs.pr_number }} diff --git a/.github/workflows/ci-self-cpu.yml b/.github/workflows/ci-self-cpu.yml new file mode 100644 index 0000000000..14158afc0a --- /dev/null +++ b/.github/workflows/ci-self-cpu.yml @@ -0,0 +1,521 @@ +name: CI Self CPU +run-name: CI Self CPU / PR ${{ inputs.pr_number || inputs.ref }} + +# Emergency lane: run T1 (no-hardware Linux) on the repo-level self-hosted cpu +# runner plus T3 (NPU) on the a2a3/a5 runners, when GitHub-hosted runners are +# congested. Triggered manually (workflow_dispatch) or by the /run-cpu button +# (ci-self-cpu-button.yml via workflow_call). T2 (macOS) is intentionally absent. +# Jobs gate on a lane-local detect-changes (same NON_CODE vocabulary as ci.yml) +# so only the diff-affected jobs run; keep the two detect-changes in sync. +# +# Provisioning contract for the cpu runner (out-of-repo, dnf): +# cmake ninja-build gcc-c++ clang-tools-extra graphviz gtest-devel python3-devel +# plus pip: torch (aarch64 CPU wheel — verify availability) and '.[test]' deps. +# g++-15 is a symlink stand-in for the ubuntu-toolchain ppa g++ — see below. + +permissions: + contents: read + +on: + workflow_call: + inputs: + repository: + description: "Target repo (fork PRs pass fork full name); defaults to this repo." + required: false + type: string + ref: + description: "PR head SHA or branch to validate. Empty = ref this run was triggered on." + required: false + type: string + pr_number: + description: "PR number (run naming / concurrency keying)." + required: false + type: string + workflow_dispatch: + inputs: + repository: + description: "Target repo (fork PRs pass fork full name)." + required: false + default: hw-native-sys/simpler + type: string + ref: + description: "PR head SHA or branch to validate." + required: false + type: string + pr_number: + description: "PR number (run naming / concurrency keying)." + required: false + type: string + +concurrency: + group: ci-self-cpu-${{ inputs.pr_number || inputs.ref }} + cancel-in-progress: true + +jobs: + # ---------- Change detection: same NON_CODE vocabulary as ci.yml ---------- + detect-changes: + runs-on: [self-hosted, cpu] + outputs: + a2a3_changed: ${{ steps.check.outputs.a2a3_changed }} + a5_changed: ${{ steps.check.outputs.a5_changed }} + non_code_only: ${{ steps.check.outputs.non_code_only }} + st_affected: ${{ steps.check.outputs.st_affected }} + ut_affected: ${{ steps.check.outputs.ut_affected }} + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + fetch-depth: 0 + - name: Check file changes + id: check + run: | + BASE=$(git merge-base origin/main HEAD) + if ! FILES=$(git diff --name-only "$BASE"...HEAD); then + echo "git diff failed (unresolved base/head SHA?); treating as unattributable" + FILES="" + fi + + # Fail open, in ONE place: no usable file list means every flag runs. + if [ -z "$FILES" ]; then + echo "No usable file list: cannot attribute changes, running everything" + echo "non_code_only=false" >> "$GITHUB_OUTPUT" + echo "a2a3_changed=true" >> "$GITHUB_OUTPUT" + echo "a5_changed=true" >> "$GITHUB_OUTPUT" + echo "st_affected=true" >> "$GITHUB_OUTPUT" + echo "ut_affected=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + NON_CODE='^(docs/|\.docs/|\.claude/|mkdocs\.yml$|\.github/workflows/docs\.yml$|\.gitignore$|\.pre-commit-config\.yaml$)|\.md$' + + CODE=$(echo "$FILES" | grep -vE "$NON_CODE" || true) + if [ -z "$CODE" ]; then + echo "non_code_only=true" >> "$GITHUB_OUTPUT" + else + echo "non_code_only=false" >> "$GITHUB_OUTPUT" + fi + + A5_ONLY='^(src/a5/|examples/a5/|tests/(st|ut/cpp)/a5/)' + A2A3_REMAINING=$(echo "$FILES" | grep -vE "$A5_ONLY" | grep -vE "$NON_CODE" || true) + if [ -n "$A2A3_REMAINING" ]; then + echo "a2a3_changed=true" >> "$GITHUB_OUTPUT" + else + echo "a2a3_changed=false" >> "$GITHUB_OUTPUT" + fi + + A2A3_ONLY='^(src/a2a3/|examples/a2a3/|tests/(st|ut/cpp)/a2a3/)' + A5_REMAINING=$(echo "$FILES" | grep -vE "$A2A3_ONLY" | grep -vE "$NON_CODE" || true) + if [ -n "$A5_REMAINING" ]; then + echo "a5_changed=true" >> "$GITHUB_OUTPUT" + else + echo "a5_changed=false" >> "$GITHUB_OUTPUT" + fi + + ST_ONLY='^(tests/st/|examples/)' + UT_ONLY='^tests/ut/' + + ST_REMAINING=$(echo "$FILES" | grep -vE "$UT_ONLY" | grep -vE "$NON_CODE" || true) + if [ -n "$ST_REMAINING" ]; then + echo "st_affected=true" >> "$GITHUB_OUTPUT" + else + echo "st_affected=false" >> "$GITHUB_OUTPUT" + fi + + UT_REMAINING=$(echo "$FILES" | grep -vE "$ST_ONLY" | grep -vE "$NON_CODE" || true) + if [ -n "$UT_REMAINING" ]; then + echo "ut_affected=true" >> "$GITHUB_OUTPUT" + else + echo "ut_affected=false" >> "$GITHUB_OUTPUT" + fi + + # ---------- T1: no-hardware Linux, on the cpu runner ---------- + pre-commit: + runs-on: [self-hosted, cpu] + timeout-minutes: 30 + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + fetch-depth: 0 + - name: Verify provisioned toolchain + run: | + command -v cmake + command -v ninja + command -v g++ + command -v clang-tidy + command -v graphviz + 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 + - name: venv + deps + 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 . + - name: Build sim runtimes (per-target compile_commands.json for clang-tidy) + run: | + source .venv/bin/activate + python simpler_setup/build_runtimes.py --platforms a2a3sim a5sim + - name: Resolve base SHA (merge-base with target main) + id: base + run: echo "base_sha=$(git merge-base origin/main HEAD)" >> "$GITHUB_OUTPUT" + - name: Run pre-commit + uses: pre-commit/action@v3.0.0 + with: + extra_args: --from-ref ${{ steps.base.outputs.base_sha }} --to-ref ${{ github.sha }} + + ut: + needs: [detect-changes] + if: needs.detect-changes.outputs.non_code_only != 'true' && needs.detect-changes.outputs.ut_affected == 'true' + runs-on: [self-hosted, cpu] + timeout-minutes: 20 + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + fetch-depth: 0 + - name: venv + deps + 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: Run Python unit tests + run: | + source .venv/bin/activate + pytest tests/ut -m "not requires_hardware" -v + - name: Build and run C++ unit tests + run: | + cmake -B tests/ut/cpp/build -S tests/ut/cpp + cmake --build tests/ut/cpp/build + ctest --test-dir tests/ut/cpp/build -LE requires_hardware --output-on-failure + + packaging: + needs: [detect-changes] + if: needs.detect-changes.outputs.non_code_only != 'true' + runs-on: [self-hosted, cpu] + timeout-minutes: 60 + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + - name: Install build + runtime deps in venv + run: | + python3 -m venv .venv + . .venv/bin/activate + pip install --upgrade pip + pip install scikit-build-core nanobind cmake pytest + pip install torch --index-url https://download.pytorch.org/whl/cpu + - name: Run packaging matrix (5 modes x 4 entry points, fully isolated) + run: | + . .venv/bin/activate + bash tools/verify_packaging.sh + + profiling-flags-smoke: + needs: [detect-changes] + if: (needs.detect-changes.outputs.a2a3_changed == 'true' || needs.detect-changes.outputs.a5_changed == 'true') && needs.detect-changes.outputs.st_affected == 'true' + runs-on: [self-hosted, cpu] + timeout-minutes: 30 + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + - name: venv + editable install + 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 -e '.[test]' + - name: Run all profiling flag combos x 2 arches + run: | + source .venv/bin/activate + COMBO_NAMES=(dfx-off orch orch-tensormap sched orch-sched all-on) + COMBO_DEFS=( + "-DSIMPLER_DFX=0" + "-DSIMPLER_ORCH_PROFILING=1" + "-DSIMPLER_ORCH_PROFILING=1 -DSIMPLER_TENSORMAP_PROFILING=1" + "-DSIMPLER_SCHED_PROFILING=1" + "-DSIMPLER_ORCH_PROFILING=1 -DSIMPLER_SCHED_PROFILING=1" + "-DSIMPLER_ORCH_PROFILING=1 -DSIMPLER_SCHED_PROFILING=1 -DSIMPLER_TENSORMAP_PROFILING=1" + ) + ARCHES=(a2a3sim a5sim) + FAIL=() + for ARCH in "${ARCHES[@]}"; do + ARCH_DIR=${ARCH%sim} + for i in "${!COMBO_NAMES[@]}"; do + NAME=${COMBO_NAMES[$i]} + DEFS=${COMBO_DEFS[$i]} + echo "::group::$ARCH / $NAME — CXX defines: $DEFS" + if ! CXX="g++ $DEFS" python simpler_setup/build_runtimes.py --platforms "$ARCH"; then + echo "::error::Build failed: $ARCH / $NAME"; FAIL+=("$ARCH/$NAME (build)") + echo "::endgroup::"; continue + fi + if ! python -m pytest \ + "examples/${ARCH_DIR}/tensormap_and_ringbuffer/vector_example/test_vector_example.py" \ + --platform "$ARCH" --device 0-1 -p no:xdist --pto-session-timeout 600; then + echo "::error::Smoke failed: $ARCH / $NAME"; FAIL+=("$ARCH/$NAME (pytest)") + fi + echo "::endgroup::" + done + done + if [ ${#FAIL[@]} -gt 0 ]; then + printf 'FAILED: %s\n' "${FAIL[@]}"; exit 1 + fi + + st-sim-a2a3: + needs: [detect-changes] + if: needs.detect-changes.outputs.a2a3_changed == 'true' && needs.detect-changes.outputs.st_affected == 'true' + runs-on: [self-hosted, cpu] + timeout-minutes: 30 + env: + SIMPLER_SCHEDULER_TIMEOUT_MS: "5000" + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + - name: Verify provisioned toolchain + run: | + 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 + - name: venv + deps + 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: Run pytest scene tests (a2a3sim) + run: | + source .venv/bin/activate + python -m pytest examples tests/st --platform a2a3sim --device 0-15 -v \ + --pto-session-timeout 600 --require-pto-isa + # DFX per-feature smokes (dep_gen / l2_swimlane / pmu / args_dump) mirror + # ci.yml's st-sim-a2a3; append here if emergency coverage needs them. + + st-sim-a5: + needs: [detect-changes] + if: needs.detect-changes.outputs.a5_changed == 'true' && needs.detect-changes.outputs.st_affected == 'true' + runs-on: [self-hosted, cpu] + timeout-minutes: 30 + env: + SIMPLER_SCHEDULER_TIMEOUT_MS: "5000" + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + - name: Verify provisioned toolchain + run: | + 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 + - name: venv + deps + 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: Run pytest scene tests (a5sim) + run: | + source .venv/bin/activate + python -m pytest examples tests/st --platform a5sim --device 0-15 -v \ + --pto-session-timeout 600 --require-pto-isa + + # ---------- T3: NPU, on the a2a3/a5 runners (never github-hosted) ---------- + ut-a2a3: + needs: [detect-changes] + if: needs.detect-changes.outputs.non_code_only != 'true' && needs.detect-changes.outputs.ut_affected == 'true' + runs-on: [self-hosted, a2a3] + timeout-minutes: 30 + env: + SIMPLER_SCHEDULER_TIMEOUT_MS: "2000" + SIMPLER_OP_EXECUTE_TIMEOUT_US: "3000000" + SIMPLER_STREAM_SYNC_TIMEOUT_MS: "4000" + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + - name: Set up environment + run: | + source /usr/local/Ascend/cann/set_env.sh + python3 -m venv --system-site-packages .venv + source .venv/bin/activate + pip install --upgrade pip + pip install '.[test]' + - name: Run Python hardware unit tests + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + if [ "$(uname -m)" = "x86_64" ]; then + python -m pytest tests -m requires_hardware --platform a2a3 --device ${DEVICE_RANGE} -v + else + task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ + --run "python -m pytest tests -m requires_hardware --platform a2a3 --device \$TASK_DEVICE -v" + fi + - name: Build and run C++ hardware unit tests + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + python -c "from simpler_setup.runtime_builder import RuntimeBuilder; RuntimeBuilder('a2a3').get_binaries('tensormap_and_ringbuffer', build=True)" + cmake -B tests/ut/cpp/build -S tests/ut/cpp -DSIMPLER_ENABLE_HARDWARE_TESTS=ON + cmake --build tests/ut/cpp/build + if [ "$(uname -m)" = "x86_64" ]; then + python3 -c " + import json, os + p = os.environ['DEVICE_RANGE'].split('-'); s, e = p[0], p[-1] + npus = [{'id': str(i), 'slots': 1} for i in range(int(s), int(e)+1)] + json.dump({'version': {'major': 1, 'minor': 0}, 'local': [{'npus': npus}]}, + open('tests/ut/cpp/build/resources.json', 'w')) + " + ctest --test-dir tests/ut/cpp/build -L '^requires_hardware(_a2a3)?$' --resource-spec-file $PWD/tests/ut/cpp/build/resources.json -j$(nproc) --output-on-failure + else + task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ + --run "python3 -c \"import json,os; ids=os.environ['TASK_DEVICE'].split(','); json.dump({'version':{'major':1,'minor':0},'local':[{'npus':[{'id':i,'slots':1} for i in ids]}]}, open('tests/ut/cpp/build/resources.json','w'))\" && ctest --test-dir tests/ut/cpp/build -L '^requires_hardware(_a2a3)?\$' --resource-spec-file $PWD/tests/ut/cpp/build/resources.json -j$(nproc) --output-on-failure" + fi + + st-onboard-a2a3: + needs: [detect-changes] + if: needs.detect-changes.outputs.a2a3_changed == 'true' && needs.detect-changes.outputs.st_affected == 'true' + runs-on: [self-hosted, a2a3] + timeout-minutes: 60 + env: + SIMPLER_SCHEDULER_TIMEOUT_MS: "2000" + SIMPLER_OP_EXECUTE_TIMEOUT_US: "3000000" + SIMPLER_STREAM_SYNC_TIMEOUT_MS: "4000" + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + - name: Set up environment + run: | + source /usr/local/Ascend/cann/set_env.sh + python3 -m venv --system-site-packages .venv + source .venv/bin/activate + pip install --upgrade pip + pip install '.[test]' + - name: Run pytest scene tests (a2a3) + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + SDMA_IGNORE="--ignore=examples/a2a3/tensormap_and_ringbuffer/prefetch_async_demo --ignore=examples/a2a3/tensormap_and_ringbuffer/sdma_async_completion_demo" + if [ "$(uname -m)" = "x86_64" ]; then + python -m pytest examples tests/st $SDMA_IGNORE --platform a2a3 --device ${DEVICE_RANGE} -v --require-pto-isa --pto-session-timeout 600 + else + task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ + --run "python -m pytest examples tests/st $SDMA_IGNORE --platform a2a3 --device \$TASK_DEVICE -v --require-pto-isa --pto-session-timeout 600" + fi + - name: SDMA pytest (a2a3, dedicated device, issue #1425) + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + SDMA_TESTS="examples/a2a3/tensormap_and_ringbuffer/prefetch_async_demo examples/a2a3/tensormap_and_ringbuffer/sdma_async_completion_demo" + if [ "$(uname -m)" = "x86_64" ]; then + python -m pytest $SDMA_TESTS --platform a2a3 --device ${DEVICE_RANGE} -v --require-pto-isa --pto-session-timeout 600 + else + task-submit --timeout 1800 --max-time 1800 --device auto --device-num 2 \ + --run "python -m pytest $SDMA_TESTS --platform a2a3 --device \$TASK_DEVICE -v --require-pto-isa --pto-session-timeout 600" + fi + + ut-a5: + needs: [detect-changes] + if: needs.detect-changes.outputs.non_code_only != 'true' && needs.detect-changes.outputs.ut_affected == 'true' + runs-on: [self-hosted, a5] + timeout-minutes: 30 + env: + SIMPLER_SCHEDULER_TIMEOUT_MS: "2000" + SIMPLER_OP_EXECUTE_TIMEOUT_US: "3000000" + SIMPLER_STREAM_SYNC_TIMEOUT_MS: "4000" + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + - name: Set up environment + run: | + python3 -m venv --system-site-packages .venv + source .venv/bin/activate + pip install --upgrade pip + pip install '.[test]' + - name: Run Python hardware unit tests (a5) + run: | + source .venv/bin/activate + DEVICE_LIST=$(python -c "p='${DEVICE_RANGE}'.split('-'); s,e=p[0],p[-1]; print(','.join(str(i) for i in range(int(s),int(e)+1)))") + task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "python -m pytest tests -m requires_hardware --platform a5 --device ${DEVICE_RANGE} -v" + - name: Build and run C++ hardware unit tests (a5) + run: | + source .venv/bin/activate + cmake -B tests/ut/cpp/build -S tests/ut/cpp -DSIMPLER_ENABLE_HARDWARE_TESTS=ON + cmake --build tests/ut/cpp/build + python3 -c " + import json, os + p = os.environ['DEVICE_RANGE'].split('-'); s, e = p[0], p[-1] + npus = [{'id': str(i), 'slots': 1} for i in range(int(s), int(e)+1)] + json.dump({'version': {'major': 1, 'minor': 0}, 'local': [{'npus': npus}]}, + open('tests/ut/cpp/build/resources.json', 'w')) + " + DEVICE_LIST=$(python -c "p='${DEVICE_RANGE}'.split('-'); s,e=p[0],p[-1]; print(','.join(str(i) for i in range(int(s),int(e)+1)))") + task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "ctest --test-dir tests/ut/cpp/build -L '^requires_hardware(_a5)?\$' --resource-spec-file $PWD/tests/ut/cpp/build/resources.json -j$(nproc) --output-on-failure" + + st-onboard-a5: + needs: [detect-changes] + if: needs.detect-changes.outputs.a5_changed == 'true' && needs.detect-changes.outputs.st_affected == 'true' + runs-on: [self-hosted, a5] + timeout-minutes: 60 + env: + SIMPLER_SCHEDULER_TIMEOUT_MS: "2000" + SIMPLER_OP_EXECUTE_TIMEOUT_US: "3000000" + SIMPLER_STREAM_SYNC_TIMEOUT_MS: "4000" + steps: + - name: Checkout target PR head + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + - name: Set up environment + run: | + python3 -m venv --system-site-packages .venv + source .venv/bin/activate + pip install --upgrade pip + pip install '.[test]' + - name: Run pytest scene tests (a5) + run: | + source .venv/bin/activate + DEVICE_LIST=$(python -c "p='${DEVICE_RANGE}'.split('-'); s,e=p[0],p[-1]; print(','.join(str(i) for i in range(int(s),int(e)+1)))") + PYTEST="python -m pytest examples tests/st --platform a5 --device ${DEVICE_RANGE} -v --require-pto-isa" + task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "$PYTEST --pto-session-timeout 1200" diff --git a/docs/ci.md b/docs/ci.md index 14a916b836..c54e0469b7 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -140,6 +140,28 @@ not need `--max-parallel` manually. - **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 own `task-submit --device auto --device-num 2`, so the two steps are disjoint in devices as well; on x86_64 there is no `task-submit` and 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's `aicore_op_timeout` fault injection must therefore never share a device with them ([#1425](https://github.com/hw-native-sys/simpler/issues/1425)). Selection is by marker on both sides, so the two cannot drift apart; the split can be dropped once #1425 is fixed. Nothing outside `st-onboard-a2a3` filters on the marker, so a local `pytest examples tests/st` still runs everything. +### CPU emergency lane (`ci-self-cpu.yml`) and the `/run-cpu` button + +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-cpu` on the PR (`ci-self-cpu-button.yml`), or + `gh workflow run ci-self-cpu.yml -f repository= -f ref=` manually + (covers forks and arbitrary SHAs). The button gate is + `permission(commenter) == 'admin'` only (`getCollaboratorPermissionLevel`). + `issue_comment` from 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. A lane-local + `detect-changes` (same `NON_CODE` vocabulary and gates as `ci.yml` — keep the + two copies in sync) skips jobs a diff cannot affect. +- **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++-15` is a symlink stand-in for the ubuntu-toolchain ppa g++. +- The lane run is standalone — it attaches no checks to the PR; results are read from the run. + ## Hardware Classification Three hardware tiers, applied to all test categories. See [testing.md](testing.md#hardware-classification) for the full table including per-category mechanisms (pytest markers, ctest labels, folder structure).