TileLang-Ascend CI #2403
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # TileLang-Ascend 持续集成工作流 | |
| name: TileLang-Ascend CI | |
| # 触发条件 | |
| on: | |
| pull_request: | |
| branches: | |
| - ascendc_pto* | |
| paths-ignore: | |
| - '**.md' | |
| - '**.png' | |
| - '.gitignore' | |
| - 'docs/**' | |
| - 'images/**' | |
| - '.agents/**' | |
| # 手动触发(通过 GitHub Actions 页面点击) | |
| workflow_dispatch: | |
| inputs: | |
| run_tests: | |
| description: 'Run benchmark tests' # 是否运行测试 | |
| required: false | |
| default: true | |
| type: boolean | |
| full_build: # 是否全量编译 | |
| description: 'Force full build (no cache)' | |
| required: false | |
| default: false | |
| type: boolean | |
| schedule: | |
| - cron: '0 18 * * *' # 每天 UTC 18:00 运行 (北京时间凌晨 2:00) | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| statuses: write | |
| checks: write | |
| actions: write | |
| issues: write | |
| concurrency: | |
| # PR 场景(含 pull_request 和 /re-test):统一按 PR 号分组,新运行自动取消旧运行 | |
| # 定时任务按分支分组避免积压;手动场景退化为 github.run_id(每次唯一),互不干扰 | |
| group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number || (github.event_name == 'schedule' && github.ref_name) || github.run_id }} | |
| # 同一组中有新运行时,自动取消旧的正在运行的 workflow | |
| cancel-in-progress: true | |
| jobs: | |
| check_changes: | |
| name: Check PR Status and Changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| merged: ${{ steps.pr_info.outputs.merged || steps.pr_status.outputs.merged || steps.default_changes.outputs.merged }} | |
| test_dirs: ${{ steps.detect_changes.outputs.test_dirs || steps.default_changes.outputs.test_dirs }} | |
| experiment_dirs: ${{ steps.detect_changes.outputs.experiment_dirs || steps.default_changes.outputs.experiment_dirs }} | |
| pytest_files: ${{ steps.detect_changes.outputs.pytest_files || steps.default_changes.outputs.pytest_files }} | |
| run_examples: ${{ steps.detect_changes.outputs.run_examples || steps.default_changes.outputs.run_examples }} | |
| run_pytest_only: ${{ steps.detect_changes.outputs.run_pytest_only || steps.default_changes.outputs.run_pytest_only }} | |
| should_skip: ${{ steps.detect_changes.outputs.should_skip || steps.default_changes.outputs.should_skip }} | |
| steps: | |
| - name: Set default changes for full test events | |
| if: github.event_name != 'pull_request' && (github.event_name != 'issue_comment' || !github.event.issue.pull_request) | |
| id: default_changes | |
| run: | | |
| echo "merged=false" >> $GITHUB_OUTPUT | |
| echo "test_dirs=" >> $GITHUB_OUTPUT | |
| echo "experiment_dirs=" >> $GITHUB_OUTPUT | |
| echo "pytest_files=" >> $GITHUB_OUTPUT | |
| echo "run_examples=true" >> $GITHUB_OUTPUT | |
| echo "run_pytest_only=false" >> $GITHUB_OUTPUT | |
| echo "should_skip=false" >> $GITHUB_OUTPUT | |
| - name: Get PR info | |
| if: github.event_name == 'issue_comment' && github.event.issue.pull_request | |
| id: pr_info | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const pr = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number | |
| }); | |
| core.setOutput('head_ref', pr.data.head.ref); | |
| core.setOutput('head_sha', pr.data.head.sha); | |
| core.setOutput('base_ref', pr.data.base.ref); | |
| core.setOutput('merged', pr.data.merged ? 'true' : 'false'); | |
| core.setOutput('state', pr.data.state); | |
| - name: Set merged status for PR event | |
| if: github.event_name == 'pull_request' | |
| id: pr_status | |
| run: | | |
| echo "merged=false" >> $GITHUB_OUTPUT | |
| echo "base_ref=${{ github.base_ref }}" >> $GITHUB_OUTPUT | |
| - name: React to merged PR comment | |
| if: github.event_name == 'issue_comment' && github.event.issue.pull_request && steps.pr_info.outputs.merged == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: 'confused' | |
| }); | |
| github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: '⚠️ This PR has been merged. Skipping tests.' | |
| }); | |
| - name: Rerun failed jobs for /re-test | |
| if: github.event_name == 'issue_comment' && github.event.issue.pull_request && steps.pr_info.outputs.merged != 'true' && contains(github.event.comment.body, '/re-test') | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // Add reaction to comment | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: 'rocket' | |
| }); | |
| // Find original workflow run by head_sha | |
| // Use numeric workflow ID (261026938) to ensure only ci_cd.yml runs are returned | |
| // Using filename 'ci_cd.yml' in listWorkflowRunsForRepo may return runs from other workflows | |
| const runs = await github.rest.actions.listWorkflowRuns({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'ci_cd.yml', | |
| head_sha: '${{ steps.pr_info.outputs.head_sha }}', | |
| event: 'pull_request' | |
| }); | |
| if (runs.data.workflow_runs.length === 0) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: '⚠️ No previous workflow run found for this PR. Cannot re-run failed jobs.' | |
| }); | |
| return; | |
| } | |
| const originalRun = runs.data.workflow_runs[0]; | |
| const runUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${originalRun.id}`; | |
| // Rerun failed jobs | |
| try { | |
| await github.rest.actions.reRunWorkflowFailedJobs({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: originalRun.id | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `🔄 **Re-running failed jobs**\n\nOriginal workflow run: [View details](<${runUrl}>)\n\nOnly the failed jobs will be re-executed.` | |
| }); | |
| } catch (error) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `❌ Failed to re-run jobs: ${error.message}\n\nYou may need to re-run manually from the Actions page: [View workflow](<${runUrl}>)` | |
| }); | |
| } | |
| - name: Checkout code | |
| if: (github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && github.event.issue.pull_request)) && (steps.pr_info.outputs.merged != 'true' || github.event_name == 'pull_request') | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'issue_comment' && format('refs/pull/{0}/head', github.event.issue.number) || '' }} | |
| - name: Detect changed files | |
| if: (github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && github.event.issue.pull_request)) && (steps.pr_info.outputs.merged != 'true' || github.event_name == 'pull_request') | |
| id: detect_changes | |
| run: | | |
| BASE_REF="${{ github.event_name == 'issue_comment' && steps.pr_info.outputs.base_ref || github.base_ref }}" | |
| git fetch origin $BASE_REF --depth=1 | |
| changed_files=$(git diff --name-only origin/$BASE_REF HEAD) | |
| echo "Changed files:" | |
| echo "$changed_files" | |
| run_examples=false | |
| run_pytest_only=false | |
| should_skip=true | |
| test_dirs_array=() | |
| experiment_dirs_array=() | |
| pytest_files_array=() | |
| pytest_dirs_array=() | |
| for file in $changed_files; do | |
| # 排除 .agents 目录(skill 配置,无需测试) | |
| if [[ "$file" =~ ^\.agents/ ]]; then | |
| echo "Agents skill file: $file - will skip" | |
| continue | |
| fi | |
| # 排除文档/图片文件 | |
| if [[ "$file" =~ \.md$ ]] || [[ "$file" =~ \.png$ ]]; then | |
| echo "Doc/image file: $file - will skip" | |
| continue | |
| fi | |
| # testing/ 修改 → 收集 pytest 文件/目录 | |
| if [[ "$file" =~ ^testing/python/ ]]; then | |
| if [[ "$file" =~ \.py$ ]]; then | |
| pytest_files_array+=("$file") | |
| # 提取子目录 | |
| if [[ "$file" =~ ^testing/python/([^/]+)/ ]]; then | |
| pytest_subdir="${BASH_REMATCH[1]}" | |
| if [[ ! " ${pytest_dirs_array[*]} " =~ " $pytest_subdir " ]]; then | |
| pytest_dirs_array+=("$pytest_subdir") | |
| echo "Mapped $file → pytest subdir: $pytest_subdir" | |
| fi | |
| fi | |
| fi | |
| run_examples=false | |
| run_pytest_only=true | |
| should_skip=false | |
| continue | |
| fi | |
| # 核心文件修改 → 全量 examples + pytest | |
| # 排除文档/图片文件 | |
| if [[ "$file" =~ \.md$ ]] || [[ "$file" =~ \.png$ ]]; then | |
| echo "Doc/image file: $file - will skip" | |
| continue | |
| fi | |
| if [[ "$file" =~ ^tilelang/ ]] || [[ "$file" =~ ^src/ ]] || \ | |
| [[ "$file" =~ CMakeLists\.txt ]] || [[ "$file" =~ requirements ]] || [[ "$file" =~ install_ascend\.sh ]]; then | |
| echo "Core file modified: $file - will run full examples + pytest" | |
| run_examples=true | |
| run_pytest_only=false | |
| should_skip=false | |
| test_dirs_array=() | |
| experiment_dirs_array=() | |
| pytest_files_array=() | |
| pytest_dirs_array=() | |
| break | |
| fi | |
| # examples 文件 → 增量 examples + pytest | |
| if [[ "$file" =~ ^examples/([^/]+)/ ]]; then | |
| test_dir="${BASH_REMATCH[1]}" | |
| # 排除非测试目录 | |
| if [[ "$test_dir" != "dispatch_combine" ]] && [[ "$test_dir" != "shmem" ]]; then | |
| if [[ ! " ${test_dirs_array[*]} " =~ " $test_dir " ]]; then | |
| test_dirs_array+=("$test_dir") | |
| echo "Mapped $file → example dir: $test_dir" | |
| fi | |
| run_examples=true | |
| run_pytest_only=false | |
| should_skip=false | |
| fi | |
| continue | |
| fi | |
| # examples_experiment 文件 → 增量 experiment(examples_experiment 根下) | |
| if [[ "$file" =~ ^examples_experiment/([^/]+)/ ]]; then | |
| exp_dir="${BASH_REMATCH[1]}" | |
| if [[ ! " ${experiment_dirs_array[*]} " =~ " $exp_dir " ]]; then | |
| experiment_dirs_array+=("$exp_dir") | |
| echo "Mapped $file → experiment dir: $exp_dir" | |
| fi | |
| run_examples=true | |
| run_pytest_only=false | |
| should_skip=false | |
| continue | |
| fi | |
| # examples_experiment 根目录文件 → 文档跳过,其它触发全量 | |
| if [[ "$file" =~ ^examples_experiment/[^/]+$ ]]; then | |
| if [[ "$file" =~ \.md$ ]] || [[ "$file" =~ \.png$ ]] || \ | |
| [[ "$file" =~ \.gitignore ]] || [[ "$file" =~ LICENSE ]]; then | |
| echo "Experiment root doc file: $file - will skip" | |
| continue | |
| fi | |
| echo "Experiment root file modified: $file - will run full examples + pytest" | |
| run_examples=true | |
| run_pytest_only=false | |
| should_skip=false | |
| test_dirs_array=() | |
| experiment_dirs_array=() | |
| pytest_files_array=() | |
| pytest_dirs_array=() | |
| break | |
| fi | |
| # examples 根目录文件(如 bench_test.sh)→ 判断是否需要测试 | |
| if [[ "$file" =~ ^examples/[^/]+$ ]]; then | |
| # 排除文档文件 | |
| if [[ "$file" =~ \.md$ ]] || [[ "$file" =~ \.png$ ]] || \ | |
| [[ "$file" =~ \.gitignore ]] || [[ "$file" =~ LICENSE ]]; then | |
| echo "Examples root doc file: $file - will skip" | |
| continue | |
| fi | |
| # 关键文件(bench_test.sh 等)→ 全量测试 | |
| echo "Examples root file modified: $file - will run full examples + pytest" | |
| run_examples=true | |
| run_pytest_only=false | |
| should_skip=false | |
| test_dirs_array=() | |
| experiment_dirs_array=() | |
| pytest_files_array=() | |
| pytest_dirs_array=() | |
| break | |
| fi | |
| # 其他文件(非 docs/images)-> 需要测试 | |
| if [[ ! "$file" =~ \.md$ ]] && [[ ! "$file" =~ \.png$ ]] && \ | |
| [[ ! "$file" =~ ^docs/ ]] && [[ ! "$file" =~ ^images/ ]] && \ | |
| [[ ! "$file" =~ ^testing/ ]] && [[ ! "$file" =~ ^tilelang/ ]] && \ | |
| [[ ! "$file" =~ ^src/ ]] && [[ ! "$file" =~ ^examples/ ]] && \ | |
| [[ ! "$file" =~ ^examples_experiment/ ]]; then | |
| echo "Other file modified: $file - will run full tests" | |
| run_examples=true | |
| run_pytest_only=false | |
| should_skip=false | |
| test_dirs_array=() | |
| experiment_dirs_array=() | |
| pytest_files_array=() | |
| pytest_dirs_array=() | |
| break | |
| fi | |
| done | |
| # 决定 pytest 测试范围 | |
| pytest_files="" | |
| if [[ ${#pytest_files_array[@]} -gt 0 ]]; then | |
| # 如果所有修改在同一个子目录,只跑该子目录 | |
| if [[ ${#pytest_dirs_array[@]} -eq 1 ]]; then | |
| pytest_files="testing/python/${pytest_dirs_array[0]}/" | |
| echo "All testing files in same subdir: ${pytest_dirs_array[0]}" | |
| elif [[ ${#pytest_files_array[@]} -le 3 ]]; then | |
| # 少量文件(≤3),只跑具体文件 | |
| pytest_files="${pytest_files_array[*]}" | |
| echo "Few testing files modified: ${pytest_files_array[*]}" | |
| else | |
| # 多文件散布不同目录,跑全量 pytest | |
| pytest_files="" | |
| echo "Many testing files scattered, will run full pytest" | |
| fi | |
| fi | |
| test_dirs="${test_dirs_array[*]}" | |
| experiment_dirs="${experiment_dirs_array[*]}" | |
| echo "run_examples=${run_examples}" >> $GITHUB_OUTPUT | |
| echo "run_pytest_only=${run_pytest_only}" >> $GITHUB_OUTPUT | |
| echo "test_dirs=${test_dirs}" >> $GITHUB_OUTPUT | |
| echo "experiment_dirs=${experiment_dirs}" >> $GITHUB_OUTPUT | |
| echo "pytest_files=${pytest_files}" >> $GITHUB_OUTPUT | |
| echo "should_skip=${should_skip}" >> $GITHUB_OUTPUT | |
| if [[ "$run_pytest_only" == "true" ]]; then | |
| if [[ -n "$pytest_files" ]]; then | |
| echo "Will run incremental pytest for: $pytest_files" | |
| else | |
| echo "Will run full pytest (testing/ modified)" | |
| fi | |
| elif [[ "$run_examples" == "true" ]]; then | |
| if [[ -n "$test_dirs" || -n "$experiment_dirs" ]]; then | |
| echo "Will run incremental examples + pytest for: examples=[$test_dirs] experiment=[$experiment_dirs]" | |
| else | |
| echo "Will run full examples + pytest (core files modified)" | |
| fi | |
| elif [[ "$should_skip" == "true" ]]; then | |
| echo "No testable files modified (docs/images only) - will skip" | |
| fi | |
| test: | |
| name: Benchmark Tests (Ascend NPU) | |
| runs-on: [self-hosted, Linux, X64, ascend_910b] | |
| needs: check_changes | |
| outputs: | |
| pass_rate: ${{ steps.check_results.outputs.pass_rate }} | |
| test_status: ${{ steps.check_results.outputs.test_status }} | |
| test_summary: ${{ steps.check_results.outputs.test_summary }} | |
| if: | | |
| ( | |
| (github.event_name != 'workflow_dispatch' || github.event.inputs.run_tests == 'true') && | |
| (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && | |
| needs.check_changes.outputs.merged != 'true' && | |
| needs.check_changes.outputs.should_skip != 'true' | |
| ) | |
| timeout-minutes: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && 360 || 60 }} | |
| env: | |
| FULL_BUILD: ${{ github.event_name == 'schedule' || github.event.inputs.full_build == 'true' }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| fetch-depth: 0 | |
| - name: Check if skip pytest | |
| id: check_skip_pytest | |
| if: github.event_name == 'pull_request' | |
| run: | | |
| BASE_REF="${{ github.base_ref }}" | |
| git fetch origin $BASE_REF | |
| changed_files=$(git diff --name-only origin/$BASE_REF HEAD) | |
| skip_pytest=true | |
| for file in $changed_files; do | |
| if [[ "$file" =~ ^\.agents/ ]]; then | |
| echo "Agents skill file: $file - ignored for pytest skip check" | |
| continue | |
| fi | |
| if [[ "$file" =~ \.md$ ]] || [[ "$file" =~ \.png$ ]]; then | |
| echo "Doc/image file: $file - ignored for pytest skip check" | |
| continue | |
| fi | |
| if [[ ! "$file" =~ ^examples/ ]] && [[ ! "$file" =~ ^examples_experiment/ ]]; then | |
| echo "File outside examples/ and examples_experiment/: $file" | |
| skip_pytest=false | |
| break | |
| fi | |
| if [[ ! "$file" =~ \.py$ ]] && [[ ! "$file" =~ \.md$ ]] && [[ ! "$file" =~ \.png$ ]]; then | |
| echo "File type not allowed: $file" | |
| skip_pytest=false | |
| break | |
| fi | |
| done | |
| echo "skip_pytest=${skip_pytest}" >> $GITHUB_OUTPUT | |
| if [ "$skip_pytest" = true ]; then | |
| echo "Will skip pytest: only .py/.md/.png files in examples/ or examples_experiment/ modified" | |
| else | |
| echo "Will run full tests" | |
| fi | |
| # 非全量编译,缓存编译产物,除非关键文件变更 | |
| - name: Restore build cache | |
| if: env.FULL_BUILD != 'true' | |
| id: cache-build | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: build-cache/ | |
| key: tilelang-build-${{ github.base_ref || github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'requirements.txt', 'install_ascend.sh') }} | |
| # 全量编译,清理所有残留编译产物 | |
| - name: Clean build artifacts for full build | |
| if: env.FULL_BUILD == 'true' | |
| run: | | |
| echo "🗑️ Full build mode: cleaning all build artifacts" | |
| rm -rf build build-cache | |
| echo "Clean completed. Will build from scratch." | |
| - name: Build and Test in Local Container | |
| run: | | |
| # 准备缓存目录 | |
| mkdir -p build-cache | |
| if [[ "${FULL_BUILD}" == "true" ]]; then | |
| echo "📅 Full build (schedule/manual): NO cache, NO incremental" | |
| INCREMENTAL_FLAG="" | |
| elif [[ "${{ steps.cache-build.outputs.cache-hit }}" == 'true' ]]; then | |
| echo "✅ Cache hit! Will use incremental build." | |
| INCREMENTAL_FLAG="--enable-incremental" | |
| else | |
| echo "🔄 Cache miss. Performing clean build." | |
| INCREMENTAL_FLAG="" | |
| fi | |
| SKIP_PYTEST_VALUE="${{ steps.check_skip_pytest.outputs.skip_pytest }}" | |
| # 确定 TEST_DIRS 参数和测试策略 | |
| # schedule/workflow_dispatch 事件:全量测试 | |
| # PR 事件:根据 check_changes outputs | |
| if [[ "$GITHUB_EVENT_NAME" == "schedule" ]] || [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then | |
| TEST_DIRS_ARG="" | |
| EXPERIMENT_DIRS_ARG="" | |
| PYTEST_FILES_ARG="" | |
| RUN_EXAMPLES="true" | |
| RUN_PYTEST_ONLY="false" | |
| echo "Event: $GITHUB_EVENT_NAME - running full tests (examples + experiment)" | |
| elif [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then | |
| TEST_DIRS_VALUE="${{ needs.check_changes.outputs.test_dirs }}" | |
| EXPERIMENT_DIRS_VALUE="${{ needs.check_changes.outputs.experiment_dirs }}" | |
| PYTEST_FILES_VALUE="${{ needs.check_changes.outputs.pytest_files }}" | |
| RUN_EXAMPLES="${{ needs.check_changes.outputs.run_examples }}" | |
| RUN_PYTEST_ONLY="${{ needs.check_changes.outputs.run_pytest_only }}" | |
| if [[ "$RUN_PYTEST_ONLY" == "true" ]]; then | |
| TEST_DIRS_ARG="" | |
| EXPERIMENT_DIRS_ARG="" | |
| if [[ -n "$PYTEST_FILES_VALUE" ]]; then | |
| PYTEST_FILES_ARG="$PYTEST_FILES_VALUE" | |
| echo "Running incremental pytest for: $PYTEST_FILES_VALUE" | |
| else | |
| PYTEST_FILES_ARG="" | |
| echo "Running full pytest (testing/ modified)" | |
| fi | |
| elif [[ "$RUN_EXAMPLES" == "true" ]]; then | |
| PYTEST_FILES_ARG="" | |
| # 只有 examples 和 experiment 目录都为空才是全量;任一非空则走增量 | |
| if [[ -z "$TEST_DIRS_VALUE" && -z "$EXPERIMENT_DIRS_VALUE" ]]; then | |
| TEST_DIRS_ARG="" | |
| EXPERIMENT_DIRS_ARG="" | |
| echo "Running full examples + pytest (core files modified)" | |
| else | |
| if [[ -n "$TEST_DIRS_VALUE" ]]; then | |
| TEST_DIRS_ARG="--dirs ${TEST_DIRS_VALUE}" | |
| else | |
| TEST_DIRS_ARG="" | |
| fi | |
| if [[ -n "$EXPERIMENT_DIRS_VALUE" ]]; then | |
| EXPERIMENT_DIRS_ARG="--experiment-dirs ${EXPERIMENT_DIRS_VALUE}" | |
| else | |
| EXPERIMENT_DIRS_ARG="" | |
| fi | |
| echo "Running incremental examples + pytest for: examples=[$TEST_DIRS_VALUE] experiment=[$EXPERIMENT_DIRS_VALUE]" | |
| fi | |
| fi | |
| fi | |
| # 确定 pytest marker 过滤策略 | |
| # schedule/workflow_dispatch 事件:跳过 ci_skip,保留 low_priority | |
| # PR 事件:跳过 low_priority 和 ci_skip | |
| if [[ "$GITHUB_EVENT_NAME" == "schedule" ]] || [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then | |
| PYTEST_MARKERS="not ci_skip" | |
| elif [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then | |
| PYTEST_MARKERS="not (low_priority or ci_skip)" | |
| fi | |
| docker exec \ | |
| -w $GITHUB_WORKSPACE \ | |
| -e GITHUB_ENV=$GITHUB_ENV \ | |
| -e GITHUB_OUTPUT=$GITHUB_OUTPUT \ | |
| -e BASH_ENV=/root/.bashrc \ | |
| -e INCREMENTAL_FLAG="$INCREMENTAL_FLAG" \ | |
| -e SKIP_PYTEST=$SKIP_PYTEST_VALUE \ | |
| -e TEST_DIRS_ARG="$TEST_DIRS_ARG" \ | |
| -e EXPERIMENT_DIRS_ARG="$EXPERIMENT_DIRS_ARG" \ | |
| -e PYTEST_FILES_ARG="$PYTEST_FILES_ARG" \ | |
| -e RUN_EXAMPLES="$RUN_EXAMPLES" \ | |
| -e RUN_PYTEST_ONLY="$RUN_PYTEST_ONLY" \ | |
| -e PYTEST_MARKERS="$PYTEST_MARKERS" \ | |
| tilelang_x1 \ | |
| bash -c " | |
| set -e | |
| # 如果有缓存,复制到 build 目录 | |
| if [ -d build-cache ] && [ \"\$(ls -A build-cache)\" ]; then | |
| echo \"Copy build cache...\" | |
| mkdir -p build | |
| cp -ra build-cache/* build/ | |
| fi | |
| # 1. 增量编译项目 | |
| bash install_ascend.sh \$INCREMENTAL_FLAG | |
| # 保存编译产物 | |
| if [ -d \"build\" ]; then | |
| echo \"Saving build artifacts...\" | |
| rm -rf build-cache/* | |
| cp -ra build/* build-cache/ | |
| fi | |
| # 2. 加载环境变量 | |
| echo 'Sourcing environment...' | |
| source set_env.sh | |
| echo 'TILELANG_PATH='\$PWD >> \$GITHUB_ENV | |
| # 3. 运行测试用例 | |
| cd examples | |
| chmod +x bench_test.sh | |
| # 构建 pytest marker 过滤参数 | |
| PYTEST_MARKER_ARGS=() | |
| if [ -n \"\$PYTEST_MARKERS\" ]; then | |
| PYTEST_MARKER_ARGS+=(-m \"\$PYTEST_MARKERS\") | |
| echo 'Applying pytest marker filter: -m '\$PYTEST_MARKERS | |
| fi | |
| if [ \"\$RUN_PYTEST_ONLY\" = true ]; then | |
| # 只跑 pytest(testing/ 修改) | |
| if [ -n \"\$PYTEST_FILES_ARG\" ]; then | |
| echo 'Running incremental pytest for: '\$PYTEST_FILES_ARG | |
| set +e | |
| pytest --forked ../\$PYTEST_FILES_ARG -v -n 4 \"\${PYTEST_MARKER_ARGS[@]}\" 2>&1 | tee test_output.log | |
| else | |
| echo 'Running full pytest (testing/ modified)' | |
| set +e | |
| pytest --forked ../testing/python/ -v -n 8 \"\${PYTEST_MARKER_ARGS[@]}\" 2>&1 | tee test_output.log | |
| fi | |
| TEST_EXIT_CODE=\${PIPESTATUS[0]} | |
| set -e | |
| elif [ \"\$RUN_EXAMPLES\" = true ]; then | |
| # 跑 examples + experiment + pytest | |
| echo 'Running benchmark tests with args: '\$TEST_DIRS_ARG' '\$EXPERIMENT_DIRS_ARG | |
| set +e | |
| if [ \"\$SKIP_PYTEST\" = true ]; then | |
| ./bench_test.sh --skip-pytest \$TEST_DIRS_ARG \$EXPERIMENT_DIRS_ARG 2>&1 | tee test_output.log | |
| else | |
| if [ -n \"\$PYTEST_MARKERS\" ]; then | |
| ./bench_test.sh --pytest-markers \"\$PYTEST_MARKERS\" \$TEST_DIRS_ARG \$EXPERIMENT_DIRS_ARG 2>&1 | tee test_output.log | |
| else | |
| ./bench_test.sh \$TEST_DIRS_ARG \$EXPERIMENT_DIRS_ARG 2>&1 | tee test_output.log | |
| fi | |
| fi | |
| TEST_EXIT_CODE=\${PIPESTATUS[0]} | |
| set -e | |
| fi | |
| echo 'Test exit code: '\$TEST_EXIT_CODE | |
| echo \"test_exit_code=\$TEST_EXIT_CODE\" >> \$GITHUB_OUTPUT | |
| # 如果测试失败,让脚本退出码非0 | |
| if [ \"\$TEST_EXIT_CODE\" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| " | |
| # 全量编译下先删除旧缓存,否则 save 会失败 | |
| - name: Delete old cache for overwrite | |
| if: always() && env.FULL_BUILD == 'true' | |
| run: | | |
| KEY="tilelang-build-${{ github.base_ref || github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'requirements.txt', 'install_ascend.sh') }}" | |
| echo "🗑️ Deleting old cache to allow overwrite: $KEY" | |
| curl -s -L \ | |
| -X DELETE \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${{ github.repository }}/actions/caches?key=$KEY&ref=${{ github.ref }}" | |
| echo "✅ Old cache deleted (if existed)" | |
| continue-on-error: true | |
| # 全量编 | 增量编但无缓存,需要保存缓存 | |
| - name: Save build cache | |
| if: always() && (env.FULL_BUILD == 'true' || steps.cache-build.outputs.cache-hit == 'false') | |
| continue-on-error: true # key 已存在时不报错 | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: build-cache/ | |
| key: tilelang-build-${{ github.base_ref || github.ref_name }}-${{ hashFiles('CMakeLists.txt', 'requirements.txt', 'install_ascend.sh') }} | |
| - name: Check pass rate | |
| id: check_results | |
| run: | | |
| echo "Checking test results..." | |
| cd examples | |
| if [ -f test_output.log ]; then | |
| # 提取失败测试列表 | |
| FAILED_TESTS=$(grep -E "^\[FAILED\]" test_output.log || echo "") | |
| # 检查是否是 pytest only 的输出 | |
| if grep -q "pytest" test_output.log && ! grep -q "Pass rate:" test_output.log; then | |
| # pytest only 模式 | |
| if grep -q "passed" test_output.log && grep -q "failed" test_output.log; then | |
| PASSED=$(grep -o "[0-9]* passed" test_output.log | head -1 | grep -o "[0-9]*" || echo "0") | |
| FAILED=$(grep -o "[0-9]* failed" test_output.log | head -1 | grep -o "[0-9]*" || echo "0") | |
| TOTAL=$((PASSED + FAILED)) | |
| echo "pytest results: ${PASSED} passed, ${FAILED} failed" | |
| echo "pass_rate=${PASSED}" >> $GITHUB_OUTPUT | |
| # 构建摘要 | |
| SUMMARY="Total: ${TOTAL} | Passed: ${PASSED} | Failed: ${FAILED}" | |
| echo "test_summary=${SUMMARY}" >> $GITHUB_OUTPUT | |
| if [ "$FAILED" -eq 0 ]; then | |
| echo "✅ All pytest tests passed" | |
| echo "test_status=passed" >> $GITHUB_OUTPUT | |
| else | |
| echo "❌ Pytest tests failed (${FAILED} failures)" | |
| echo "test_status=failed" >> $GITHUB_OUTPUT | |
| if [ -n "$FAILED_TESTS" ]; then | |
| echo "Failed tests:" | |
| echo "$FAILED_TESTS" | |
| fi | |
| exit 1 | |
| fi | |
| elif grep -q "passed" test_output.log; then | |
| PASSED=$(grep -o "[0-9]* passed" test_output.log | head -1 | grep -o "[0-9]*" || echo "0") | |
| echo "pytest results: ${PASSED} passed, 0 failed" | |
| echo "pass_rate=${PASSED}" >> $GITHUB_OUTPUT | |
| echo "test_summary=Total: ${PASSED} | Passed: ${PASSED} | Failed: 0" >> $GITHUB_OUTPUT | |
| echo "✅ All pytest tests passed" | |
| echo "test_status=passed" >> $GITHUB_OUTPUT | |
| elif grep -q "failed" test_output.log; then | |
| FAILED=$(grep -o "[0-9]* failed" test_output.log | head -1 | grep -o "[0-9]*" || echo "0") | |
| echo "❌ Pytest tests failed (${FAILED} failures)" | |
| echo "pass_rate=0" >> $GITHUB_OUTPUT | |
| echo "test_summary=Total: ${FAILED} | Passed: 0 | Failed: ${FAILED}" >> $GITHUB_OUTPUT | |
| echo "test_status=failed" >> $GITHUB_OUTPUT | |
| if [ -n "$FAILED_TESTS" ]; then | |
| echo "Failed tests:" | |
| echo "$FAILED_TESTS" | |
| fi | |
| exit 1 | |
| elif grep -q "error" test_output.log; then | |
| echo "⚠️ Pytest encountered errors" | |
| echo "pass_rate=0" >> $GITHUB_OUTPUT | |
| echo "test_summary=Pytest encountered errors" >> $GITHUB_OUTPUT | |
| echo "test_status=error" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| else | |
| # bench_test.sh 模式(或 bench + pytest 模式) | |
| # 使用 tail -1 提取最后一次 Pass rate(合并后的结果) | |
| PASS_RATE_LINE=$(grep -i "Pass rate:" test_output.log | tail -1 || echo "") | |
| FINAL_SUMMARY=$(grep -i "Final Execution Summary" -A 4 test_output.log || echo "") | |
| if [ -n "$PASS_RATE_LINE" ]; then | |
| PASS_RATE=$(echo "$PASS_RATE_LINE" | grep -o '[0-9]*%' | tr -d '%') | |
| # 从 Final Execution Summary 提取合并后的统计数据 | |
| if [ -n "$FINAL_SUMMARY" ]; then | |
| TOTAL=$(echo "$FINAL_SUMMARY" | grep -o "Total: [0-9]*" | tail -1 | grep -o "[0-9]*" || echo "0") | |
| PASSED=$(echo "$FINAL_SUMMARY" | grep -o "Passed: [0-9]*" | tail -1 | grep -o "[0-9]*" || echo "0") | |
| FAILED=$(echo "$FINAL_SUMMARY" | grep -o "Failed: [0-9]*" | tail -1 | grep -o "[0-9]*" || echo "0") | |
| else | |
| # 没有 Final Summary,可能是 bench only 模式 | |
| TOTAL_LINE=$(grep -i "Execution Summary" -A 2 test_output.log | head -3 || echo "") | |
| TOTAL=$(echo "$TOTAL_LINE" | grep -o "Total: [0-9]*" | grep -o "[0-9]*" || echo "0") | |
| PASSED=$(echo "$TOTAL_LINE" | grep -o "Passed: [0-9]*" | grep -o "[0-9]*" || echo "0") | |
| FAILED=$(echo "$TOTAL_LINE" | grep -o "Failed: [0-9]*" | grep -o "[0-9]*" || echo "0") | |
| fi | |
| echo "Found Pass rate: ${PASS_RATE}%" | |
| echo "pass_rate=${PASS_RATE}" >> $GITHUB_OUTPUT | |
| # 构建摘要 | |
| SUMMARY="Total: ${TOTAL} | Passed: ${PASSED} | Failed: ${FAILED} (Pass rate: ${PASS_RATE}%)" | |
| echo "test_summary=${SUMMARY}" >> $GITHUB_OUTPUT | |
| if [ "$PASS_RATE" = "100" ]; then | |
| echo "✅ All tests passed (Pass rate: 100%)" | |
| echo "test_status=passed" >> $GITHUB_OUTPUT | |
| else | |
| echo "❌ Tests failed (Pass rate: ${PASS_RATE}%)" | |
| echo "test_status=failed" >> $GITHUB_OUTPUT | |
| if [ -n "$FAILED_TESTS" ]; then | |
| echo "Failed tests:" | |
| echo "$FAILED_TESTS" | |
| fi | |
| echo "Failing step because Pass rate is not 100%" | |
| exit 1 | |
| fi | |
| else | |
| echo "⚠️ Could not find 'Pass rate:' in test output" | |
| echo "pass_rate=0" >> $GITHUB_OUTPUT | |
| echo "test_summary=No pass rate found in test output" >> $GITHUB_OUTPUT | |
| echo "test_status=error" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| if [ -n "$FINAL_SUMMARY" ]; then | |
| echo "Final Execution Summary:" | |
| echo "$FINAL_SUMMARY" | |
| fi | |
| fi | |
| else | |
| echo "⚠️ Test output file not found" | |
| echo "pass_rate=0" >> $GITHUB_OUTPUT | |
| echo "test_summary=Test output file not found" >> $GITHUB_OUTPUT | |
| echo "test_status=error" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: test-results | |
| path: | | |
| examples/test_results/ | |
| examples/*.log | |
| examples/*.json | |
| retention-days: 30 | |
| notify: | |
| name: Notify Daily Test Results | |
| runs-on: ubuntu-latest | |
| needs: test | |
| if: always() && github.event_name == 'schedule' | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Get test artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: test-results | |
| path: test-results/ | |
| continue-on-error: true | |
| - name: Extract failed tests | |
| id: extract_failed | |
| run: | | |
| if [ -f test-results/examples/test_output.log ]; then | |
| FAILED_TESTS=$(grep -E "^\[FAILED\]" test-results/examples/test_output.log | head -20 || echo "") | |
| if [ -n "$FAILED_TESTS" ]; then | |
| echo "failed_tests<<EOF" >> $GITHUB_OUTPUT | |
| echo "$FAILED_TESTS" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| else | |
| echo "failed_tests=" >> $GITHUB_OUTPUT | |
| fi | |
| else | |
| echo "failed_tests=" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create notification issue | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const testResult = '${{ needs.test.result }}'; | |
| const testStatus = '${{ needs.test.outputs.test_status }}' || testResult || 'error'; | |
| const passRate = '${{ needs.test.outputs.pass_rate }}' || '0'; | |
| const testSummary = '${{ needs.test.outputs.test_summary }}' || `Workflow test job ${testResult || 'completed without summary'}`; | |
| const failedTests = `${{ steps.extract_failed.outputs.failed_tests }}`; | |
| const runDate = new Date().toLocaleString('zh-CN', { | |
| timeZone: 'Asia/Shanghai', | |
| year: 'numeric', | |
| month: '2-digit', | |
| day: '2-digit', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| const statusEmoji = testStatus === 'passed' ? '✅' : (testStatus === 'failed' ? '❌' : '⚠️'); | |
| const statusText = testStatus === 'passed' ? 'All tests passed' : (testStatus === 'failed' ? 'Some tests failed' : 'Tests encountered errors'); | |
| let body = `## 📊 Daily Test Report - ${runDate} | |
| **Status:** ${statusEmoji} ${statusText} | |
| **Summary:** ${testSummary} | |
| **Workflow Run:** [View Details](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) | |
| --- | |
| `; | |
| if (testStatus === 'failed' && failedTests) { | |
| body += `### ❌ Failed Tests (showing first 20) | |
| ${failedTests} | |
| `; | |
| } | |
| body += `### 📁 Artifacts | |
| - [test-results](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) (available for 30 days) | |
| --- | |
| *This issue was automatically created by the daily scheduled test.* | |
| *Assignees: @benyang0506, @fuhouyu-hw, @z00520135*`; | |
| const title = `[Daily Test] ${statusEmoji} ${testSummary} - ${runDate}`; | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: title, | |
| body: body, | |
| assignees: ['benyang0506', 'fuhouyu-hw', 'z00520135'], | |
| labels: ['daily-test', testStatus === 'passed' ? 'test-passed' : 'test-failed'] | |
| }); |