Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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"
Expand Down
18 changes: 18 additions & 0 deletions src/a5/docs/hardware.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion src/a5/platform/onboard/host/aicpu_topology_probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,13 @@ bool probe_aicpu_topology_uncached(uint32_t device_id, std::vector<AicpuLogicalC
if (!query_occupy(device_id, occupy)) return false;

DsmiCpuTopo topo{};
if (!query_cpu_topo(device_id, topo)) return false;
if (!query_cpu_topo(device_id, topo)) {
LOG_WARN(
"aicpu_topology_probe: CPU_TOPO unavailable; deriving topology from OCCUPY=0x%llx",
static_cast<unsigned long long>(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];
Expand All @@ -170,6 +176,21 @@ bool probe_aicpu_topology_uncached(uint32_t device_id, std::vector<AicpuLogicalC

} // namespace

bool derive_topology_from_occupy(uint64_t occupy, std::vector<AicpuLogicalCpu> &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<AicpuLogicalCpu> &out_user_cpus) {
{
std::lock_guard<std::mutex> lk(s_topo_cache_mu);
Expand Down
15 changes: 11 additions & 4 deletions src/a5/platform/onboard/host/aicpu_topology_probe.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<AicpuLogicalCpu> &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<AicpuLogicalCpu> &out_user_cpus);

// Compute the `ALLOWED_CPUS` selection for the surviving threads.
//
// Inputs:
Expand Down
21 changes: 21 additions & 0 deletions tests/ut/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>

#include <cstdint>
#include <vector>

#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<AicpuLogicalCpu> 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<AicpuLogicalCpu> cpus;
ASSERT_TRUE(derive_topology_from_occupy(0x3e, cpus));

std::vector<int32_t> allowed;
ASSERT_TRUE(compute_allowed_cpus(cpus, /*n_sched=*/2, /*n_orch=*/1, allowed));
EXPECT_EQ(allowed, (std::vector<int32_t>{4, 5, 1}));
}

TEST(A5AicpuTopologyFallback, RejectsEmptyOccupyMask) {
std::vector<AicpuLogicalCpu> cpus = {{1, 1, 0, 0, 0}};

EXPECT_FALSE(derive_topology_from_occupy(0, cpus));
EXPECT_TRUE(cpus.empty());
}

} // namespace
Loading