From 2857055a63d8fa8b1a5cc2f949716cc55bacbc66 Mon Sep 17 00:00:00 2001 From: MagicDoge Date: Tue, 4 Aug 2026 09:43:49 +0800 Subject: [PATCH] CI: deduplicate reusable CI workflows Extract duplicated CI job bodies from ci.yml and ci-self-cpu.yml into reusable workflows and composite actions. Keep the caller workflows focused on triggers, topology, gates, runner labels, setup variants, and PR/fork checkout inputs while preserving /run-cpu compatibility and the existing test coverage shape. Validation: git diff --check; YAML parse for workflows/actions; actionlint with self-hosted runner labels; full GitHub CI passed before history cleanup. --- .claude/rules/ci-change-detection.md | 61 +- .github/actions/cache-pip/action.yml | 13 + .github/actions/setup-venv/action.yml | 48 + .github/workflows/_detect-changes.yml | 228 ++++ .github/workflows/_packaging.yml | 70 ++ .github/workflows/_pre-commit.yml | 132 +++ .github/workflows/_profiling-flags-smoke.yml | 110 ++ .github/workflows/_st-npu.yml | 241 +++++ .github/workflows/_st-sim.yml | 168 +++ .github/workflows/_ut-no-hardware.yml | 92 ++ .github/workflows/_ut-npu.yml | 146 +++ .github/workflows/ci-self-cpu.yml | 602 ++--------- .github/workflows/ci.yml | 1015 ++---------------- docs/ci.md | 22 +- 14 files changed, 1454 insertions(+), 1494 deletions(-) create mode 100644 .github/actions/cache-pip/action.yml create mode 100644 .github/actions/setup-venv/action.yml create mode 100644 .github/workflows/_detect-changes.yml create mode 100644 .github/workflows/_packaging.yml create mode 100644 .github/workflows/_pre-commit.yml create mode 100644 .github/workflows/_profiling-flags-smoke.yml create mode 100644 .github/workflows/_st-npu.yml create mode 100644 .github/workflows/_st-sim.yml create mode 100644 .github/workflows/_ut-no-hardware.yml create mode 100644 .github/workflows/_ut-npu.yml diff --git a/.claude/rules/ci-change-detection.md b/.claude/rules/ci-change-detection.md index a59396a3f..3893fb732 100644 --- a/.claude/rules/ci-change-detection.md +++ b/.claude/rules/ci-change-detection.md @@ -1,13 +1,15 @@ # CI Change Detection and Job Gating -Every *downstream* PR job in `.github/workflows/ci.yml` is gated by an output -of the `detect-changes` job. Two are deliberately ungated: `detect-changes` -itself, which produces the outputs, and `pre-commit`, which every gated job -declares in `needs:` and which must therefore always run. Those gates decide -whether a change occupies four self-hosted NPU machines for ten minutes or -finishes in ninety seconds on a GitHub runner. They are cheap to get subtly wrong and the symptom is silent — -a job that should have skipped merely *passes*, so nobody notices until a -flake in it reddens an unrelated PR. +Every *downstream* PR job in `.github/workflows/ci.yml` and the emergency CPU +lane in `.github/workflows/ci-self-cpu.yml` is gated by an output of the +canonical `.github/workflows/_detect-changes.yml` reusable workflow. Two jobs +are deliberately ungated in the main CI path: `detect-changes` itself, which +produces the outputs, and `pre-commit`, which every gated job declares in +`needs:` and which must therefore always run. Those gates decide whether a +change occupies four self-hosted NPU machines for ten minutes or finishes in +ninety seconds on a GitHub runner. They are cheap to get subtly wrong and the +symptom is silent — a job that should have skipped merely *passes*, so nobody +notices until a flake in it reddens an unrelated PR. This rule is about keeping the gates honest. It is not a description of the current filters; those change. It is the set of invariants they must satisfy. @@ -36,9 +38,10 @@ A file belongs in `NON_CODE` when changing it cannot change what the code does - `mkdocs.yml` sits at the repo root and is pure docs tooling. **In.** - `.github/workflows/docs.yml` is a workflow, and is also pure docs tooling — and it runs unconditionally on every PR, so it is its own gate. **In.** -- `.github/workflows/ci.yml` defines the gates themselves. **Out, always.** A - change to the gating must run everything, including the jobs it might have - just switched off. +- `.github/workflows/ci.yml`, `.github/workflows/ci-self-cpu.yml`, and the + reusable `.github/workflows/_*.yml` CI implementation workflows define gates + or job bodies. **Out, always.** A change to CI implementation must run + everything, including the jobs it might have just switched off. The corollary is a habit, not a pattern: **adding a top-level config file or a tooling workflow is a change-detection event.** Ask whether `NON_CODE` needs to @@ -94,13 +97,13 @@ contracts, so the cost of a falsely-skipped regression outweighs the minutes. That is a decision about the *arch* axis only; the category axis is a different question, because a scene-test-only change genuinely cannot break a unit test. -The vocabulary exists twice. `ci-self-cpu.yml` runs its own lane-local -`detect-changes` with the same outputs, and its file header says the two must -be kept in sync. That has now failed twice — `examples_only` never reached the -lane (#1607), and `tests_only` did not either (#1635) — because the gate tables -here only name `ci.yml`. Any change to an axis therefore means two files: -`.github/workflows/ci.yml` and `.github/workflows/ci-self-cpu.yml`, same commit, -and the outputs-consistency grep in §7 runs against both. +The vocabulary exists once, in `.github/workflows/_detect-changes.yml`. Both +`.github/workflows/ci.yml` and `.github/workflows/ci-self-cpu.yml` call that +workflow and consume the same output names. Do not reintroduce a lane-local +copy. That copy drifted twice — `examples_only` never reached the CPU lane +(#1607), and `tests_only` did not either (#1635). Any change to an axis now +means one detector file plus any consumers whose `if:` expression actually +changes. ### Write every axis in the same shape @@ -176,15 +179,23 @@ Confirm the jobs you expected to skip report `skipping`, not `pass`. **A gating bug shows up as a green check, so "CI passed" is not evidence.** Where the change is to `NON_CODE` itself, replay the pattern locally against representative file lists — a docs-only set, a `.gitignore`-only set, a -single-arch set, a `ci.yml` set — and check all four land where you intended -before pushing. Where the change is to an *axis*, also verify both workflow -copies expose the same output set — the lane drifted twice because only -`ci.yml` was checked: +single-arch set, a CI workflow set — and check all four land where you +intended before pushing. Where the change is to an *axis*, also verify both callers +consume only outputs declared by the canonical reusable workflow: ```bash -for f in ci.yml ci-self-cpu.yml; do - echo "== $f"; grep -oE 'outputs\.[a-z_]+' ".github/workflows/$f" | sort -u -done # the two lists must be identical +echo "== declared by _detect-changes.yml" +awk ' + /^jobs:/ { in_outputs = 0; seen_jobs = 1; next } + !seen_jobs && /^ outputs:/ { in_outputs = 1; next } + in_outputs && /^ [a-z0-9_]+:/ { in_outputs = 0 } + in_outputs && /^ [a-z0-9_]+:/ { print } +' .github/workflows/_detect-changes.yml | sed 's/[ :]*//g' | sort -u + +echo "== consumed by callers" +grep -h -oE 'needs\.detect-changes\.outputs\.[a-z0-9_]+' \ + .github/workflows/ci.yml .github/workflows/ci-self-cpu.yml | + sed 's/.*outputs\.//' | sort -u ``` ## Relation to the other rules diff --git a/.github/actions/cache-pip/action.yml b/.github/actions/cache-pip/action.yml new file mode 100644 index 000000000..7e2085c36 --- /dev/null +++ b/.github/actions/cache-pip/action.yml @@ -0,0 +1,13 @@ +name: Cache pip packages +description: Restore and save the per-runner pip cache. + +runs: + using: composite + steps: + - name: Cache pip packages + uses: actions/cache@v5 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} + restore-keys: | + ${{ runner.os }}-pip- diff --git a/.github/actions/setup-venv/action.yml b/.github/actions/setup-venv/action.yml new file mode 100644 index 000000000..0a1715150 --- /dev/null +++ b/.github/actions/setup-venv/action.yml @@ -0,0 +1,48 @@ +name: Set up venv +description: Create .venv and install Python dependencies. + +inputs: + packages: + description: Arguments passed to pip install after torch, for example ".[test]" or "-e .[test]". + required: false + default: "" + install-torch: + description: Install the CPU torch wheel before packages. + required: false + default: "true" + system-site-packages: + description: Create the venv with --system-site-packages. + required: false + default: "false" + source-cann: + description: Source the CANN environment before creating the venv. + required: false + default: "false" + +runs: + using: composite + steps: + - name: Create venv and install dependencies + shell: bash + env: + PACKAGES: ${{ inputs.packages }} + run: | + set -f + if [ "${{ inputs.source-cann }}" = "true" ]; then + source /usr/local/Ascend/cann/set_env.sh + fi + + VENV_ARGS=() + if [ "${{ inputs.system-site-packages }}" = "true" ]; then + VENV_ARGS+=(--system-site-packages) + fi + + python3 -m venv "${VENV_ARGS[@]}" .venv + source .venv/bin/activate + pip install --upgrade pip + if [ "${{ inputs.install-torch }}" = "true" ]; then + pip install torch --index-url https://download.pytorch.org/whl/cpu + fi + if [ -n "$PACKAGES" ]; then + pip install $PACKAGES + fi diff --git a/.github/workflows/_detect-changes.yml b/.github/workflows/_detect-changes.yml new file mode 100644 index 000000000..41200e2e7 --- /dev/null +++ b/.github/workflows/_detect-changes.yml @@ -0,0 +1,228 @@ +name: Detect Changes + +permissions: + contents: read + +on: + workflow_call: + inputs: + repository: + description: "Repository containing the head ref to classify." + required: false + type: string + ref: + description: "Head ref or SHA to classify." + required: false + type: string + base_repository: + description: "Canonical base repository used for merge-base calculation." + required: false + type: string + base_ref: + description: "Canonical base branch to fetch when base_sha is not enough or not provided." + required: false + default: main + type: string + base_sha: + description: "Exact base SHA, usually github.event.pull_request.base.sha." + required: false + type: string + head_sha: + description: "Exact head SHA, usually github.event.pull_request.head.sha." + required: false + type: string + runs_on: + description: "JSON-encoded runs-on value for the detector job." + required: false + default: '["ubuntu-latest"]' + type: string + outputs: + a2a3_changed: + description: "Whether the diff can affect a2a3." + value: ${{ jobs.detect.outputs.a2a3_changed }} + a5_changed: + description: "Whether the diff can affect a5." + value: ${{ jobs.detect.outputs.a5_changed }} + non_code_only: + description: "Whether every changed file is non-code." + value: ${{ jobs.detect.outputs.non_code_only }} + st_affected: + description: "Whether scene tests can be affected." + value: ${{ jobs.detect.outputs.st_affected }} + ut_affected: + description: "Whether unit tests can be affected." + value: ${{ jobs.detect.outputs.ut_affected }} + examples_only: + description: "Whether every code change is under examples/." + value: ${{ jobs.detect.outputs.examples_only }} + tests_only: + description: "Whether every code change is under tests/." + value: ${{ jobs.detect.outputs.tests_only }} + +jobs: + detect: + name: detect-changes + runs-on: ${{ fromJSON(inputs.runs_on) }} + 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 }} + examples_only: ${{ steps.check.outputs.examples_only }} + tests_only: ${{ steps.check.outputs.tests_only }} + steps: + - name: Checkout target ref + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || inputs.head_sha || github.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Check file changes + id: check + env: + BASE_REPOSITORY: ${{ inputs.base_repository || github.repository }} + BASE_REF: ${{ inputs.base_ref || 'main' }} + BASE_SHA: ${{ inputs.base_sha }} + HEAD_SHA: ${{ inputs.head_sha }} + run: | + HEAD_REV=HEAD + if [ -n "$HEAD_SHA" ]; then + if git cat-file -e "${HEAD_SHA}^{commit}" 2>/dev/null; then + HEAD_REV="$HEAD_SHA" + else + echo "head_sha $HEAD_SHA is not present after checkout; using HEAD" + fi + fi + + BASE_URL="https://github.com/${BASE_REPOSITORY}.git" + BASE_REMOTE="refs/remotes/base/${BASE_REF}" + BASE_FETCHED=false + if git fetch --no-tags "$BASE_URL" "+refs/heads/${BASE_REF}:${BASE_REMOTE}"; then + BASE_FETCHED=true + else + echo "base fetch failed for ${BASE_REPOSITORY}/${BASE_REF}; treating diff as unattributable" + fi + + BASE="" + if [ -n "$BASE_SHA" ]; then + if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then + git fetch --no-tags "$BASE_URL" "$BASE_SHA" || true + fi + BASE="$BASE_SHA" + elif [ "$BASE_FETCHED" = "true" ]; then + if ! BASE=$(git merge-base "$BASE_REMOTE" "$HEAD_REV"); then + echo "merge-base failed for $BASE_REMOTE...$HEAD_REV; treating diff as unattributable" + BASE="" + fi + fi + + # `run:` is `bash -e`, so a failing git diff would abort the step + # before the guard below and take every output with it. Let only the + # exit status decide, and turn a failure into an empty list so the + # fail-open guard handles both cases identically. + if [ -z "$BASE" ] || ! git cat-file -e "${BASE}^{commit}" 2>/dev/null; then + echo "No usable base commit: cannot attribute changes" + FILES="" + elif ! FILES=$(git diff --name-only "${BASE}...${HEAD_REV}"); then + echo "git diff failed (unresolved base/head SHA?); treating as unattributable" + FILES="" + fi + + # Fail open, in ONE place. No usable file list means attribution is + # impossible, so every affected flag must say "run". + 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" + echo "examples_only=false" >> "$GITHUB_OUTPUT" + echo "tests_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "Changed files:" + echo "$FILES" + + # ONE definition of "changing this cannot change what the code does". + # Every gate below derives from this set. CI implementation workflows + # are deliberately absent: a change to gates or job bodies must run + # everything. + 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" + echo "No file here can change what the code does; pre-commit + docs are the whole gate" + else + echo "non_code_only=false" >> "$GITHUB_OUTPUT" + fi + + # Architecture axis: skip a partition only when every changed file is + # in the sibling partition or non-code. + 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" + echo "Files affecting a2a3:" + echo "$A2A3_REMAINING" + else + echo "a2a3_changed=false" >> "$GITHUB_OUTPUT" + echo "All changes are a5-only or non-code; skipping a2a3" + 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" + echo "Files affecting a5:" + echo "$A5_REMAINING" + else + echo "a5_changed=false" >> "$GITHUB_OUTPUT" + echo "All changes are a2a3-only or non-code; skipping a5" + fi + + # Test-category axis, same shape as the arch axis. + 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" + echo "All changes are unit-test-only or non-code; skipping scene tests" + 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" + echo "All changes are scene-test/example-only or non-code; skipping unit tests" + fi + + # Corpus axis: product-build jobs read neither examples/ nor tests/ + # as a corpus, so a diff confined to either partition cannot reach + # them and is covered by the scene-test job that reads the payload. + EXAMPLES_ONLY='^examples/' + NON_EXAMPLE=$(echo "$FILES" | grep -vE "$EXAMPLES_ONLY" | grep -vE "$NON_CODE" || true) + if [ -n "$NON_EXAMPLE" ]; then + echo "examples_only=false" >> "$GITHUB_OUTPUT" + else + echo "examples_only=true" >> "$GITHUB_OUTPUT" + echo "All changes are under examples/ or non-code; skipping packaging and the profiling-flag smoke" + fi + + TESTS_ONLY='^(tests/)' + NON_TEST=$(echo "$FILES" | grep -vE "$TESTS_ONLY" | grep -vE "$NON_CODE" || true) + if [ -n "$NON_TEST" ]; then + echo "tests_only=false" >> "$GITHUB_OUTPUT" + else + echo "tests_only=true" >> "$GITHUB_OUTPUT" + echo "All changes are under tests/ or non-code; skipping packaging and the profiling-flag smoke" + fi diff --git a/.github/workflows/_packaging.yml b/.github/workflows/_packaging.yml new file mode 100644 index 000000000..cdc41afca --- /dev/null +++ b/.github/workflows/_packaging.yml @@ -0,0 +1,70 @@ +name: Packaging Matrix + +permissions: + contents: read + +on: + workflow_call: + inputs: + repository: + required: false + type: string + ref: + required: false + type: string + runs_on: + required: true + type: string + setup_variant: + description: github or self-cpu. + required: true + type: string + python_version: + required: false + default: "3.10" + type: string + +jobs: + run: + name: packaging + runs-on: ${{ fromJSON(inputs.runs_on) }} + timeout-minutes: 60 + steps: + - name: Checkout target + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.ref }} + persist-credentials: false + + - name: Set up C++ compiler (Linux) + if: inputs.setup_variant == 'github' && runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build g++ + + - name: Set up C++ compiler (macOS) + if: inputs.setup_variant == 'github' && runner.os == 'macOS' + run: | + brew install ninja cmake + brew install gcc@15 || brew install gcc + + - name: Set up Python ${{ inputs.python_version }} + if: inputs.setup_variant == 'github' + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python_version }} + + - name: Cache pip packages + if: inputs.setup_variant == 'self-cpu' + uses: ./.github/actions/cache-pip + + - name: Install build + runtime deps in venv + uses: ./.github/actions/setup-venv + with: + packages: "scikit-build-core nanobind cmake pytest" + + - name: Run packaging matrix + run: | + . .venv/bin/activate + bash tools/verify_packaging.sh diff --git a/.github/workflows/_pre-commit.yml b/.github/workflows/_pre-commit.yml new file mode 100644 index 000000000..3dd8888fb --- /dev/null +++ b/.github/workflows/_pre-commit.yml @@ -0,0 +1,132 @@ +name: Pre-commit + +permissions: + contents: read + +on: + workflow_call: + inputs: + repository: + description: Repository to checkout. + required: false + type: string + ref: + description: Ref or SHA to checkout. + required: false + type: string + base_repository: + description: Canonical base repository for self-cpu diff resolution. + required: false + type: string + base_sha: + description: Exact base SHA for GitHub-hosted PR runs. + required: false + type: string + head_sha: + description: Exact head SHA for GitHub-hosted PR runs. + required: false + type: string + runs_on: + description: JSON-encoded runs-on value. + required: true + type: string + setup_variant: + description: github or self-cpu. + required: true + type: string + +jobs: + run: + name: pre-commit + runs-on: ${{ fromJSON(inputs.runs_on) }} + timeout-minutes: 30 + steps: + - name: Checkout target + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.ref }} + fetch-depth: 0 + persist-credentials: false + + - name: Install build and lint tools + if: inputs.setup_variant == 'github' + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build g++ clang-tidy + + - name: Verify provisioned toolchain + if: inputs.setup_variant == 'self-cpu' + run: | + command -v cmake + command -v ninja + command -v g++ + command -v clang-tidy + 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 + if command -v clang-tidy-18 >/dev/null 2>&1; then + mkdir -p "$RUNNER_TEMP/bin" + ln -sf "$(command -v clang-tidy-18)" "$RUNNER_TEMP/bin/clang-tidy" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + fi + PATH="$RUNNER_TEMP/bin:$PATH" command -v clang-tidy + PATH="$RUNNER_TEMP/bin:$PATH" clang-tidy --version + + - name: Set up Python 3.10 + if: inputs.setup_variant == 'github' + uses: actions/setup-python@v6 + with: + python-version: "3.10" + + - name: Cache pip packages + if: inputs.setup_variant == 'self-cpu' + uses: ./.github/actions/cache-pip + + - name: Install Python dependencies + if: inputs.setup_variant == 'github' + run: pip install torch --index-url https://download.pytorch.org/whl/cpu + + - name: Install package (for pyright C extension symbol resolution) + if: inputs.setup_variant == 'github' + run: pip install . + + - name: venv + deps + if: inputs.setup_variant == 'self-cpu' + uses: ./.github/actions/setup-venv + with: + packages: "." + + - name: Cache cmake build directories + if: inputs.setup_variant == 'github' + uses: actions/cache@v5 + with: + path: build/cache + key: cmake-sim-${{ runner.os }}-${{ hashFiles('src/**/CMakeLists.txt', 'src/**/build_config.py', 'simpler_setup/runtime_compiler.py', 'simpler_setup/kernel_compiler.py', 'simpler_setup/toolchain.py') }} + restore-keys: | + cmake-sim-${{ runner.os }}- + + - name: Build sim runtimes + run: | + if [ "${{ inputs.setup_variant }}" = "self-cpu" ]; then + source .venv/bin/activate + fi + python simpler_setup/build_runtimes.py --platforms a2a3sim a5sim + + - name: Resolve base SHA + id: base + if: inputs.setup_variant == 'self-cpu' + env: + BASE_REPOSITORY: ${{ inputs.base_repository || github.repository }} + run: | + git fetch --no-tags "https://github.com/${BASE_REPOSITORY}.git" main + echo "base_sha=$(git merge-base FETCH_HEAD HEAD)" >> "$GITHUB_OUTPUT" + + - name: Run pre-commit + uses: pre-commit/action@v3.0.0 + with: + extra_args: >- + --from-ref ${{ inputs.setup_variant == 'self-cpu' && steps.base.outputs.base_sha || inputs.base_sha }} + --to-ref ${{ inputs.setup_variant == 'self-cpu' && (inputs.ref || github.sha) || inputs.head_sha }} diff --git a/.github/workflows/_profiling-flags-smoke.yml b/.github/workflows/_profiling-flags-smoke.yml new file mode 100644 index 000000000..52d6320bf --- /dev/null +++ b/.github/workflows/_profiling-flags-smoke.yml @@ -0,0 +1,110 @@ +name: Profiling Flags Smoke + +permissions: + contents: read + +on: + workflow_call: + inputs: + repository: + required: false + type: string + ref: + required: false + type: string + runs_on: + required: true + type: string + setup_variant: + description: github or self-cpu. + required: true + type: string + +jobs: + run: + name: profiling-flags-smoke + runs-on: ${{ fromJSON(inputs.runs_on) }} + timeout-minutes: 30 + steps: + - name: Checkout target + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.ref }} + persist-credentials: false + + - name: Set up C++ compiler + if: inputs.setup_variant == 'github' + run: | + 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 + + - name: Set up Python 3.10 + if: inputs.setup_variant == 'github' + uses: actions/setup-python@v6 + with: + python-version: "3.10" + + - name: Cache pip packages + uses: ./.github/actions/cache-pip + + - name: Install dependencies + if: inputs.setup_variant == 'github' + run: | + pip install torch --index-url https://download.pytorch.org/whl/cpu + pip install -e '.[test]' + + - name: venv + editable install + if: inputs.setup_variant == 'self-cpu' + uses: ./.github/actions/setup-venv + with: + packages: "-e .[test]" + + - name: Run all profiling flag combos x 2 arches + run: | + if [ "${{ inputs.setup_variant }}" = "self-cpu" ]; then + source .venv/bin/activate + fi + + 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 diff --git a/.github/workflows/_st-npu.yml b/.github/workflows/_st-npu.yml new file mode 100644 index 000000000..8e4482e07 --- /dev/null +++ b/.github/workflows/_st-npu.yml @@ -0,0 +1,241 @@ +name: NPU Scene Tests + +permissions: + contents: read + +on: + workflow_call: + inputs: + repository: + required: false + type: string + ref: + required: false + type: string + platform: + description: a2a3 or a5. + required: true + type: string + runs_on: + required: true + type: string + include_dfx_smokes: + required: false + default: false + type: boolean + a2a3_sdma_mode: + description: marker for main CI, legacy-paths for the CPU emergency lane. + required: false + default: marker + type: string + +jobs: + run: + name: st-onboard-${{ inputs.platform }} + runs-on: ${{ fromJSON(inputs.runs_on) }} + 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 + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.ref }} + persist-credentials: false + + - name: Set up environment + uses: ./.github/actions/setup-venv + with: + packages: ".[test]" + install-torch: "false" + system-site-packages: "true" + source-cann: ${{ inputs.platform == 'a2a3' && 'true' || 'false' }} + + - name: Warn when graphviz is missing + if: inputs.platform == 'a2a3' && inputs.include_dfx_smokes + run: | + if ! command -v dot >/dev/null 2>&1; then + echo "::warning::graphviz 'dot' not found on PATH; deps_viewer tool smoke will skip. Install graphviz on this self-hosted runner to restore coverage." + fi + + - name: Run pytest scene tests (a2a3) + if: inputs.platform == 'a2a3' && inputs.a2a3_sdma_mode == 'marker' + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + if [ "$(uname -m)" = "x86_64" ]; then + python -m pytest examples tests/st -m "not sdma" --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 -m 'not sdma' --platform a2a3 --device \$TASK_DEVICE -v --require-pto-isa --pto-session-timeout 600" + fi + + - name: Run pytest scene tests (a2a3 legacy SDMA paths) + if: inputs.platform == 'a2a3' && inputs.a2a3_sdma_mode == 'legacy-paths' + 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) + if: inputs.platform == 'a2a3' && inputs.a2a3_sdma_mode == 'marker' + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + SDMA_TESTS="examples tests/st -m sdma" + 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 + + - name: SDMA pytest (a2a3 legacy paths) + if: inputs.platform == 'a2a3' && inputs.a2a3_sdma_mode == 'legacy-paths' + 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 + + - name: Run pytest scene tests (a5) + if: inputs.platform == '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" + + - name: dep_gen smoke (a2a3) + if: inputs.platform == 'a2a3' && inputs.include_dfx_smokes + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + if [ "$(uname -m)" = "x86_64" ]; then + python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ + --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-dep-gen + else + task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ + --run "python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ + --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-dep-gen" + fi + + - name: dep_gen smoke (a2a3 host_build_graph) + if: inputs.platform == 'a2a3' && inputs.include_dfx_smokes + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + if [ "$(uname -m)" = "x86_64" ]; then + python -m pytest tests/st/a2a3/host_build_graph/dfx/dep_gen/test_dep_gen.py \ + --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-dep-gen + else + task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ + --run "python -m pytest tests/st/a2a3/host_build_graph/dfx/dep_gen/test_dep_gen.py \ + --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-dep-gen" + fi + + - name: l2_swimlane smoke (a2a3) + if: inputs.platform == 'a2a3' && inputs.include_dfx_smokes + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + if [ "$(uname -m)" = "x86_64" ]; then + python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ + --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-l2-swimlane --enable-dep-gen + else + task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ + --run "python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ + --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-l2-swimlane --enable-dep-gen" + fi + + - name: PMU smoke (a2a3) + if: inputs.platform == 'a2a3' && inputs.include_dfx_smokes + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + if [ "$(uname -m)" = "x86_64" ]; then + python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ + --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-pmu 2 + else + task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ + --run "python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ + --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-pmu 2" + fi + + - name: args_dump smoke (a2a3) + if: inputs.platform == 'a2a3' && inputs.include_dfx_smokes + run: | + source /usr/local/Ascend/cann/set_env.sh + source .venv/bin/activate + if [ "$(uname -m)" = "x86_64" ]; then + python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ + --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --dump-args + else + task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ + --run "python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ + --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --dump-args" + fi + + - name: dep_gen smoke (a5) + if: inputs.platform == 'a5' && inputs.include_dfx_smokes + 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 tests/st/a5/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ + --platform a5 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-dep-gen" + task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "$PYTEST" + + - name: l2_swimlane smoke (a5) + if: inputs.platform == 'a5' && inputs.include_dfx_smokes + 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 tests/st/a5/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ + --platform a5 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-l2-swimlane --enable-dep-gen" + task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "$PYTEST" + + - name: PMU smoke (a5) + if: inputs.platform == 'a5' && inputs.include_dfx_smokes + 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 tests/st/a5/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ + --platform a5 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --enable-pmu 2" + task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "$PYTEST" + + - name: args_dump smoke (a5) + if: inputs.platform == 'a5' && inputs.include_dfx_smokes + 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 tests/st/a5/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ + --platform a5 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ + --require-pto-isa --dump-args" + task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "$PYTEST" diff --git a/.github/workflows/_st-sim.yml b/.github/workflows/_st-sim.yml new file mode 100644 index 000000000..a46d25f11 --- /dev/null +++ b/.github/workflows/_st-sim.yml @@ -0,0 +1,168 @@ +name: Simulation Scene Tests + +permissions: + contents: read + +on: + workflow_call: + inputs: + repository: + required: false + type: string + ref: + required: false + type: string + runs_on: + required: true + type: string + setup_variant: + description: github or self-cpu. + required: true + type: string + platform: + description: a2a3sim or a5sim. + required: true + type: string + include_dfx_smokes: + required: false + default: false + type: boolean + python_version: + required: false + default: "3.10" + type: string + +jobs: + run: + name: st-sim-${{ inputs.platform }} + runs-on: ${{ fromJSON(inputs.runs_on) }} + timeout-minutes: 30 + env: + SIMPLER_SCHEDULER_TIMEOUT_MS: "5000" + steps: + - name: Checkout target + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.ref }} + persist-credentials: false + + - name: Set up C++ compiler + if: inputs.setup_variant == 'github' + 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 + sudo apt-get install -y graphviz + else + brew install ninja + brew install gcc@15 || brew install gcc + brew install graphviz + fi + + - name: Verify provisioned toolchain + if: inputs.setup_variant == 'self-cpu' + 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: Set up Python ${{ inputs.python_version }} + if: inputs.setup_variant == 'github' + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python_version }} + + - name: Cache pip packages + uses: ./.github/actions/cache-pip + + - name: Install dependencies + if: inputs.setup_variant == 'github' + run: | + pip install torch --index-url https://download.pytorch.org/whl/cpu + pip install '.[test]' + + - name: venv + deps + if: inputs.setup_variant == 'self-cpu' + uses: ./.github/actions/setup-venv + with: + packages: ".[test]" + + - name: Run pytest scene tests + run: | + if [ "${{ inputs.setup_variant }}" = "self-cpu" ]; then + source .venv/bin/activate + fi + python -m pytest examples tests/st --platform "${{ inputs.platform }}" --device 0-15 -v \ + --pto-session-timeout 600 --require-pto-isa + + - name: dep_gen smoke (a2a3) + if: inputs.include_dfx_smokes && inputs.platform == 'a2a3sim' + run: | + 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 + + - name: dep_gen smoke (a2a3 host_build_graph) + if: inputs.include_dfx_smokes && inputs.platform == 'a2a3sim' + run: | + 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 (a2a3) + if: inputs.include_dfx_smokes && inputs.platform == 'a2a3sim' + run: | + 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 (a2a3) + if: inputs.include_dfx_smokes && inputs.platform == 'a2a3sim' + run: | + 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 (a2a3) + if: inputs.include_dfx_smokes && inputs.platform == 'a2a3sim' + run: | + 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 + + - name: dep_gen smoke (a5) + if: inputs.include_dfx_smokes && inputs.platform == 'a5sim' + run: | + 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 (a5) + if: inputs.include_dfx_smokes && inputs.platform == 'a5sim' + run: | + 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 (a5) + if: inputs.include_dfx_smokes && inputs.platform == 'a5sim' + run: | + 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 (a5) + if: inputs.include_dfx_smokes && inputs.platform == 'a5sim' + run: | + 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 diff --git a/.github/workflows/_ut-no-hardware.yml b/.github/workflows/_ut-no-hardware.yml new file mode 100644 index 000000000..d133c9bca --- /dev/null +++ b/.github/workflows/_ut-no-hardware.yml @@ -0,0 +1,92 @@ +name: No-hardware Unit Tests + +permissions: + contents: read + +on: + workflow_call: + inputs: + repository: + required: false + type: string + ref: + required: false + type: string + runs_on: + required: true + type: string + setup_variant: + description: github or self-cpu. + required: true + type: string + python_version: + required: false + default: "3.10" + type: string + +jobs: + run: + name: ut + runs-on: ${{ fromJSON(inputs.runs_on) }} + timeout-minutes: 20 + steps: + - name: Checkout target + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.ref }} + fetch-depth: ${{ inputs.setup_variant == 'self-cpu' && '0' || '1' }} + persist-credentials: false + + - name: Set up Python ${{ inputs.python_version }} + if: inputs.setup_variant == 'github' + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python_version }} + + - name: Install GoogleTest (Linux) + if: inputs.setup_variant == 'github' && runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y cmake libgtest-dev + cd /usr/src/googletest + sudo cmake -B build + sudo cmake --build build + sudo cmake --install build + + - name: Install GoogleTest (macOS) + if: inputs.setup_variant == 'github' && runner.os == 'macOS' + run: brew install googletest + + - name: Cache pip packages + uses: ./.github/actions/cache-pip + + - name: Install dependencies + if: inputs.setup_variant == 'github' + run: | + pip install torch --index-url https://download.pytorch.org/whl/cpu + pip install '.[test]' + + - name: venv + deps + if: inputs.setup_variant == 'self-cpu' + uses: ./.github/actions/setup-venv + with: + packages: ".[test]" + + - name: Run Python unit tests + run: | + if [ "${{ inputs.setup_variant }}" = "self-cpu" ]; then + source .venv/bin/activate + python -m pytest tests/ut -m "not requires_hardware" -v + else + pytest tests/ut -m "not requires_hardware" -v + fi + + - name: Build and run C++ unit tests + run: | + if [ "${{ inputs.setup_variant }}" = "self-cpu" ]; then + source .venv/bin/activate + fi + 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 diff --git a/.github/workflows/_ut-npu.yml b/.github/workflows/_ut-npu.yml new file mode 100644 index 000000000..de24150ca --- /dev/null +++ b/.github/workflows/_ut-npu.yml @@ -0,0 +1,146 @@ +name: NPU Unit Tests + +permissions: + contents: read + +on: + workflow_call: + inputs: + repository: + required: false + type: string + ref: + required: false + type: string + platform: + description: a2a3 or a5. + required: true + type: string + runs_on: + required: true + type: string + include_cann_examples: + required: false + default: false + type: boolean + +jobs: + run: + name: ut-${{ inputs.platform }} + runs-on: ${{ fromJSON(inputs.runs_on) }} + 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 + uses: actions/checkout@v5 + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.ref }} + persist-credentials: false + + - name: Set up environment + uses: ./.github/actions/setup-venv + with: + packages: ".[test]" + install-torch: "false" + system-site-packages: "true" + source-cann: ${{ inputs.platform == 'a2a3' && 'true' || 'false' }} + + - name: Run Python hardware unit tests (a2a3) + if: inputs.platform == 'a2a3' + 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 (a2a3) + if: inputs.platform == 'a2a3' + 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 + + - name: Run Python hardware unit tests (a5) + if: inputs.platform == '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) + if: inputs.platform == '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" + + - name: Build cann-examples/query + if: inputs.include_cann_examples + run: | + if [ "${{ inputs.platform }}" = "a2a3" ]; then + source /usr/local/Ascend/cann/set_env.sh + fi + cd tools/cann-examples/query + cmake -B build . + cmake --build build + ./build/query version + + - name: Build cann-examples/aicpu-device-query + if: inputs.include_cann_examples + run: | + if [ "${{ inputs.platform }}" = "a2a3" ]; then + source /usr/local/Ascend/cann/set_env.sh + fi + export CROSS=${ASCEND_HOME_PATH}/tools/hcc/bin/aarch64-target-linux-gnu + cd tools/cann-examples/aicpu-device-query/device + cmake -B build . -DCMAKE_C_COMPILER=${CROSS}-gcc -DCMAKE_CXX_COMPILER=${CROSS}-g++ + cmake --build build + cd ../host + cmake -B build . + cmake --build build + + - name: Build cann-examples/aicpu-kernel-launch + if: inputs.include_cann_examples + run: | + if [ "${{ inputs.platform }}" = "a2a3" ]; then + source /usr/local/Ascend/cann/set_env.sh + fi + export CROSS=${ASCEND_HOME_PATH}/tools/hcc/bin/aarch64-target-linux-gnu + cd tools/cann-examples/aicpu-kernel-launch/device + cmake -B build . -DCMAKE_C_COMPILER=${CROSS}-gcc -DCMAKE_CXX_COMPILER=${CROSS}-g++ + cmake --build build + cd ../host + cmake -B build . + cmake --build build diff --git a/.github/workflows/ci-self-cpu.yml b/.github/workflows/ci-self-cpu.yml index 1cc60d9d2..0fe1361f2 100644 --- a/.github/workflows/ci-self-cpu.yml +++ b/.github/workflows/ci-self-cpu.yml @@ -1,12 +1,12 @@ 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. +# Emergency lane: T1 no-hardware Linux runs on [self-hosted, cpu], while T3 +# NPU jobs run on the a2a3/a5 pools. The lane stays callable from /run-cpu via +# workflow_call and from manual workflow_dispatch. T2 (macOS) is intentionally +# absent. Job bodies live in reusable workflows shared with ci.yml, and only the +# runner/setup inputs differ here; gate outputs come from the canonical reusable +# detect-changes workflow, executed on [self-hosted, cpu] in this lane. # # Provisioning contract for the cpu runner (out-of-repo, dnf): every cpu agent # needs the same set — cmake ninja-build gcc-c++ clang-tools-extra graphviz @@ -60,555 +60,119 @@ concurrency: 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 }} - examples_only: ${{ steps.check.outputs.examples_only }} - tests_only: ${{ steps.check.outputs.tests_only }} - 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 + uses: ./.github/workflows/_detect-changes.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + base_repository: ${{ github.repository }} + base_ref: main + runs_on: '["self-hosted","cpu"]' - # 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" - echo "examples_only=false" >> "$GITHUB_OUTPUT" - echo "tests_only=false" >> "$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 - - # Corpus axis, mirroring ci.yml: a diff confined to examples/ or to - # tests/ cannot reach the product jobs, whose payload files live in - # those partitions but are exercised by the scene-test job instead. - EXAMPLES_ONLY='^examples/' - NON_EXAMPLE=$(echo "$FILES" | grep -vE "$EXAMPLES_ONLY" | grep -vE "$NON_CODE" || true) - if [ -n "$NON_EXAMPLE" ]; then - echo "examples_only=false" >> "$GITHUB_OUTPUT" - else - echo "examples_only=true" >> "$GITHUB_OUTPUT" - fi - - TESTS_ONLY='^(tests/)' - NON_TEST=$(echo "$FILES" | grep -vE "$TESTS_ONLY" | grep -vE "$NON_CODE" || true) - if [ -n "$NON_TEST" ]; then - echo "tests_only=false" >> "$GITHUB_OUTPUT" - else - echo "tests_only=true" >> "$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 - 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 - if command -v clang-tidy-18 >/dev/null 2>&1; then - mkdir -p "$RUNNER_TEMP/bin" - ln -sf "$(command -v clang-tidy-18)" "$RUNNER_TEMP/bin/clang-tidy" - echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" - fi - PATH="$RUNNER_TEMP/bin:$PATH" command -v clang-tidy - PATH="$RUNNER_TEMP/bin:$PATH" clang-tidy --version - - name: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: venv + deps - run: | - python3 -m venv .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 canonical main) - id: base - run: | - # The checkout is the PR head of the input repository (a fork), whose - # main lags; pre-commit's diff must span the PR head against the - # canonical repo's main, so fetch it (the runner's github extraheader - # authenticates the fetch). - git fetch --no-tags https://github.com/${{ github.repository }}.git main - echo "base_sha=$(git merge-base FETCH_HEAD 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 ${{ inputs.ref || github.sha }} + uses: ./.github/workflows/_pre-commit.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + base_repository: ${{ github.repository }} + runs_on: '["self-hosted","cpu"]' + setup_variant: self-cpu 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: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: venv + deps - run: | - python3 -m venv .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 - python -m 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 + uses: ./.github/workflows/_ut-no-hardware.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + runs_on: '["self-hosted","cpu"]' + setup_variant: self-cpu packaging: needs: [detect-changes] if: needs.detect-changes.outputs.non_code_only != 'true' && needs.detect-changes.outputs.examples_only != 'true' && needs.detect-changes.outputs.tests_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: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- - - # verify_packaging.sh requires an activated venv, created from the - # runner's python3. - - 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 + uses: ./.github/workflows/_packaging.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + runs_on: '["self-hosted","cpu"]' + setup_variant: self-cpu 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.examples_only != 'true' && needs.detect-changes.outputs.tests_only != '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: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: venv + editable install - run: | - python3 -m venv .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 + uses: ./.github/workflows/_profiling-flags-smoke.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + runs_on: '["self-hosted","cpu"]' + setup_variant: self-cpu 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: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: venv + deps - run: | - python3 -m venv .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. + uses: ./.github/workflows/_st-sim.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + runs_on: '["self-hosted","cpu"]' + setup_variant: self-cpu + platform: a2a3sim + include_dfx_smokes: false 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: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: venv + deps - run: | - python3 -m venv .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 + uses: ./.github/workflows/_st-sim.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + runs_on: '["self-hosted","cpu"]' + setup_variant: self-cpu + platform: a5sim + include_dfx_smokes: false - # ---------- 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 + uses: ./.github/workflows/_ut-npu.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + platform: a2a3 + runs_on: '["self-hosted","a2a3"]' + include_cann_examples: false 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 + uses: ./.github/workflows/_st-npu.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + platform: a2a3 + runs_on: '["self-hosted","a2a3"]' + include_dfx_smokes: false + a2a3_sdma_mode: legacy-paths 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" + uses: ./.github/workflows/_ut-npu.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + platform: a5 + runs_on: '["self-hosted","a5"]' + include_cann_examples: false 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" + uses: ./.github/workflows/_st-npu.yml + with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref || github.sha }} + platform: a5 + runs_on: '["self-hosted","a5"]' + include_dfx_smokes: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef2b4d7b9..05fd547ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,998 +13,123 @@ concurrency: cancel-in-progress: true jobs: - # ---------- Pre-commit hooks (format, lint, clang-tidy) ---------- - pre-commit: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Install build and lint tools - run: | - sudo apt-get update - sudo apt-get install -y cmake ninja-build g++ clang-tidy - - - name: Set up Python 3.10 - uses: actions/setup-python@v6 - with: - python-version: '3.10' - - - name: Install Python dependencies - run: pip install torch --index-url https://download.pytorch.org/whl/cpu - - - name: Install package (for pyright C extension symbol resolution) - run: pip install . - - - name: Cache cmake build directories - uses: actions/cache@v5 - with: - path: build/cache - key: cmake-sim-${{ runner.os }}-${{ hashFiles('src/**/CMakeLists.txt', 'src/**/build_config.py', 'simpler_setup/runtime_compiler.py', 'simpler_setup/kernel_compiler.py', 'simpler_setup/toolchain.py') }} - restore-keys: | - cmake-sim-${{ runner.os }}- - - - name: Build sim runtimes (generates per-target compile_commands.json) - run: python simpler_setup/build_runtimes.py --platforms a2a3sim a5sim + detect-changes: + uses: ./.github/workflows/_detect-changes.yml + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} + base_repository: ${{ github.event.pull_request.base.repo.full_name }} + base_ref: ${{ github.event.pull_request.base.ref }} + base_sha: ${{ github.event.pull_request.base.sha }} + head_sha: ${{ github.event.pull_request.head.sha }} + runs_on: '["ubuntu-latest"]' - - 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 }} + pre-commit: + uses: ./.github/workflows/_pre-commit.yml + with: + base_sha: ${{ github.event.pull_request.base.sha }} + head_sha: ${{ github.event.pull_request.head.sha }} + runs_on: '["ubuntu-latest"]' + setup_variant: github - # ---------- Packaging matrix: 5 install paths × 4 entry points, on macOS + Ubuntu ---------- - # Smoke-checks that every supported install path exposes the four user entry points. - # Each mode is wiped to a fully clean state (no caches) so a previous mode's - # build artifacts cannot mask a regression in the next mode. Slow but reliable. - # See docs/python-packaging.md and tools/verify_packaging.sh. packaging-matrix: needs: [detect-changes, pre-commit] if: needs.detect-changes.outputs.non_code_only != 'true' && needs.detect-changes.outputs.examples_only != 'true' && needs.detect-changes.outputs.tests_only != 'true' - runs-on: ${{ matrix.os }} - timeout-minutes: 60 strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.10'] - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up C++ compiler (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y cmake ninja-build g++ - - - name: Set up C++ compiler (macOS) - if: runner.os == 'macOS' - run: | - brew install ninja cmake - brew install gcc@15 || brew install gcc - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} + python-version: ["3.10"] + uses: ./.github/workflows/_packaging.yml + with: + runs_on: ${{ toJSON(matrix.os) }} + setup_variant: github + python_version: ${{ matrix.python-version }} - - 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 × 4 entry points, fully isolated) - run: | - . .venv/bin/activate - bash tools/verify_packaging.sh - - # ---------- Unit tests (no hardware, Python + C++) ---------- ut: needs: [detect-changes, pre-commit] if: needs.detect-changes.outputs.non_code_only != 'true' && needs.detect-changes.outputs.ut_affected == 'true' - runs-on: ${{ matrix.os }} - timeout-minutes: 15 strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.10'] - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Install GoogleTest (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y cmake libgtest-dev - cd /usr/src/googletest - sudo cmake -B build - sudo cmake --build build - sudo cmake --install build - - - name: Install GoogleTest (macOS) - if: runner.os == 'macOS' - run: | - brew install googletest - - - name: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- + python-version: ["3.10"] + uses: ./.github/workflows/_ut-no-hardware.yml + with: + runs_on: ${{ toJSON(matrix.os) }} + setup_variant: github + python_version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - pip install torch --index-url https://download.pytorch.org/whl/cpu - pip install '.[test]' - - - name: Run Python unit tests - run: 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 - - # ---------- Simulation scene tests ---------- st-sim-a2a3: needs: [detect-changes, pre-commit] if: needs.detect-changes.outputs.a2a3_changed == 'true' && needs.detect-changes.outputs.st_affected == 'true' - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - env: - SIMPLER_SCHEDULER_TIMEOUT_MS: "5000" strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.10'] - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - 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 - else - brew install ninja - brew install gcc@15 || brew install gcc - brew install graphviz - fi - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install dependencies - 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 \ - --pto-session-timeout 600 --require-pto-isa - - # DFX per-feature smokes — the default pytest above passes no --enable-* - # flag, so each capture pipeline gets its own invocation here. Kept as - # separate steps so a failing capture pipeline is attributed to its own - # 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 \ - --platform a2a3sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-dep-gen - - # host_build_graph captures its graph on the host orchestrator instead of - # replaying a device ring, so it is a separate capture pipeline and gets - # its own step. One runtime per invocation: a single pytest spanning both - # makes the scene-test scheduler spawn a child run per runtime, and those - # 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 \ - --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/ \ - --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 \ - --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 \ - --platform a2a3sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --dump-args + python-version: ["3.10"] + uses: ./.github/workflows/_st-sim.yml + with: + runs_on: ${{ toJSON(matrix.os) }} + setup_variant: github + platform: a2a3sim + include_dfx_smokes: true + python_version: ${{ matrix.python-version }} st-sim-a5: needs: [detect-changes, pre-commit] if: needs.detect-changes.outputs.a5_changed == 'true' && needs.detect-changes.outputs.st_affected == 'true' - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - env: - SIMPLER_SCHEDULER_TIMEOUT_MS: "5000" strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.10'] - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - 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 - else - brew install ninja - brew install gcc@15 || brew install gcc - brew install graphviz - fi - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- + python-version: ["3.10"] + uses: ./.github/workflows/_st-sim.yml + with: + runs_on: ${{ toJSON(matrix.os) }} + setup_variant: github + platform: a5sim + include_dfx_smokes: true + python_version: ${{ matrix.python-version }} - - name: Install dependencies - 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 \ - --pto-session-timeout 600 --require-pto-isa - - # DFX per-feature smokes — the default pytest above passes no --enable-* - # flag, so each capture pipeline gets its own invocation here. Kept as - # separate steps so a failing capture pipeline is attributed to its own - # 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 \ - --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/ \ - --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 \ - --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 \ - --platform a5sim --device 0-15 -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --dump-args - - # ---------- Profiling sub-flags smoke (compile + run) ---------- - # SIMPLER_DFX / SIMPLER_ORCH_PROFILING / SIMPLER_SCHED_PROFILING / - # SIMPLER_TENSORMAP_PROFILING are compile-time gates whose defaults and - # dependency checks live in src/common/task_interface/profiling_config.h. - # The defaults (SIMPLER_DFX=1, sub-flags=0) are exercised by every CI job, - # but non-default branches are otherwise dead coverage: a developer flipping - # any of them for perf debugging or to minimize logging overhead had no gate - # protecting the gated code from drift (renamed fields, changed signatures, - # format-string mismatches in LOG_INFO summaries, dead parameters caught - # only by -Wunused-parameter -Werror). - # - # All combos run sequentially in a single job (not a matrix) so the - # apt/python/pip setup cost is paid once, not 12×. Failures across combos - # are accumulated and reported together at the end — first failure does - # not abort the rest. - # - # Combos walk the macro hierarchy explicitly (not just "all on" or "all - # off") so log scrubbing the failing leg pinpoints the specific gate: - # - # SIMPLER_DFX (default=1, covered by every other CI job) - # ├── dfx-off: -DSIMPLER_DFX=0 (no profiling at all) - # ├── orch: -DSIMPLER_ORCH_PROFILING=1 - # │ └── orch-tensormap: + -DSIMPLER_TENSORMAP_PROFILING=1 - # ├── sched: -DSIMPLER_SCHED_PROFILING=1 - # ├── orch-sched: ORCH=1 + SCHED=1 (both sub-flags on, tensormap off) - # └── all-on: ORCH=1 + SCHED=1 + TENSORMAP=1 - # - # Each combo rebuilds the sim runtime with its CXXFLAGS and runs the - # smallest full-pipeline example (vector_example) — exercises orchestrator, - # scheduler, AICore dispatch, and completion paths gated by the macros. - # - # Verified locally that each combo's compile_commands.json contains *only* - # its declared -D flags, the resulting libaicpu_kernel.so contains *only* - # the matching profiling log strings, and pytest output emits *only* the - # corresponding profiling block — no cross-contamination from cmake cache. profiling-flags-smoke: needs: [detect-changes, pre-commit] if: (needs.detect-changes.outputs.a2a3_changed == 'true' || needs.detect-changes.outputs.a5_changed == 'true') && needs.detect-changes.outputs.examples_only != 'true' && needs.detect-changes.outputs.tests_only != 'true' - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up C++ compiler - run: | - 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 - - - name: Set up Python 3.10 - uses: actions/setup-python@v6 - with: - python-version: '3.10' - - - name: Cache pip packages - uses: actions/cache@v5 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*.py') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install dependencies (editable so simpler_setup always resolves to source tree) - run: | - pip install torch --index-url https://download.pytorch.org/whl/cpu - # Editable install (`-e .`) avoids a cwd-dependent footgun: with a - # plain wheel install, `import simpler_setup` from a non-source-tree - # cwd resolves to the wheel-internal copy (PROJECT_ROOT = _assets/), - # silently reading runtime .so files from the wheel instead of the - # build/lib/ that our combo loop overwrites. With editable mode the - # shim always redirects to source-tree paths, regardless of cwd. - # pyproject.toml sets editable.rebuild=false so import-time auto - # rebuilds cannot clobber our profiling-on binaries. - # - # Build isolation (pip default, no --no-build-isolation) pulls - # scikit-build-core into a throwaway env automatically — matches the - # `pre-commit` job's `pip install .` pattern and works on a fresh - # CI runner that has no build deps pre-installed. - pip install -e '.[test]' + uses: ./.github/workflows/_profiling-flags-smoke.yml + with: + runs_on: '["ubuntu-latest"]' + setup_variant: github - - name: Run all profiling flag combos × 2 arches - run: | - # Combo names paired with their CXX defines. Parallel arrays (not - # an associative array) so the loop iteration order is deterministic - # and the log reads top-to-bottom in hierarchy order. - 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 - - echo "" - echo "==== SUMMARY ====" - if [ ${#FAIL[@]} -gt 0 ]; then - printf 'FAILED: %s\n' "${FAIL[@]}" - exit 1 - fi - echo "All $((${#COMBO_NAMES[@]} * ${#ARCHES[@]})) combos passed." - - # ---------- Unit tests (a2a3 hardware, Python + C++) ---------- ut-a2a3: needs: [detect-changes, pre-commit] 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 repository - uses: actions/checkout@v5 - - 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 - # The host_runtime build and test-time kernels both resolve PTO-ISA - # from pto_isa.pin, so they share one ISA revision. - 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] # tolerate a single id (no hyphen) - 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 + uses: ./.github/workflows/_ut-npu.yml + with: + platform: a2a3 + runs_on: '["self-hosted","a2a3"]' + include_cann_examples: true - - name: Build cann-examples/query (CANN host-API smoke) - run: | - source /usr/local/Ascend/cann/set_env.sh - cd tools/cann-examples/query - cmake -B build . - cmake --build build - ./build/query version - - - name: Build cann-examples/aicpu-device-query (device-side HAL probe smoke) - run: | - source /usr/local/Ascend/cann/set_env.sh - export CROSS=${ASCEND_HOME_PATH}/tools/hcc/bin/aarch64-target-linux-gnu - cd tools/cann-examples/aicpu-device-query/device - cmake -B build . -DCMAKE_C_COMPILER=${CROSS}-gcc -DCMAKE_CXX_COMPILER=${CROSS}-g++ - cmake --build build - cd ../host - cmake -B build . - cmake --build build - - - name: Build cann-examples/aicpu-kernel-launch (dispatcher bootstrap smoke) - run: | - source /usr/local/Ascend/cann/set_env.sh - export CROSS=${ASCEND_HOME_PATH}/tools/hcc/bin/aarch64-target-linux-gnu - cd tools/cann-examples/aicpu-kernel-launch/device - cmake -B build . -DCMAKE_C_COMPILER=${CROSS}-gcc -DCMAKE_CXX_COMPILER=${CROSS}-g++ - cmake --build build - cd ../host - cmake -B build . - cmake --build build - - # ---------- Scene tests (a2a3 hardware) ---------- st-onboard-a2a3: needs: [detect-changes, pre-commit] 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 repository - uses: actions/checkout@v5 - - 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 - # The host_runtime build and test-time kernels both resolve PTO-ISA - # from pto_isa.pin, so they share one ISA revision. - pip install '.[test]' - # Warn if graphviz isn't installed on the runner — the dep_gen - # smoke step needs `dot` to render deps_viewer. The Python - # has_binary("dot") guard in the test will skip cleanly without - # it, but coverage drops, so flag it loudly here. - if ! command -v dot >/dev/null 2>&1; then - echo "::warning::graphviz 'dot' not found on PATH; deps_viewer tool smoke will skip. Install graphviz on this self-hosted runner to restore coverage." - fi - - - name: Run pytest scene tests (a2a3) - run: | - source /usr/local/Ascend/cann/set_env.sh - source .venv/bin/activate - # SDMA workloads provision 48 device-only STARS streams that sit in the - # device fault/sync domain, so a fault on that device pays a multi-minute - # teardown (issue #1425). Keep EVERY SDMA test out of this general sweep - # — which also runs the aicore_op_timeout fault-injection test — and run - # them in the SDMA step below, after this sweep, so an SDMA fault's slow - # teardown can never mix with ordinary fault recovery. Device - # disjointness holds only on aarch64 (`task-submit --device auto`); the - # x86_64 branch has no task-submit and shares ${DEVICE_RANGE}, so there - # ordering is the separation. - # - # Selected by marker, not by path. `pytest --ignore=` exits - # 0 and says nothing, so a rename used to drop the quarantine silently - # and land these back beside the fault-injection test. `-m` travels - # with the test instead. - if [ "$(uname -m)" = "x86_64" ]; then - python -m pytest examples tests/st -m "not sdma" --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 -m 'not sdma' --platform a2a3 --device \$TASK_DEVICE -v --require-pto-isa --pto-session-timeout 600" - fi - - # SDMA pytest — every SDMA test runs here, after the sweep above, isolated - # from the general sweep's aicore_op_timeout fault-injection test - # (issue #1425). Both demos create their Worker with enable_sdma=True so the - # runtime provisions the SDMA workspace: prefetch_async_demo on 1 device (L2), - # sdma_async_completion_demo on 2 devices (L3, workspace provisioned per chip - # child) with a comm domain for the peer windows. - - name: SDMA pytest (a2a3) - run: | - source /usr/local/Ascend/cann/set_env.sh - source .venv/bin/activate - # Selected by marker, so this step cannot drift from what the sweep - # deselects. Kept as a step of its own until #1425 is fixed: an - # AICore fault on a device that has provisioned SDMA still costs - # minutes, so these must not share a device with the fault-injection - # cases even though the in-session ordering already separates them. - SDMA_TESTS="examples tests/st -m sdma" - 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 - - # DFX per-feature smokes — hardware mirror of the st-sim-a2a3 set. The - # race window in dep_gen only fires on real timing, so this row is - # what would catch onboard-only regressions like the missing kernel.cpp - # propagation #742 fixed. - - name: dep_gen smoke - run: | - source /usr/local/Ascend/cann/set_env.sh - source .venv/bin/activate - if [ "$(uname -m)" = "x86_64" ]; then - python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ - --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-dep-gen - else - task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ - --run "python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ - --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-dep-gen" - fi - - # Hardware mirror of the sim job's host_build_graph dep_gen step; one - # runtime per invocation for the same reason (a single pytest spanning both - # spawns per-runtime children that `-p no:xdist` breaks). - - name: dep_gen smoke (host_build_graph) - run: | - source /usr/local/Ascend/cann/set_env.sh - source .venv/bin/activate - if [ "$(uname -m)" = "x86_64" ]; then - python -m pytest tests/st/a2a3/host_build_graph/dfx/dep_gen/test_dep_gen.py \ - --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-dep-gen - else - task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ - --run "python -m pytest tests/st/a2a3/host_build_graph/dfx/dep_gen/test_dep_gen.py \ - --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-dep-gen" - fi - - - name: l2_swimlane smoke - run: | - source /usr/local/Ascend/cann/set_env.sh - source .venv/bin/activate - if [ "$(uname -m)" = "x86_64" ]; then - python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ - --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-l2-swimlane --enable-dep-gen - else - task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ - --run "python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ - --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-l2-swimlane --enable-dep-gen" - fi - - - name: PMU smoke - run: | - source /usr/local/Ascend/cann/set_env.sh - source .venv/bin/activate - if [ "$(uname -m)" = "x86_64" ]; then - python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ - --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-pmu 2 - else - task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ - --run "python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ - --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-pmu 2" - fi - - - name: args_dump smoke - run: | - source /usr/local/Ascend/cann/set_env.sh - source .venv/bin/activate - if [ "$(uname -m)" = "x86_64" ]; then - python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ - --platform a2a3 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --dump-args - else - task-submit --timeout 1800 --max-time 1800 --device auto --device-num "$DEVICE_NUM" \ - --run "python -m pytest tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ - --platform a2a3 --device \$TASK_DEVICE -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --dump-args" - fi - - - # ---------- Detect platform-specific changes (runs on GitHub server) ---------- - detect-changes: - runs-on: ubuntu-latest - 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 }} - examples_only: ${{ steps.check.outputs.examples_only }} - tests_only: ${{ steps.check.outputs.tests_only }} - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - name: Check file changes - id: check - run: | - # `run:` is `bash -e`, so a failing `git diff` would abort the step - # before the guard below and take every output with it — and a - # `detect-changes` failure leaves downstream `needs:` unsatisfied, which - # SKIPS the matrix rather than running it. Let only the exit status - # decide, and turn a failure into an empty list so the fail-open guard - # handles both cases identically. - if ! FILES=$(git diff --name-only \ - ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}); then - echo "git diff failed (unresolved base/head SHA?); treating as unattributable" - FILES="" - fi - - # Fail open, in ONE place. No usable file list means attribution is - # impossible — a PR with no files, or base/head SHAs that did not - # resolve — so nothing below can be trusted and every flag must say - # "affected". Deciding this per-flag is how the axes drifted apart: - # the emptiness test used to live inside the non_code_only branch - # only, which left it false (UT and packaging ran) while both arch - # flags independently came out false too (scene tests skipped). - 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" - echo "examples_only=false" >> "$GITHUB_OUTPUT" - echo "tests_only=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # ONE definition of "changing this cannot change what the code does". - # Every gate below derives from this set — see - # .claude/rules/ci-change-detection.md before editing it. Membership - # follows a file's effect, not its path: the docs toolchain counts - # even where it sits outside docs/ (mkdocs.yml, workflows/docs.yml), - # because docs.yml itself runs unconditionally on every PR and is the - # whole gate for it. ci.yml is deliberately absent — a change to the - # gates must run everything. - NON_CODE='^(docs/|\.docs/|\.claude/|mkdocs\.yml$|\.github/workflows/docs\.yml$|\.gitignore$|\.pre-commit-config\.yaml$)|\.md$' + uses: ./.github/workflows/_st-npu.yml + with: + platform: a2a3 + runs_on: '["self-hosted","a2a3"]' + include_dfx_smokes: true + a2a3_sdma_mode: marker - # Non-code-only: pre-commit and docs.yml are the whole gate. - CODE=$(echo "$FILES" | grep -vE "$NON_CODE" || true) - if [ -z "$CODE" ]; then - echo "non_code_only=true" >> "$GITHUB_OUTPUT" - echo "No file here can change what the code does; pre-commit + docs are the whole gate" - else - echo "non_code_only=false" >> "$GITHUB_OUTPUT" - fi - - # a2a3: skip only when ALL changed files are a5-only or non-code - 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" - echo "Files affecting a2a3:" - echo "$A2A3_REMAINING" - else - echo "a2a3_changed=false" >> "$GITHUB_OUTPUT" - echo "All changes are a5-only or non-code; skipping a2a3" - fi - - # a5: skip only when ALL changed files are a2a3-only or non-code - 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" - echo "Files affecting a5:" - echo "$A5_REMAINING" - else - echo "a5_changed=false" >> "$GITHUB_OUTPUT" - echo "All changes are a2a3-only or non-code; skipping a5" - fi - - # Test-category axis, same shape as the arch axis: a category is - # unaffected only when EVERY changed file belongs exclusively to the - # other one. Shared test infra (root conftest.py, pyproject.toml, - # simpler_setup/, tests/lint/) matches neither ST_ONLY nor UT_ONLY, so - # it correctly flips both. The two suites execute disjoint trees — - # `pytest examples tests/st` vs `pytest tests/ut` + the cpp ctest — - # and no unit test reads examples/ or tests/st/. - ST_ONLY='^(tests/st/|examples/)' - UT_ONLY='^tests/ut/' - - # Scene tests are irrelevant only when every change is UT-exclusive. - 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" - echo "All changes are unit-test-only or non-code; skipping scene tests" - fi - - # Unit tests are irrelevant only when every change is ST-exclusive. - 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" - echo "All changes are scene-test/example-only or non-code; skipping unit tests" - fi - - # `packaging-matrix` and `profiling-flags-smoke` build and install the - # product and then exercise it with a fixed, tiny payload — - # profiling-flags-smoke's lives under examples/ (vector_example), - # packaging-matrix's under tests/ (one tests/st/ file is the - # entry-point smoke of tools/verify_packaging.sh). Neither job reads - # either corpus: `wheel.packages` is ["simpler_setup", - # "python/simpler"], so both partitions are provably absent from the - # product, and a payload file changed under either is exercised by - # the scene-test job that also reads it. Each partition therefore - # gets the same axis, written in the same shape. - EXAMPLES_ONLY='^examples/' - NON_EXAMPLE=$(echo "$FILES" | grep -vE "$EXAMPLES_ONLY" | grep -vE "$NON_CODE" || true) - if [ -n "$NON_EXAMPLE" ]; then - echo "examples_only=false" >> "$GITHUB_OUTPUT" - else - echo "examples_only=true" >> "$GITHUB_OUTPUT" - echo "All changes are under examples/ or non-code; skipping packaging and the profiling-flag smoke" - fi - - TESTS_ONLY='^(tests/)' - NON_TEST=$(echo "$FILES" | grep -vE "$TESTS_ONLY" | grep -vE "$NON_CODE" || true) - if [ -n "$NON_TEST" ]; then - echo "tests_only=false" >> "$GITHUB_OUTPUT" - else - echo "tests_only=true" >> "$GITHUB_OUTPUT" - echo "All changes are under tests/ or non-code; skipping packaging and the profiling-flag smoke" - fi - - # ---------- Unit tests (a5 hardware, Python + C++) ---------- ut-a5: needs: [detect-changes, pre-commit] 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 repository - uses: actions/checkout@v5 - - name: Set up environment - run: | - python3 -m venv --system-site-packages .venv - source .venv/bin/activate - pip install --upgrade pip - # The host_runtime build and test-time kernels both resolve PTO-ISA - # from pto_isa.pin, so they share one ISA revision. - 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] # tolerate a single id (no hyphen) - 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" - - - name: Build cann-examples/query (CANN host-API smoke) - run: | - cd tools/cann-examples/query - cmake -B build . - cmake --build build - ./build/query version - - - name: Build cann-examples/aicpu-device-query (device-side HAL probe smoke) - run: | - export CROSS=${ASCEND_HOME_PATH}/tools/hcc/bin/aarch64-target-linux-gnu - cd tools/cann-examples/aicpu-device-query/device - cmake -B build . -DCMAKE_C_COMPILER=${CROSS}-gcc -DCMAKE_CXX_COMPILER=${CROSS}-g++ - cmake --build build - cd ../host - cmake -B build . - cmake --build build - - - name: Build cann-examples/aicpu-kernel-launch (dispatcher bootstrap smoke) - run: | - export CROSS=${ASCEND_HOME_PATH}/tools/hcc/bin/aarch64-target-linux-gnu - cd tools/cann-examples/aicpu-kernel-launch/device - cmake -B build . -DCMAKE_C_COMPILER=${CROSS}-gcc -DCMAKE_CXX_COMPILER=${CROSS}-g++ - cmake --build build - cd ../host - cmake -B build . - cmake --build build + uses: ./.github/workflows/_ut-npu.yml + with: + platform: a5 + runs_on: '["self-hosted","a5"]' + include_cann_examples: true st-onboard-a5: needs: [detect-changes, pre-commit] 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 repository - uses: actions/checkout@v5 - - name: Set up environment - run: | - python3 -m venv --system-site-packages .venv - source .venv/bin/activate - pip install --upgrade pip - # The host_runtime build and test-time kernels both resolve PTO-ISA - # from pto_isa.pin, so they share one ISA revision. - 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" - - # DFX per-feature smokes — hardware mirror of the st-sim-a5 set. The - # race window in dep_gen only fires on real timing, so this row is - # what would catch onboard-only regressions like the missing kernel.cpp - # propagation #742 fixed. - - name: dep_gen smoke - 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 tests/st/a5/tensormap_and_ringbuffer/dfx/dep_gen/test_dep_gen.py \ - --platform a5 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-dep-gen" - task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "$PYTEST" - - - name: l2_swimlane smoke - 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 tests/st/a5/tensormap_and_ringbuffer/dfx/l2_swimlane/ \ - --platform a5 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-l2-swimlane --enable-dep-gen" - task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "$PYTEST" - - - name: PMU smoke - 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 tests/st/a5/tensormap_and_ringbuffer/dfx/pmu/test_pmu.py \ - --platform a5 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --enable-pmu 2" - task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "$PYTEST" - - - name: args_dump smoke - 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 tests/st/a5/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py \ - --platform a5 --device ${DEVICE_RANGE} -p no:xdist --pto-session-timeout 600 \ - --require-pto-isa --dump-args" - task-submit --timeout 1800 --max-time 1800 --device "$DEVICE_LIST" --run "$PYTEST" + uses: ./.github/workflows/_st-npu.yml + with: + platform: a5 + runs_on: '["self-hosted","a5"]' + include_dfx_smokes: true diff --git a/docs/ci.md b/docs/ci.md index 3196896ae..7d6da7fb1 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -24,13 +24,22 @@ The complete test-type × hardware-tier matrix. Empty cells have no tests yet; o ## GitHub Actions Jobs +`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.yml`, +`_ut-npu.yml`, and `_st-npu.yml`. Shared step scaffolding that is safe to run +after checkout lives in composite actions under `.github/actions/` +(`cache-pip`, `setup-venv`). + ```text 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 (ubuntu-latest) — outputs non_code_only, a{2a3,5}_changed, {st,ut}_affected, examples_only, tests_only + ├── 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] @@ -131,7 +140,7 @@ not need `--max-parallel` manually. ### Scheduling constraints - Sim scene tests and no-hardware unit tests run on github-hosted runners (no hardware). -- `detect-changes` 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 one `NON_CODE` set**: `docs/`, `.docs/`, `.claude/`, `mkdocs.yml`, `.github/workflows/docs.yml`, `.gitignore`, `.pre-commit-config.yaml`, and any `*.md` file anywhere. Membership follows a file's *effect*, not its path — `mkdocs.yml` and `docs.yml` are docs tooling that happens to live outside `docs/`. An arch 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 `NON_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 **`.github/workflows/ci.yml` itself** — flips both flags to `true`. `ci.yml` is deliberately excluded from `NON_CODE`: a change to the gates must run everything, including whatever it just switched off. +- `detect-changes` is implemented once in [`.github/workflows/_detect-changes.yml`](../.github/workflows/_detect-changes.yml) and 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 one `NON_CODE` set**: `docs/`, `.docs/`, `.claude/`, `mkdocs.yml`, `.github/workflows/docs.yml`, `.gitignore`, `.pre-commit-config.yaml`, and any `*.md` file anywhere. Membership follows a file's *effect*, not its path — `mkdocs.yml` and `docs.yml` are docs tooling that happens to live outside `docs/`. An arch 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 `NON_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 to `true`. CI implementation workflows are deliberately excluded from `NON_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/)'` and `UT_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_affected` gates the four scene-test jobs; `ut_affected` gates `ut`, `ut-a2a3`, `ut-a5`. Anything belonging to neither (root `conftest.py`, `pyproject.toml`, `simpler_setup/`, `tests/lint/`) flips both, so shared infrastructure always runs both suites. - **Corpus axis:** `EXAMPLES_ONLY='^examples/'` and `TESTS_ONLY='^(tests/)'`, same shape again, one per side of the product jobs' payload. `packaging-matrix` and `profiling-flags-smoke` build and install the product and then exercise it with a fixed, tiny payload — one entry-point script (a `tests/st/` file) and one `vector_example` — so neither reads either suite as a corpus, and a diff confined to `examples/` or to `tests/` cannot reach them: `wheel.packages` is `["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 **and** `st_affected` are `true`. @@ -158,9 +167,12 @@ queueing entirely: 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. + `[self-hosted, a2a3/a5]`. T2 (macOS) is intentionally absent. The lane calls + the same reusable job-body workflows as `ci.yml`, passing + `setup_variant=self-cpu`, `repository`, `ref`, and self-hosted runner labels + where the main CI passes `setup_variant=github` and GitHub-hosted runners. + Gate outputs still come from the canonical `detect-changes` workflow, 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++-15` is a symlink stand-in for the ubuntu-toolchain ppa g++. On the agents `g++` resolves to a conda GCC 15 prefix rather than `/usr/bin/g++`, so the lane's sim artifacts are built with GCC 15 and `compile_commands.json` names that prefix's `-g++`; `tests/lint/clang_tidy.py` drops the triple before replaying a command, without which clang-tidy adopts it as a target and resolves no C++ standard library at all. `ci.yml` lints with clang-tidy 18 and HCE 2.0 packages only LLVM 12, so an agent additionally provides 18 on `PATH` as `clang-tidy-18`, installed together with its clang builtin headers — a clang-tidy whose prefix carries no `lib/clang//include` resolves no resource dir and fails every `#include `. The `pre-commit` job shadows the distro `clang-tidy` with it when present. - The lane run is standalone — it attaches no checks to the PR; results are read from the run.