diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98b14508a9..6ceaaf05b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,15 @@ concurrency: cancel-in-progress: true jobs: + # TEMPORARY: only the a5 self-hosted jobs (ut-a5, st-onboard-a5) run, so the + # new [self-hosted, a5, test] runner can be validated in isolation. Every + # other job is gated off with `if: false` and the a5 jobs have their + # `needs:`/`detect-changes` gating dropped. Revert this whole block to + # restore the normal pipeline. + # ---------- Pre-commit hooks (format, lint, clang-tidy) ---------- pre-commit: + if: false runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -62,7 +69,7 @@ jobs: # See docs/python-packaging.md and tools/verify_packaging.sh. packaging-matrix: needs: [detect-changes, pre-commit] - if: needs.detect-changes.outputs.docs_only != 'true' + if: false runs-on: ${{ matrix.os }} timeout-minutes: 60 strategy: @@ -107,7 +114,7 @@ jobs: # ---------- Unit tests (no hardware, Python + C++) ---------- ut: needs: [detect-changes, pre-commit] - if: needs.detect-changes.outputs.docs_only != 'true' + if: false runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: @@ -163,7 +170,7 @@ jobs: # ---------- Simulation scene tests ---------- st-sim-a2a3: needs: [detect-changes, pre-commit] - if: needs.detect-changes.outputs.a2a3_changed == 'true' + if: false runs-on: ${{ matrix.os }} timeout-minutes: 30 env: @@ -257,7 +264,7 @@ jobs: st-sim-a5: needs: [detect-changes, pre-commit] - if: needs.detect-changes.outputs.a5_changed == 'true' + if: false runs-on: ${{ matrix.os }} timeout-minutes: 30 env: @@ -375,7 +382,7 @@ jobs: # 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' + if: false runs-on: ubuntu-latest timeout-minutes: 30 @@ -473,7 +480,7 @@ jobs: # ---------- Unit tests (a2a3 hardware, Python + C++) ---------- ut-a2a3: needs: [detect-changes, pre-commit] - if: needs.detect-changes.outputs.docs_only != 'true' + if: false runs-on: [self-hosted, a2a3] timeout-minutes: 30 env: @@ -559,7 +566,7 @@ jobs: # ---------- Scene tests (a2a3 hardware) ---------- st-onboard-a2a3: needs: [detect-changes, pre-commit] - if: needs.detect-changes.outputs.a2a3_changed == 'true' + if: false runs-on: [self-hosted, a2a3] timeout-minutes: 60 env: @@ -728,6 +735,7 @@ jobs: # ---------- Detect platform-specific changes (runs on GitHub server) ---------- detect-changes: + if: false runs-on: ubuntu-latest outputs: a2a3_changed: ${{ steps.check.outputs.a2a3_changed }} @@ -784,9 +792,7 @@ jobs: # ---------- Unit tests (a5 hardware, Python + C++) ---------- ut-a5: - needs: [detect-changes, pre-commit] - if: needs.detect-changes.outputs.docs_only != 'true' - runs-on: [self-hosted, a5] + runs-on: [self-hosted, test] timeout-minutes: 30 env: SIMPLER_SCHEDULER_TIMEOUT_MS: "2000" @@ -816,6 +822,10 @@ jobs: 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 + # The no_hardware suite needs no device, so it runs outside the + # task-submit lock. --no-tests=error catches a label typo silently + # selecting nothing. + ctest --test-dir tests/ut/cpp/build -L no_hardware -j$(nproc) --no-tests=error --output-on-failure python3 -c " import json, os p = os.environ['DEVICE_RANGE'].split('-'); s, e = p[0], p[-1] # tolerate a single id (no hyphen) @@ -824,6 +834,10 @@ jobs: 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)))") + # No test carries requires_hardware_a5 yet — the only hardware UT, + # test_comm_lifecycle, is a2a3-bound. This selection is therefore + # empty and omits --no-tests=error until the first a5 hardware UT + # lands; the no_hardware run above is what gives this job coverage. 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) @@ -854,9 +868,7 @@ jobs: cmake --build build st-onboard-a5: - needs: [detect-changes, pre-commit] - if: needs.detect-changes.outputs.a5_changed == 'true' - runs-on: [self-hosted, a5] + runs-on: [self-hosted, test] timeout-minutes: 60 env: SIMPLER_SCHEDULER_TIMEOUT_MS: "2000" diff --git a/src/a5/docs/hardware.md b/src/a5/docs/hardware.md index 84f4c2134e..3d9f29ef39 100644 --- a/src/a5/docs/hardware.md +++ b/src/a5/docs/hardware.md @@ -150,6 +150,24 @@ For cross-generation portable code: **always go through ACL or CANN ini, never HAL**. HAL's CORE_NUM semantics shift between a3 and a5 in ways that have no public documentation. +### CPU_TOPO compatibility on newer a5 drivers + +On `Ascend950PR_9579` with driver `25.7.rc1.6`, both host-side and +device-side `AICPU + OCCUPY` report `0x3e`, so cpu_ids 1 through 5 are +the complete user-schedulable pool. Launching five AICPU threads reaches +each of those cpu_ids exactly once. + +The same driver returns `DRV_ERROR_NOT_SUPPORT` for both +`halGetDeviceInfoByBuff(SYSTEM, CPU_TOPO)` and +`dsmi_get_device_info(SOC_INFO, CPU_TOPO)`. Its public DSMI header only +defines SOC_INFO subcommands 0 and 1. + +When CPU_TOPO is unavailable, the host runtime enumerates OCCUPY directly +and treats each set bit as a distinct non-SMT physical CPU. This preserves +the a5 cpu_id-to-cluster mapping while keeping the launch count equal to the +actual user pool. Drivers that provide CPU_TOPO continue to use its detailed +physical and hyperthread metadata. + ## CANN AICPU thread dispatch under varying launch budgets How CANN distributes N AICPU threads across the user pool determines diff --git a/src/a5/platform/onboard/host/aicpu_topology_probe.cpp b/src/a5/platform/onboard/host/aicpu_topology_probe.cpp index b2f4e22ad5..25bacbf4bb 100644 --- a/src/a5/platform/onboard/host/aicpu_topology_probe.cpp +++ b/src/a5/platform/onboard/host/aicpu_topology_probe.cpp @@ -144,7 +144,13 @@ bool probe_aicpu_topology_uncached(uint32_t device_id, std::vector(occupy) + ); + return derive_topology_from_occupy(occupy, out_user_cpus); + } for (uint32_t i = 0; i < topo.total_nums; ++i) { const DsmiSingleCpu &c = topo.cpus[i]; @@ -170,6 +176,21 @@ bool probe_aicpu_topology_uncached(uint32_t device_id, std::vector &out_user_cpus) { + out_user_cpus.clear(); + for (int32_t cpu_id = 0; cpu_id < 64; ++cpu_id) { + if (((occupy >> cpu_id) & 1ULL) == 0) continue; + AicpuLogicalCpu cpu{}; + cpu.cpu_id = cpu_id; + cpu.phy_cpu_id = cpu_id; + cpu.hyperthread_id = 0; + cpu.cluster_id = cpu.phy_cpu_id / 2; + cpu.die_id = cpu.phy_cpu_id / 4; + out_user_cpus.push_back(cpu); + } + return !out_user_cpus.empty(); +} + bool probe_aicpu_topology(uint32_t device_id, std::vector &out_user_cpus) { { std::lock_guard lk(s_topo_cache_mu); diff --git a/src/a5/platform/onboard/host/aicpu_topology_probe.h b/src/a5/platform/onboard/host/aicpu_topology_probe.h index 99df94ebe8..8648db53e0 100644 --- a/src/a5/platform/onboard/host/aicpu_topology_probe.h +++ b/src/a5/platform/onboard/host/aicpu_topology_probe.h @@ -17,9 +17,10 @@ namespace pto::a5 { -// Per-cpu_id metadata used by the packing algorithm. Filled from DSMI -// CPU_TOPO + halGetDeviceInfo(AICPU, OCCUPY). cluster/die ids derive from -// phy_cpu_id via the a5 mapping (cluster = phy/2, die = phy/4). +// Per-cpu_id metadata used by the packing algorithm. Filled from the driver +// CPU_TOPO data when available, otherwise derived from the AICPU OCCUPY +// bitmap. cluster/die ids follow the a5 mapping (cluster = phy/2, +// die = phy/4). struct AicpuLogicalCpu { int32_t cpu_id; int32_t phy_cpu_id; @@ -33,15 +34,21 @@ struct AicpuLogicalCpu { // only contains cpu_ids that are in the device-side OCCUPY bitmap (i.e. // user-schedulable), sorted by cpu_id ascending. // -// This function performs three driver calls: +// This function uses these driver calls: // * halGetDeviceInfo(AICPU, OCCUPY) — user-schedulable bitmap // * halGetDeviceInfoByBuff(SYSTEM, CPU_TOPO) (primary) // * dsmi_get_device_info(SOC_INFO, CPU_TOPO) (fallback) // +// Drivers that do not expose CPU_TOPO use the OCCUPY-only topology below. // All driver entry points are dlsym'd from the host process (CANN is // expected to be already loaded by the surrounding `aclInit` path). bool probe_aicpu_topology(uint32_t device_id, std::vector &out_user_cpus); +// Build conservative a5 topology metadata from a user-schedulable OCCUPY +// bitmap. Every set bit is a distinct non-SMT physical CPU; AICPU cpu_ids +// retain the a5 two-physical-CPUs-per-cluster, two-clusters-per-die layout. +bool derive_topology_from_occupy(uint64_t occupy, std::vector &out_user_cpus); + // Compute the `ALLOWED_CPUS` selection for the surviving threads. // // Inputs: diff --git a/tests/ut/cpp/CMakeLists.txt b/tests/ut/cpp/CMakeLists.txt index 0a5cee98ae..826b372250 100644 --- a/tests/ut/cpp/CMakeLists.txt +++ b/tests/ut/cpp/CMakeLists.txt @@ -732,6 +732,27 @@ target_link_libraries(test_a2a3_aicpu_affinity_select PRIVATE add_test(NAME test_a2a3_aicpu_affinity_select COMMAND test_a2a3_aicpu_affinity_select) set_tests_properties(test_a2a3_aicpu_affinity_select PROPERTIES LABELS "no_hardware") +# a5 CPU_TOPO fallback and affinity selection. Pure logic; the fallback +# enumerates the driver OCCUPY bitmap when detailed topology is unavailable. +set(A5_ONBOARD_HOST_DIR ${CMAKE_SOURCE_DIR}/../../../src/a5/platform/onboard/host) +add_executable(test_a5_aicpu_topology_fallback + a5/test_aicpu_topology_fallback.cpp + ${A5_ONBOARD_HOST_DIR}/aicpu_topology_probe.cpp +) +target_include_directories(test_a5_aicpu_topology_fallback PRIVATE + ${GTEST_INCLUDE_DIRS} + ${A5_ONBOARD_HOST_DIR} + ${SIMPLER_LOG_DIR}/include +) +target_link_libraries(test_a5_aicpu_topology_fallback PRIVATE + ${GTEST_MAIN_LIB} + ${GTEST_LIB} + ${CMAKE_DL_LIBS} + pthread +) +add_test(NAME test_a5_aicpu_topology_fallback COMMAND test_a5_aicpu_topology_fallback) +set_tests_properties(test_a5_aicpu_topology_fallback PROPERTIES LABELS "no_hardware") + # Hardware-gated tests. Block is only entered when the project is configured # with -DSIMPLER_ENABLE_HARDWARE_TESTS=ON. CI's no-hw `ut` job does not pass # this flag, so nothing here is compiled there — this is the structural diff --git a/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp b/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp new file mode 100644 index 0000000000..8ca014fe4b --- /dev/null +++ b/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ + +#include + +#include +#include + +#include "aicpu_topology_probe.h" + +extern "C" { +void unified_log_error(const char *, const char *, ...) {} +void unified_log_warn(const char *, const char *, ...) {} +void unified_log_info_v(const char *, int, const char *, ...) {} +} + +namespace { + +using pto::a5::AicpuLogicalCpu; +using pto::a5::compute_allowed_cpus; +using pto::a5::derive_topology_from_occupy; + +TEST(A5AicpuTopologyFallback, EnumeratesEveryOccupiedCpu) { + std::vector cpus; + + ASSERT_TRUE(derive_topology_from_occupy(0x3e, cpus)); + ASSERT_EQ(cpus.size(), 5U); + + for (int32_t i = 0; i < 5; ++i) { + EXPECT_EQ(cpus[i].cpu_id, i + 1); + EXPECT_EQ(cpus[i].phy_cpu_id, i + 1); + EXPECT_EQ(cpus[i].hyperthread_id, 0); + EXPECT_EQ(cpus[i].cluster_id, (i + 1) / 2); + EXPECT_EQ(cpus[i].die_id, (i + 1) / 4); + } +} + +TEST(A5AicpuTopologyFallback, PreservesAffinitySelection) { + std::vector cpus; + ASSERT_TRUE(derive_topology_from_occupy(0x3e, cpus)); + + std::vector allowed; + ASSERT_TRUE(compute_allowed_cpus(cpus, /*n_sched=*/2, /*n_orch=*/1, allowed)); + EXPECT_EQ(allowed, (std::vector{4, 5, 1})); +} + +TEST(A5AicpuTopologyFallback, RejectsEmptyOccupyMask) { + std::vector cpus = {{1, 1, 0, 0, 0}}; + + EXPECT_FALSE(derive_topology_from_occupy(0, cpus)); + EXPECT_TRUE(cpus.empty()); +} + +} // namespace