diff --git a/docs/design/a5-fg-pg-core-selection.en.md b/docs/design/a5-fg-pg-core-selection.en.md index 9b424cd4ee..a94fdc5244 100644 --- a/docs/design/a5-fg-pg-core-selection.en.md +++ b/docs/design/a5-fg-pg-core-selection.en.md @@ -3,18 +3,18 @@ ## 1. Objective and scope This document defines AICPU core-selection logic for all currently known A5 -topology scenarios: default FG, FG+SMT, one failed cluster (PG1), and two -failed clusters (PG2). - -FG and FG+SMT must be identified separately, but they use the same default -policy. FG+SMT only adds SMT layouts for later exploration. Every scenario uses -`1O+4S` as its default basis; Sections 4-6 define the detailed rules. - -This document defines code design only. The repository already implements the -generic `probe_aicpu_topology()` topology probe and `compute_allowed_cpus()` -packing algorithm. It does not yet implement the PG scenario classification or -the scenario-specific policies proposed here. Section 4 lists other Scheduler -counts and layouts for later hardware performance testing. +topology scenarios: default FG, one failed cluster (PG1), and two failed +clusters (PG2). Scheduler SMT availability is a topology property of these +scenarios, not a separate scenario. A5 supports two through five active AICPU +threads: one Orchestrator and one through four Schedulers. `1O+4S` remains the +automatic default; Sections 4-6 define the detailed rules. + +This document is the design contract for A5 AICPU core selection. The runtime +implements device-side occupancy query, scenario classification +(`kFg` / `kPg1` / `kPg2` / `kUnknown`), the Section 4 policies for every +supported active count (`1O+1S` through `1O+4S`), and the unknown-topology +fallback. Section 4 also lists placement variants reserved for later hardware +performance testing. > **Architecture boundary:** AICPU PG is designed and implemented only for A5. > A2/A3 AICPU does not enter PG. It continues to use the existing non-PG @@ -62,37 +62,35 @@ The authoritative per-logical-CPU contract is `AicpuLogicalCpu` in present in the driver `OCCUPY` bitmap; Control, Data, and system-reserved CPUs are not candidates for O/S placement. -FG+SMT is selected through driver configuration. After changing that -configuration, the tool queries the effective state. `scheduler_smt_enabled` -records this queried state; it is not a temporary Runtime software switch. +`scheduler_smt_enabled` records whether the driver-reported schedulable pool +contains an SMT sibling pair. It is an observed topology property and not a +temporary Runtime software switch. In particular, `kFg` does not imply that +Scheduler SMT is disabled. The runtime may identify the scenario from any of the following sources, in priority order: 1. Prefer the scenario-type query interface in Section 3.3 and directly use - `kFg`, `kFgSmt`, `kPg1`, or `kPg2`. -2. If the interface does not return a type, use the surviving-cluster count, - cross-die distribution, and schedulable SMT sibling state. -3. If cluster information is unavailable, match the driver-reported logical - AICPU count against the current A5 BIOS topology configuration and use SMT - sibling state to distinguish FG from FG+SMT. - -| Scenario | Logical AICPU count | Surviving clusters | SMT state | Distribution | -| -------- | ------------------: | -----------------: | --------- | ------------ | -| FG | 16 (T0-T15) | 4 | Scheduler SMT siblings disabled | Both dies are complete | -| FG+SMT | 16 (T0-T15) | 4 | Scheduler SMT siblings available | Both dies are complete | -| PG1 | 12 (T0-T11) | 3 | As reported by the driver | One complete die and one surviving cluster on the other die | -| PG2 | 8 (T0-T7) | 2 | As reported by the driver | One surviving cluster on each die | - -The logical AICPU count is the total number of logical CPUs exposed by -BIOS/driver, not the number of active Control, Data, and Compute roles. For -example, PG1 exposes 12 logical AICPUs while the diagram contains 10 active -roles. Count-based detection is valid only after confirming that the device -uses the A5 BIOS mapping above. Return `kUnknown` if the count does not match -the mapping or cannot identify a scenario uniquely. FG and FG+SMT have the -same logical-AICPU and cluster counts, so the runtime must distinguish them -using driver-reported schedulable SMT sibling information rather than counts -alone. + `kFg`, `kPg1`, or `kPg2`. +2. If the interface does not return a type, use the surviving-cluster count + and cross-die distribution. +3. Record schedulable SMT sibling state independently in + `scheduler_smt_enabled`; it does not change the scenario type. + +When cluster/die metadata is present (the common host path), classification +**does not** require logical AICPU counts of 16/12/8. Those counts describe one +BIOS mapping; other valid layouts (for example a 9-logical FG SKU) still map by +surviving clusters: + +| Scenario | Surviving clusters | Distribution | SMT property | +| -------- | -----------------: | ------------ | ------------ | +| FG | 4 | Both dies complete (2+2 clusters) | Recorded independently; does not affect `kFg` classification | +| PG1 | 3 | One die 2 clusters, the other 1 | Recorded; default 1O+4S uses SMT pairs | +| PG2 | 2 | One surviving cluster on each die | Recorded; SMT sharing is allowed for 1O+4S | + +Return `kUnknown` only when the cluster/die shape cannot identify a scenario +uniquely. A complete four-cluster, two-die layout is FG regardless of its +current schedulable SMT state. Whether Data 1 and Data 2 are exposed as two independent logical CPUs or one merged logical CPU remains a BIOS/driver question. Core selection consumes only @@ -100,7 +98,7 @@ the final schedulable set returned by the driver. ### 3.3 Scenario-type query interface -Two existing tools provide reusable low-level signals: +The runtime combines these low-level signals: - `tools/cann-examples/query/query.cpp` queries CPU topology through `halGetDeviceInfoByBuff(SYSTEM, CPU_TOPO)`, with @@ -122,25 +120,26 @@ struct AicpuLogicalCpu { }; ``` -The existing `compute_allowed_cpus()` applies a generic topology-aware packing -algorithm to this probe output. Neither it nor the query tools currently provide -the PG scenario classifier or the scenario-specific policies proposed below. +`compute_allowed_cpus()` remains the generic topology-aware packer for packaged +entries that explicitly request the generic policy. Known scenarios use +`classify_aicpu_scenario()` plus `compute_scenario_allowed_cpus()` for every +supported active count from two through five. -> **TODO (not implemented):** Implement `QueryAicpuTopology()` under -> `tools/cann-examples/aicpu-topology-query/`. The interface and output -> format below are proposals and cannot be called by the current version. +The production AICPU SO exposes a one-thread topology-query entry before the +normal affinity-gated launch. It returns device-side `OCCUPY`, `PF_OCCUPY`, and +`OS_SCHED`; Host CPU_TOPO supplies physical, SMT, cluster, and die metadata. ```cpp enum class AicpuScenarioType { kNotApplicable, // A2/A3 - kFg, // A5, 4 clusters, Scheduler SMT disabled - kFgSmt, // A5, 4 clusters, Scheduler SMT enabled + kFg, // A5, 4 clusters; SMT state is recorded separately kPg1, // A5, 3 surviving clusters kPg2, // A5, one surviving cluster on each die kUnknown, }; struct AicpuTopology { + AicpuTopologySource source; // driver, verified JSON, or OCCUPY-only AicpuScenarioType scenario_type; bool scheduler_smt_enabled; uint32_t logical_cpu_count; @@ -148,6 +147,13 @@ struct AicpuTopology { std::vector os_schedulable_cpus; }; +struct AicpuLaunchPlan { + int32_t effective_active_count; + int32_t stable_reachable_count; + int32_t launch_count; + std::vector allowed_cpus; // [S..., O] +}; + AicpuTopology QueryAicpuTopology(uint32_t device_id); ``` @@ -179,35 +185,114 @@ Section 3.2 and returns `kUnknown` when classification is ambiguous. ## 4. Core-selection strategies for all scenarios -Default policies: - -| Scenario | Default `1O+4S` policy | -| -------- | ---------------------- | -| FG | O owns the last suitable physical CPU; every Scheduler uses a dedicated physical CPU close to O; prefer one die | -| FG+SMT | Same as FG; O uses the first logical thread of the last suitable physical CPU and leaves its sibling idle; Schedulers do not use the second SMT thread by default | -| PG1 | Use the fixed SMT compensation layout without searching other layouts. O uses the primary thread of the last suitable physical CPU and leaves its sibling idle. From the remaining candidates, prefer the same cluster as O, then the same die, then the other die, and select two physical CPUs that each provide a complete schedulable SMT pair; the four threads of those pairs become 4S. If this layout is unavailable, return insufficient capacity. The same topology-relative rule handles every failed-cluster position without relying on logical CPU numbering | -| PG2 | The scenario policy overrides the global isolation preferences. Order CPUs by die, cluster, physical CPU, and logical CPU; use the last eligible logical CPU as O without requiring an idle sibling. An S may use O's SMT sibling, and two S roles may use both threads of another physical CPU when necessary. Place Schedulers in O's cluster first and the remaining Schedulers on the other die | - -Under the current BIOS mapping, `[T6, T7, T8, T9, T10]`, with T6-T9 as -Schedulers and T10 as O, is a non-normative example for one right-side PG1 -cluster failure. Implementations must derive the equivalent layout from -`cluster_id`, `die_id`, `phy_cpu_id`, and `hyperthread_id` for either failure -orientation. +Topology policies for `1O+NS`, where `N` is one through four: + +| Scenario | `1O+NS` policy | +| -------- | -------------- | +| FG | O uses the primary thread of the last suitable physical CPU. Select N primary threads on dedicated physical CPUs, preferring O's cluster, then O's die, then the other die. The scenario remains FG whether or not schedulable SMT siblings are present | +| PG1 | O uses the primary thread of the last suitable physical CPU on the die with the most schedulable threads and leaves its sibling idle. Among the remaining CPUs, select one primary thread per physical CPU first, ordered by O's cluster, O's die, then the other die. Consume SMT siblings only when distinct physical CPUs cannot satisfy N. If the remaining logical CPUs are insufficient, return insufficient capacity | +| PG2 | Order CPUs by die, cluster, physical CPU, hyperthread, and logical CPU; use the last eligible logical CPU as O without requiring an idle sibling. Order the remaining candidates by proximity to O and take the first N. An S may use O's SMT sibling, and two S roles may share another physical CPU | + +### 4.1 Concrete Compute topologies and selections + +The following fixtures are the concrete FG, PG1, and PG2 layouts shown in the +design diagrams and covered by the unit tests. `Tn` means logical CPU +`cpu_id == n`; two rows with the same physical CPU are SMT siblings. These +tables show only the Compute CPUs in the driver `OCCUPY` bitmap, because only +that pool can be selected for S/O roles. Scenario classification still uses +the complete CPU_TOPO, including non-Compute CPUs, to determine the surviving +cluster shape. + +The affinity result is always written as `[S..., O]`. + +#### FG concrete layout + +| Logical CPU | Physical CPU | HT | Cluster | Die | +| ----------- | ------------ | -: | ------: | --: | +| T4 | CPU2 | 0 | 1 | 0 | +| T6 | CPU3 | 0 | 1 | 0 | +| T8 | CPU4 | 0 | 2 | 1 | +| T10 | CPU5 | 0 | 2 | 1 | +| T12 | CPU6 | 0 | 3 | 1 | +| T14 | CPU7 | 0 | 3 | 1 | + +All six schedulable Compute CPUs are primary threads on distinct physical +CPUs. O is T14. S starts with T12 in O's cluster, then T8 and T10 on O's die, +and finally T4 on the other die. + +| Active threads | Exact selection | +| -------------: | --------------- | +| 2 (`1S+1O`) | `[S:T12, O:T14]` | +| 3 (`2S+1O`) | `[S:T12, S:T8, O:T14]` | +| 4 (`3S+1O`) | `[S:T12, S:T8, S:T10, O:T14]` | +| 5 (`4S+1O`) | `[S:T12, S:T8, S:T10, S:T4, O:T14]` | + +#### PG1 concrete layout + +| Logical CPU | Physical CPU | HT | Cluster | Die | +| ----------- | ------------ | -: | ------: | --: | +| T4 | CPU2 | 0 | 1 | 0 | +| T6 | CPU3 | 0 | 1 | 0 | +| T7 | CPU3 | 1 | 1 | 0 | +| T8 | CPU4 | 0 | 2 | 1 | +| T9 | CPU4 | 1 | 2 | 1 | +| T10 | CPU5 | 0 | 2 | 1 | +| T11 | CPU5 | 1 | 2 | 1 | + +O is T10, while its SMT sibling T11 is deliberately left idle. The policy +first selects the primary Scheduler threads T8, T4, and T6 on distinct +physical CPUs. T9 is selected only for `4S+1O`, after those primary threads +are exhausted. + +| Active threads | Exact selection | +| -------------: | --------------- | +| 2 (`1S+1O`) | `[S:T8, O:T10]` | +| 3 (`2S+1O`) | `[S:T8, S:T4, O:T10]` | +| 4 (`3S+1O`) | `[S:T8, S:T4, S:T6, O:T10]` | +| 5 (`4S+1O`) | `[S:T8, S:T4, S:T6, S:T9, O:T10]` | + +In the five-thread result, T8/T9 are the only S/S SMT pair; T4 and T6 are +Schedulers on dedicated physical CPUs; T10 is O. T7 is an unused sibling of +T6, and T11 is O's unused sibling. A fully non-SMT `4S+1O` mapping is +impossible because only four Compute physical CPUs remain and one is reserved +for O. Implementations derive the mirrored result from topology metadata. + +#### PG2 concrete layout + +| Logical CPU | Physical CPU | HT | Cluster | Die | +| ----------- | ------------ | -: | ------: | --: | +| T3 | CPU1 | 1 | 0 | 0 | +| T4 | CPU2 | 0 | 2 | 1 | +| T5 | CPU2 | 1 | 2 | 1 | +| T6 | CPU3 | 0 | 2 | 1 | +| T7 | CPU3 | 1 | 2 | 1 | + +PG2 chooses the last eligible logical CPU, T7, as O. Scheduler selection first +fills T4, T5, and T6 in O's cluster and then uses T3 on the other die. + +| Active threads | Exact selection | +| -------------: | --------------- | +| 2 (`1S+1O`) | `[S:T4, O:T7]` | +| 3 (`2S+1O`) | `[S:T4, S:T5, O:T7]` | +| 4 (`3S+1O`) | `[S:T4, S:T5, S:T6, O:T7]` | +| 5 (`4S+1O`) | `[S:T4, S:T5, S:T6, S:T3, O:T7]` | + +T4/T5 are an S/S SMT pair on CPU2. T6 and O:T7 are an S/O SMT pair on CPU3. +T3 is the Scheduler selected from the surviving cluster on the other die. Strategies for later exploration: | Scenario | Strategies to explore | | -------- | --------------------- | -| FG | Compare `1O+2S`, `1O+3S`, and `1O+4S`, plus single-cluster packing, multi-cluster spreading on one die, and cross-die placement | -| FG+SMT | In addition to the FG options, compare one or two Scheduler logical threads per physical CPU, compact SMT, hybrid SMT, and multi-cluster spreading | -| PG1 | Validate only the performance and stability of the fixed SMT compensation layout; do not explore other SMT densities or placements | -| PG2 | Compare default shared-physical-CPU `1O+4S`, dedicated-O `1O+3S`/`1O+2S`, and Scheduler distribution between the two surviving clusters | +| FG | Compare the implemented `1O+1S` through `1O+4S` counts, single-cluster packing, multi-cluster spreading on one die, and cross-die placement. When schedulable SMT siblings are present, also compare compact SMT and hybrid SMT variants | +| PG1 | Compare the minimum-SMT-sharing default with compact SMT only on a machine that exposes the required PG1 topology; this machine cannot perform that hardware comparison | +| PG2 | Compare the implemented shared-physical-CPU counts with dedicated-O variants and alternative Scheduler distribution between the two surviving clusters | ## 5. Global core-selection principles The scenario-specific rules in Section 4 take precedence over the global -placement preferences in this section. In particular, PG1 explicitly uses -Scheduler SMT pairs, and PG2 explicitly permits physical-CPU sharing. +placement preferences in this section. PG1 delays Scheduler SMT sharing until +distinct physical CPUs are exhausted; PG2 explicitly permits physical sharing. Orchestrator: @@ -231,20 +316,20 @@ Scheduler placement and affinity: - Prefer O's cluster, then O's die, and cross dies last. - Prefer a dedicated physical CPU for each Scheduler except where the scenario - policy permits SMT sharing. PG1 uses exactly two complete Scheduler SMT pairs. - PG2 may assign a Scheduler to O's sibling and may assign two Schedulers to - both threads of another physical CPU when necessary. -- Multi-cluster spreading, SMT density, and other Scheduler counts are - exploration items. + policy permits SMT sharing. PG1 uses a sibling only after available primary + threads on other physical CPUs; PG2 may assign a Scheduler to O's sibling and may assign two + Schedulers to both threads of another physical CPU when necessary. +- Alternative multi-cluster spreading and SMT density are exploration items. Policy-specific capacity requirements: -| Scenario | Requirements for the default `1O+4S` layout | -| -------- | ------------------------------------------- | -| FG | At least five suitable physical CPUs; O owns one physical CPU and leaves its sibling idle; four Schedulers use four other distinct physical CPUs | -| FG+SMT | Same as FG; schedulable SMT siblings may exist, but the default layout does not use a second Scheduler thread on any physical CPU | -| PG1 | One suitable physical CPU provides O's primary thread and an idle sibling; two other physical CPUs each provide a complete schedulable SMT pair, supplying 4S | -| PG2 | At least five schedulable logical CPUs; two surviving clusters are present on different dies; O/S and Scheduler/Scheduler physical-CPU sharing is permitted | +- FG requires at least N+1 suitable physical CPUs: O owns one and each + Scheduler uses another primary thread. +- PG1 requires one suitable physical CPU for O and at least N remaining + schedulable logical CPUs. It prefers distinct physical CPUs but does not + require complete SMT pairs. +- PG2 requires at least N+1 schedulable logical CPUs across the identified PG2 + topology; physical-CPU sharing is permitted. Determinism and state consistency: @@ -262,25 +347,44 @@ Determinism and state consistency: ## 6. Common selection flow -1. Call the existing `probe_aicpu_topology()` to obtain the driver-filtered - `os_schedulable_cpus`. After the unified interface is implemented, A5 uses - `QueryAicpuTopology()` to add scenario classification. Until then, the - existing `compute_allowed_cpus()` remains a generic packing algorithm rather - than an implementation of these scenario policies. A2/A3 directly enters - the existing non-PG path. +1. Query device-side `OCCUPY` / `PF_OCCUPY` / `OS_SCHED`, then call + `probe_aicpu_topology()` to merge Host CPU_TOPO metadata, build + `os_schedulable_cpus`, and classify the scenario. A2/A3 stays on the + existing non-PG path and does not enter this flow. 2. Group logical CPUs by `phy_cpu_id`, and retain their `cluster_id`, `die_id`, and `hyperthread_id`. -3. Select a scenario policy from `kFg`, `kFgSmt`, `kPg1`, or `kPg2`. -4. Validate the common requirements and every policy-specific capacity - requirement in Section 5 before assigning any role. A count of five logical - CPUs alone is sufficient only for PG2, subject to its required cross-die - cluster distribution. +3. Select a scenario policy from `kFg`, `kPg1`, or `kPg2`, or take the + `kUnknown` fallback in step 8. +4. Resolve `aicpu_thread_num=0` to the automatic default of five. Reject manual + values outside 2-5. For every known scenario and active count, validate the + common requirements and the policy-specific capacity in Section 5. 5. Place O and S using the selected scenario policy in Section 4. The scenario policy overrides the global placement preferences where Section 4 states an exception. -6. Verify that all five selected IDs are unique and schedulable, then emit - `[S0 ... S3, O]` atomically. Synchronize the actual launch count, Runtime, +6. Verify that all selected IDs are unique and schedulable, then emit + `[S0 ... SN-1, O]` atomically. Synchronize the actual launch count, Runtime, DFX, and affinity state with that complete array. 7. If validation or placement fails, return insufficient capacity without emitting a partial affinity array. -8. For `kUnknown`, disable automatic affinity and emit only the raw probe data. +8. For `kUnknown`, sort valid metadata by die, cluster, physical CPU, + hyperthread, and logical CPU. OCCUPY-only inputs have no physical metadata, + so their order reduces to logical CPU ID. A manual request selects exactly + that many CPUs and fails on insufficient capacity; automatic mode selects up + to five and may shrink to the available count. At least two CPUs are + required. The last selection is O and the preceding selections are S. Emit + a Host warning when this fallback is reached because live CPU_TOPO was + unavailable. Driver-provided but unclassified topology remains observable + through the `kUnknown` scenario without being reported as a query failure. + +Before the generic `kUnknown` fallback, a packaged JSON entry may preserve a +verified CPU_TOPO-less signature. Its SoC and every constraint declared by the +entry must match. The 9599 entry is host-independent but requires exact +device-side `OCCUPY=0x1f8`; the 9579 entry also constrains host architecture. +Entries marked `generic` use `compute_allowed_cpus()` and honor an explicit +active count. + +The fallback's active count is the number selected, while the AICPU launch +count remains the full device-side `OCCUPY` population so the filter gate sees +one representative on every schedulable CPU. This launch count may exceed the +active limit of five and is independently capped at 14. Unknown automatic mode +may shrink the active count; a manual count is never silently changed. diff --git a/docs/dfx/args-dump.md b/docs/dfx/args-dump.md index 6dec96c65a..b8ec950362 100644 --- a/docs/dfx/args-dump.md +++ b/docs/dfx/args-dump.md @@ -841,9 +841,8 @@ host hand-off queue). ### 7.5 Configuration knobs -All defaults live in -[`platform_config.h`](../../src/a2a3/platform/include/common/platform_config.h) -and match between `a2a3` and `a5`: +All defaults live in each platform's `platform_config.h`. The values below +match between `a2a3` and `a5` except for the platform thread cap: | Constant | Default | Effect | | -------- | ------- | ------ | @@ -851,7 +850,7 @@ and match between `a2a3` and `a5`: | `PLATFORM_DUMP_BUFFERS_PER_THREAD` | 8 | Arena size multiplier (a2a3: also SPSC free queue depth) | | `PLATFORM_DUMP_AVG_TENSOR_BYTES` | 64 KiB | Arena size multiplier | | `PLATFORM_DUMP_MAX_DIMS` | 5 | Upper bound on shape / offset arrays | -| `PLATFORM_MAX_AICPU_THREADS` | 7 | Number of dump-producing threads | +| `PLATFORM_MAX_AICPU_THREADS` | a2a3: 4; a5: 5 | Maximum number of dump-producing threads | Per-thread arena = `BUFFERS_PER_THREAD × RECORDS_PER_BUFFER × AVG_TENSOR_BYTES` diff --git a/docs/dfx/chip-swimlane-profiling.md b/docs/dfx/chip-swimlane-profiling.md index ab5f204194..74c8c55a23 100644 --- a/docs/dfx/chip-swimlane-profiling.md +++ b/docs/dfx/chip-swimlane-profiling.md @@ -819,7 +819,7 @@ a5's `ChipSwimlaneCollector` derives from `ProfilerBase` and uses the same framework abstractions as a2a3, including the same split mgmt + collector shard shape (`kMaxCollectorThreads` = -`PLATFORM_MAX_AICPU_THREADS`, i.e. 7 on a5 vs 4 on a2a3, capping the +`PLATFORM_MAX_AICPU_THREADS`, i.e. 5 on a5 vs 4 on a2a3, capping the shard arrays; the live drain/collector count is `min(aicpu_thread_num, kMaxCollectorThreads)`). The behavioral deviation from §5.2 is the **transport channel**: a5 has no @@ -956,7 +956,7 @@ PHASE), same shape as a2a3. | AICPU commit on FIN | identical | | | Buffer model | rotating pool (free + ready queues) per kind | identical | | Ready queue | per-AICPU-thread, multiplexes 4 kinds via `ReadyQueueEntry::kind` | per-AICPU-thread, 2 kinds via `is_phase` | -| Host threads | split mgmt + collector shards, streams during execution | same split mgmt + collector shards (7 = `PLATFORM_MAX_AICPU_THREADS` vs a2a3's 4) | +| Host threads | split mgmt + collector shards, streams during execution | same split mgmt + collector shards (5 = `PLATFORM_MAX_AICPU_THREADS` vs a2a3's 4) | | Host-class shape | `ProfilerBase` (`kBufferKinds = 4`) | same base, `kBufferKinds = 2` | | Host transport | `halHostRegister` shared memory | host-shadow `malloc` + per-tick `rtMemcpy`/`memcpy` | | `MemoryOps` callbacks | 3 (`alloc`, `reg`, `free_`) | 5 (+ `copy_to_device`, `copy_from_device`) | diff --git a/docs/dfx/pmu-profiling.md b/docs/dfx/pmu-profiling.md index 0bd20210e0..dd5733154b 100644 --- a/docs/dfx/pmu-profiling.md +++ b/docs/dfx/pmu-profiling.md @@ -333,7 +333,7 @@ a2a3). At shutdown, AICPU flushes any partially-filled buffers via a5's `PmuCollector` derives from `ProfilerBase` and uses the same framework abstractions as a2a3, including the same split mgmt + collector shard -shape (`kMaxCollectorThreads` = `PLATFORM_MAX_AICPU_THREADS`, i.e. 7 on +shape (`kMaxCollectorThreads` = `PLATFORM_MAX_AICPU_THREADS`, i.e. 5 on a5 vs 4 on a2a3, capping the shard arrays; the live drain/collector count is `min(aicpu_thread_num, kMaxCollectorThreads)`). The behavioral deviation from §5.2 is the **transport channel**: a5 has no @@ -502,7 +502,7 @@ device-side counters. | Counter readout | AICPU MMIO `read_reg` | AICore MMIO `ld_dev` | | Per-core staging | direct write into `records[count]` | dual-issue slots, AICPU commits on FIN | | Buffer model | rotating pool (free + ready queues, SPSC protocol) | identical | -| Host threads | split mgmt + collector shards, writes shard-local temp files during execution and merges at reconcile | same split mgmt + collector shards (7 = `PLATFORM_MAX_AICPU_THREADS` vs a2a3's 4) | +| Host threads | split mgmt + collector shards, writes shard-local temp files during execution and merges at reconcile | same split mgmt + collector shards (5 = `PLATFORM_MAX_AICPU_THREADS` vs a2a3's 4) | | Host-class shape | `ProfilerBase` | identical | | Host transport | `halHostRegister` shared memory | host-shadow `malloc` + per-tick `rtMemcpy`/`memcpy` | | `MemoryOps` callbacks | 3 (`alloc`, `reg`, `free_`) | 5 (+ `copy_to_device`, `copy_from_device`) | diff --git a/simpler_setup/runtime_compiler.py b/simpler_setup/runtime_compiler.py index 2996d41505..d031b5598e 100644 --- a/simpler_setup/runtime_compiler.py +++ b/simpler_setup/runtime_compiler.py @@ -314,6 +314,9 @@ def _build(actual_build_dir: str) -> Union[bytes, Path]: od.mkdir(parents=True, exist_ok=True) dest = od / binary_name shutil.copy2(binary_path, dest) + if target_platform == "host" and self.platform == "a5": + topo_fallback = Path(cmake_source_dir) / "aicpu_cpu_topo_fallback.json" + shutil.copy2(topo_fallback, od / topo_fallback.name) return dest else: with open(binary_path, "rb") as f: diff --git a/src/a5/docs/hardware.md b/src/a5/docs/hardware.md index bb9cd72c27..83417cd560 100644 --- a/src/a5/docs/hardware.md +++ b/src/a5/docs/hardware.md @@ -162,12 +162,48 @@ The same driver returns `DRV_ERROR_NOT_SUPPORT` for both `dsmi_get_device_info(SOC_INFO, CPU_TOPO)`. Its public DSMI header only defines SOC_INFO subcommands 0 and 1. -The OCCUPY-only fallback is restricted to the verified x86 standard-card -signature `Ascend950PR_9579` with `OCCUPY=0x3e`. In that case, the host -runtime treats each set bit as a distinct non-SMT physical CPU. Other hosts, -SoCs, and masks remain unsupported when CPU_TOPO is unavailable. Drivers -that provide CPU_TOPO continue to use its detailed physical and hyperthread -metadata. +The packaged JSON preserves verified CPU_TOPO-less signatures, including +logical-to-physical mapping and selection policy. A signature is used only +when its SoC and every constraint declared by that entry match. The 9599 entry +is host-independent and requires exact device-side `OCCUPY=0x1f8`; the 9579 +entry additionally constrains host architecture. The verified generic policy +continues to honor an explicit `aicpu_thread_num`. +When neither the driver nor a verified JSON entry provides CPU_TOPO, the +runtime uses the set bits in OCCUPY as schedulable CPU IDs and applies the +unknown-topology fallback without inferring physical cores, SMT siblings, +clusters, or dies. + +Live driver topology is still preferred when present. Hardware signatures +without a matching packaged entry use the generic OCCUPY-only fallback and +emit a CPU_TOPO-unavailable warning. + +### Live FG CPU_TOPO example (`Ascend950PR_9599`) + +The following topology was measured on an FG device with CANN 9.2.0. It is +also the source of the verified 9599 packaged fallback: + +```text +cpu_id=0 phy_cpu_id=0 hyperthread_id=0 is_share=0 cpu_mask=0x1 +cpu_id=1 phy_cpu_id=1 hyperthread_id=0 is_share=1 cpu_mask=0x6 +cpu_id=2 phy_cpu_id=1 hyperthread_id=1 is_share=1 cpu_mask=0x6 +cpu_id=3 phy_cpu_id=2 hyperthread_id=0 is_share=0 cpu_mask=0x8 +cpu_id=4 phy_cpu_id=3 hyperthread_id=0 is_share=0 cpu_mask=0x10 +cpu_id=5 phy_cpu_id=4 hyperthread_id=0 is_share=0 cpu_mask=0x20 +cpu_id=6 phy_cpu_id=5 hyperthread_id=0 is_share=0 cpu_mask=0x40 +cpu_id=7 phy_cpu_id=6 hyperthread_id=0 is_share=0 cpu_mask=0x80 +cpu_id=8 phy_cpu_id=7 hyperthread_id=0 is_share=0 cpu_mask=0x100 +``` + +Each row describes one logical CPU. Equal `phy_cpu_id` values identify SMT +siblings, and `hyperthread_id` distinguishes the two logical threads. A5 maps +two physical CPUs to one cluster and two clusters to one die, so the runtime +derives `cluster_id = phy_cpu_id / 2` and `die_id = phy_cpu_id / 4`. + +For this device, `OS_SCHED=0x1` and `OCCUPY=PF_OCCUPY=0x1f8`. Therefore cpu 0 +belongs to the AICPU OS, cpu 1/2 form a Data SMT pair outside the user pool, +and cpu 3..8 are the six Compute CPUs that may receive Scheduler or +Orchestrator roles. The four surviving clusters classify the device as FG; +the automatic `1O+4S` policy selects five of those six Compute CPUs. ## CANN AICPU thread dispatch under varying launch budgets @@ -228,9 +264,9 @@ Scenario A (OCCUPY=0x1f8, 6 user cpus): the failure as `aclrtSynchronizeStream rc=507000` (runtime internal) after the launch. -The runtime implements the safe choice: the host's topology probe sets -`runtime->aicpu_launch_count = popcount(OCCUPY)` after reading the -device-side OCCUPY, and the host's `rtsLaunchCpuKernel` is called with +The runtime implements the safe choice: a one-thread preflight AICPU query +reads device-side OCCUPY, then the host topology probe sets +`runtime->aicpu_launch_count = popcount(OCCUPY)`. The host's `rtsLaunchCpuKernel` is called with that exact value. `PLATFORM_MAX_AICPU_THREADS_JUST_FOR_LAUNCH = 14` remains a compile-time **upper bound** (array sizes, headroom), not the actual launch count. See: @@ -243,6 +279,20 @@ actual launch count. See: - `src/common/platform/onboard/aicpu/platform_aicpu_affinity.cpp` — `platform_aicpu_affinity_gate_filter()` (the post-hoc classifier) +If CPU_TOPO does not match FG, PG1, or PG2 by **surviving cluster/die +layout** (logical CPU count is not a gate), the runtime uses a deterministic +fallback: valid metadata is sorted by `(die, cluster, +physical CPU, hyperthread, logical CPU)`; OCCUPY-only metadata reduces this to +logical CPU ID order. Automatic mode keeps at most five and may shrink to the +available count. A manual request from 2 through 5 must be satisfied exactly. +The last selection is the Orchestrator and preceding selections are +Schedulers. The launch count remains the full device-side OCCUPY population +required by the filter gate and may therefore exceed the active cap. On +`Ascend950PR_9599`, a measured 9-logical layout with four clusters classifies +as FG. Scheduler SMT availability is recorded separately and does not define +another scenario. When live CPU_TOPO was unavailable, the JSON or OCCUPY-only +source independently triggers the required warning. + The 0x7ffe SKU's dispatch behavior at `aicpu_num=14` has **not yet been measured** — once an a5 0x7ffe device runs an a5 onboard test, update this section with the observed (cpu_id → thread) spread. If diff --git a/src/a5/platform/include/common/kernel_args.h b/src/a5/platform/include/common/kernel_args.h index cd37d89793..76c371a9e6 100644 --- a/src/a5/platform/include/common/kernel_args.h +++ b/src/a5/platform/include/common/kernel_args.h @@ -37,6 +37,7 @@ #include #include +#include #include "common/dma_workspace.h" @@ -137,6 +138,33 @@ struct InitArgs { uint64_t dma_workspace_addr[DMA_WORKSPACE_KIND_COUNT]{}; }; +struct AicpuTopologyQueryResult { + int32_t occupy_rc{-1}; + int32_t pf_occupy_rc{-1}; + int32_t os_sched_rc{-1}; + int32_t reserved{0}; + uint64_t occupy{0}; + uint64_t pf_occupy{0}; + uint64_t os_sched{0}; +}; + +struct AicpuTopologyQueryArgs { + uint64_t result_addr{0}; +}; + +static_assert( + std::is_trivially_copyable_v && std::is_standard_layout_v, + "AicpuTopologyQueryResult must remain a memcpy-safe wire type" +); +static_assert(sizeof(AicpuTopologyQueryResult) == 40, "AicpuTopologyQueryResult ABI size drift"); +static_assert(offsetof(AicpuTopologyQueryResult, occupy) == 16, "AicpuTopologyQueryResult::occupy offset drift"); +static_assert(offsetof(AicpuTopologyQueryResult, os_sched) == 32, "AicpuTopologyQueryResult::os_sched offset drift"); +static_assert( + std::is_trivially_copyable_v && std::is_standard_layout_v, + "AicpuTopologyQueryArgs must remain a memcpy-safe wire type" +); +static_assert(sizeof(AicpuTopologyQueryArgs) == 8, "AicpuTopologyQueryArgs ABI size drift"); + /** * RegisterCallableArgs - device orchestration SO registration payload * diff --git a/src/a5/platform/include/common/platform_config.h b/src/a5/platform/include/common/platform_config.h index 4a85fb2eb4..1d749991f1 100644 --- a/src/a5/platform/include/common/platform_config.h +++ b/src/a5/platform/include/common/platform_config.h @@ -49,12 +49,13 @@ constexpr int PLATFORM_AIV_CORES_PER_BLOCKDIM = 2; * Maximum AICPU scheduling threads * Determines parallelism level of the AICPU task scheduler. */ -constexpr int PLATFORM_MAX_AICPU_THREADS = 7; +constexpr int PLATFORM_MAX_AICPU_THREADS = 5; /** * Default active AICPU thread count when aicpu_thread_num is left at 0 (auto): - * 1 orchestrator + 4 schedulers. Onboard topology probing assigns those - * threads to device-visible CPUs; simulation uses the value directly. + * 1 orchestrator + 4 schedulers. Keep this distinct from the active upper + * bound even while both values are 5; the two settings have different runtime + * semantics and match the A2/A3 platform contract. */ constexpr int PLATFORM_DEFAULT_AICPU_THREAD_NUM = 5; // 1 orch + 4 sched @@ -65,10 +66,10 @@ constexpr int PLATFORM_DEFAULT_AICPU_THREAD_NUM = 5; // 1 orch + 4 sched * popcount(OCCUPY), and DeviceRunner passes that count (not this * constant) to rtsLaunchCpuKernel. * - * The bound exists for the static gate buffers in - * src/common/platform/onboard/aicpu/platform_aicpu_affinity.cpp - * (s_filter_thread_cpu[MAX_GATE_THREADS] etc.) — keep it ≥ any - * runtime aicpu_launch_count we expect to see. + * The bound sizes the per-launched-thread phase/task-timing storage and rejects + * a runtime launch population beyond the supported A5 topology. The common + * affinity gate has its own ABI bound, MAX_GATE_THREADS=16; keep this platform + * limit at or below that independent common bound. * * Two over-subscription failure modes to keep in mind when choosing * the runtime launch count vs this bound: diff --git a/src/a5/platform/onboard/aicpu/CMakeLists.txt b/src/a5/platform/onboard/aicpu/CMakeLists.txt index f0ff7716e4..6a68f8e225 100644 --- a/src/a5/platform/onboard/aicpu/CMakeLists.txt +++ b/src/a5/platform/onboard/aicpu/CMakeLists.txt @@ -101,6 +101,8 @@ target_link_directories(aicpu_kernel ${ASCEND_HOME_PATH}/lib64 ) +target_link_libraries(aicpu_kernel PRIVATE ascend_hal) + # Output name set_target_properties(aicpu_kernel PROPERTIES OUTPUT_NAME aicpu_kernel) diff --git a/src/a5/platform/onboard/aicpu/kernel.cpp b/src/a5/platform/onboard/aicpu/kernel.cpp index b5bf0011ef..519aa03dab 100644 --- a/src/a5/platform/onboard/aicpu/kernel.cpp +++ b/src/a5/platform/onboard/aicpu/kernel.cpp @@ -10,6 +10,8 @@ */ #include +#include + #include "common/unified_log.h" #include "common/kernel_args.h" #include "common/platform_config.h" @@ -41,6 +43,23 @@ // exported directly by the TMARB runtime (host_build_graph does not export it). extern "C" int aicpu_execute(Runtime *arg); +extern "C" __attribute__((visibility("default"))) int simpler_aicpu_query_topology(void *arg) { + if (arg == nullptr) return -1; + auto *query_args = reinterpret_cast(arg); + if (query_args->result_addr == 0) return -1; + auto *result = reinterpret_cast(query_args->result_addr); + int64_t value = 0; + result->occupy_rc = halGetDeviceInfo(0, MODULE_TYPE_AICPU, INFO_TYPE_OCCUPY, &value); + result->occupy = static_cast(value); + value = 0; + result->pf_occupy_rc = halGetDeviceInfo(0, MODULE_TYPE_AICPU, INFO_TYPE_PF_OCCUPY, &value); + result->pf_occupy = static_cast(value); + value = 0; + result->os_sched_rc = halGetDeviceInfo(0, MODULE_TYPE_AICPU, INFO_TYPE_OS_SCHED, &value); + result->os_sched = static_cast(value); + return result->occupy_rc == 0 ? 0 : -1; +} + /** * AICPU kernel main execution entry point. * diff --git a/src/a5/platform/onboard/host/CMakeLists.txt b/src/a5/platform/onboard/host/CMakeLists.txt index a571c28e86..7923da21b8 100644 --- a/src/a5/platform/onboard/host/CMakeLists.txt +++ b/src/a5/platform/onboard/host/CMakeLists.txt @@ -96,6 +96,11 @@ list(LENGTH HOST_RUNTIME_SOURCES NUM_HOST_RUNTIME_SOURCES) message(STATUS "Host runtime: ${NUM_HOST_RUNTIME_SOURCES} source files") message(VERBOSE "Host runtime sources: ${HOST_RUNTIME_SOURCES}") add_library(host_runtime SHARED ${HOST_RUNTIME_SOURCES}) +add_custom_command(TARGET host_runtime POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/aicpu_cpu_topo_fallback.json" + "$/aicpu_cpu_topo_fallback.json" +) # C++ standard (applied only to C++ files) set_target_properties(host_runtime PROPERTIES diff --git a/src/a5/platform/onboard/host/aicpu_cpu_topo_fallback.json b/src/a5/platform/onboard/host/aicpu_cpu_topo_fallback.json new file mode 100644 index 0000000000..b70d121c37 --- /dev/null +++ b/src/a5/platform/onboard/host/aicpu_cpu_topo_fallback.json @@ -0,0 +1,31 @@ +{ + "_comment": "Ascend950PR_9599 CPU_TOPO verified by live DSMI dump on 2026-08-05 (CANN 9.2.0). Full 9-logical layout kept for FG cluster/die classification; only role=compute entries are O/S-schedulable (device OCCUPY=0x1f8). cpu0=Control/OS_SCHED, cpu1+2=Data SMT pair on phy1. role/available are documentation fields ignored by the loader; occupy_mask is the match gate. Used only when live CPU_TOPO is unavailable.", + "socs": { + "Ascend950PR_9599": { + "occupy_mask": 504, + "cpus": [ + {"cpu_id": 0, "phy_cpu_id": 0, "hyperthread_id": 0, "role": "control", "available": false}, + {"cpu_id": 1, "phy_cpu_id": 1, "hyperthread_id": 0, "role": "data", "available": false}, + {"cpu_id": 2, "phy_cpu_id": 1, "hyperthread_id": 1, "role": "data", "available": false}, + {"cpu_id": 3, "phy_cpu_id": 2, "hyperthread_id": 0, "role": "compute", "available": true}, + {"cpu_id": 4, "phy_cpu_id": 3, "hyperthread_id": 0, "role": "compute", "available": true}, + {"cpu_id": 5, "phy_cpu_id": 4, "hyperthread_id": 0, "role": "compute", "available": true}, + {"cpu_id": 6, "phy_cpu_id": 5, "hyperthread_id": 0, "role": "compute", "available": true}, + {"cpu_id": 7, "phy_cpu_id": 6, "hyperthread_id": 0, "role": "compute", "available": true}, + {"cpu_id": 8, "phy_cpu_id": 7, "hyperthread_id": 0, "role": "compute", "available": true} + ] + }, + "Ascend950PR_9579": { + "host_arch": "x86_64", + "occupy_mask": 62, + "selection_policy": "generic", + "cpus": [ + {"cpu_id": 1, "phy_cpu_id": 1, "hyperthread_id": 0, "role": "compute", "available": true}, + {"cpu_id": 2, "phy_cpu_id": 2, "hyperthread_id": 0, "role": "compute", "available": true}, + {"cpu_id": 3, "phy_cpu_id": 3, "hyperthread_id": 0, "role": "compute", "available": true}, + {"cpu_id": 4, "phy_cpu_id": 4, "hyperthread_id": 0, "role": "compute", "available": true}, + {"cpu_id": 5, "phy_cpu_id": 5, "hyperthread_id": 0, "role": "compute", "available": true} + ] + } + } +} diff --git a/src/a5/platform/onboard/host/aicpu_topology_probe.cpp b/src/a5/platform/onboard/host/aicpu_topology_probe.cpp index 717bcb8f2c..3f81154d1e 100644 --- a/src/a5/platform/onboard/host/aicpu_topology_probe.cpp +++ b/src/a5/platform/onboard/host/aicpu_topology_probe.cpp @@ -14,10 +14,19 @@ #include #include +#include +#include #include +#include +#include #include +#include +#include +#include +#include #include +#include "common/platform_config.h" #include "common/unified_log.h" #include "common/acl_hal_device.h" @@ -61,6 +70,15 @@ struct DsmiCpuTopo { DsmiSingleCpu cpus[kCpuTopoMaxLogical]; }; +static_assert(sizeof(DsmiSingleCpu) == 16, "DsmiSingleCpu ABI size drift"); +static_assert(offsetof(DsmiSingleCpu, cpu_id) == 8, "DsmiSingleCpu::cpu_id offset drift"); +static_assert(offsetof(DsmiSingleCpu, hyperthread_id) == 11, "DsmiSingleCpu::hyperthread_id offset drift"); +static_assert(offsetof(DsmiCpuTopo, cpus) == 8, "DsmiCpuTopo::cpus offset drift"); + +bool fill_dsmi_topo_from_json( + const char *soc_name, uint64_t occupy, DsmiCpuTopo &out, bool &out_generic_selection_only +); + // dlsym helpers — keep error reporting at WARN, callers fall back. using HalGetDeviceInfoFn = int (*)(uint64_t deviceId, int32_t moduleType, int32_t infoType, int64_t *value); using HalGetDeviceInfoByBuffFn = @@ -237,6 +255,21 @@ bool derive_topology_from_occupy(const char *soc_name, uint64_t occupy, std::vec return !out_user_cpus.empty(); } +bool enumerate_cpus_from_occupy(uint64_t occupy, std::vector &out_user_cpus) { + out_user_cpus.clear(); + for (int32_t cpu_id = 0; cpu_id < static_cast(kCpuTopoMaxLogical); ++cpu_id) { + if (((occupy >> cpu_id) & 1ULL) == 0) continue; + AicpuLogicalCpu cpu{}; + cpu.cpu_id = cpu_id; + cpu.phy_cpu_id = -1; + cpu.hyperthread_id = -1; + cpu.cluster_id = -1; + cpu.die_id = -1; + 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); @@ -269,6 +302,126 @@ bool probe_aicpu_topology(uint32_t device_id, std::vector &out_ return true; } +namespace { +bool validate_cpu_topology(const std::vector &cpus); +bool validate_cpu_ids(const std::vector &cpus); +std::vector clusters_of(const std::vector &cpus); +bool has_schedulable_smt_pair(const std::vector &cpus); +} // namespace + +bool probe_aicpu_topology( + uint32_t device_id, const AicpuDeviceOccupancy &device_occupancy, AicpuTopology &out_topology +) { + out_topology = {}; + out_topology.device_occupancy = device_occupancy; + if (!device_occupancy.occupy_valid || device_occupancy.occupy == 0) return false; + + const char *soc_name = query_soc_name(); + if (soc_name != nullptr) out_topology.soc_name = soc_name; + + DsmiCpuTopo topo{}; + std::vector all_cpus; + const char *source = "driver"; + bool occupy_only_fallback = false; + if (!query_cpu_topo(device_id, topo)) { + if (load_cpu_topo_from_json( + soc_name, device_occupancy.occupy, all_cpus, &out_topology.generic_selection_only + )) { + source = "json_fallback"; + out_topology.source = AicpuTopologySource::kJsonFallback; + out_topology.logical_cpu_count = static_cast(all_cpus.size()); + } else if (enumerate_cpus_from_occupy(device_occupancy.occupy, all_cpus)) { + source = "occupy_fallback"; + out_topology.source = AicpuTopologySource::kOccupyFallback; + occupy_only_fallback = true; + out_topology.logical_cpu_count = static_cast(all_cpus.size()); + } else { + return false; + } + } else { + out_topology.logical_cpu_count = topo.total_nums; + all_cpus.reserve(topo.total_nums); + for (uint32_t i = 0; i < topo.total_nums; ++i) { + const DsmiSingleCpu &cpu = topo.cpus[i]; + if (cpu.cpu_id >= kCpuTopoMaxLogical) return false; + AicpuLogicalCpu entry{}; + entry.cpu_id = static_cast(cpu.cpu_id); + entry.phy_cpu_id = static_cast(cpu.phy_cpu_id); + entry.hyperthread_id = static_cast(cpu.hyperthread_id); + entry.cluster_id = entry.phy_cpu_id / 2; + entry.die_id = entry.phy_cpu_id / 4; + all_cpus.push_back(entry); + } + } + LOG_INFO( + "aicpu_topology_probe: CPU_TOPO source=%s soc=%s logical=%u", source, + out_topology.soc_name.empty() ? "(unknown)" : out_topology.soc_name.c_str(), out_topology.logical_cpu_count + ); + + std::sort(all_cpus.begin(), all_cpus.end(), [](const auto &a, const auto &b) { + return a.cpu_id < b.cpu_id; + }); + for (const auto &cpu : all_cpus) { + if (cpu.cpu_id < 64 && ((device_occupancy.occupy >> cpu.cpu_id) & 1ULL) != 0) { + out_topology.os_schedulable_cpus.push_back(cpu); + } + } + const bool valid_cpus = occupy_only_fallback ? validate_cpu_ids(all_cpus) : validate_cpu_topology(all_cpus); + const bool valid_schedulable = occupy_only_fallback ? validate_cpu_ids(out_topology.os_schedulable_cpus) : + validate_cpu_topology(out_topology.os_schedulable_cpus); + if (!valid_cpus || !valid_schedulable || out_topology.os_schedulable_cpus.empty()) { + out_topology = {}; + return false; + } + if (!occupy_only_fallback) { + out_topology.surviving_cluster_ids = clusters_of(all_cpus); + out_topology.scheduler_smt_enabled = has_schedulable_smt_pair(out_topology.os_schedulable_cpus); + out_topology.scenario_type = + classify_aicpu_scenario(out_topology.logical_cpu_count, all_cpus, out_topology.os_schedulable_cpus); + } + + { + std::ostringstream dies_os, clusters_os, smt_os, sched_os; + std::set dies; + for (const auto &cpu : all_cpus) + dies.insert(cpu.die_id); + for (int32_t d : dies) { + if (dies_os.tellp() > 0) dies_os << ','; + dies_os << d; + } + for (size_t i = 0; i < out_topology.surviving_cluster_ids.size(); ++i) { + if (i) clusters_os << ','; + clusters_os << out_topology.surviving_cluster_ids[i]; + } + std::unordered_map> phy_to_cpus; + for (const auto &cpu : all_cpus) + phy_to_cpus[cpu.phy_cpu_id].push_back(cpu.cpu_id); + for (auto &entry : phy_to_cpus) { + auto &ids = entry.second; + if (ids.size() < 2) continue; + std::sort(ids.begin(), ids.end()); + if (smt_os.tellp() > 0) smt_os << ','; + smt_os << "phy" << entry.first << ':'; + for (size_t i = 0; i < ids.size(); ++i) { + if (i) smt_os << '+'; + smt_os << ids[i]; + } + } + for (size_t i = 0; i < out_topology.os_schedulable_cpus.size(); ++i) { + if (i) sched_os << ','; + sched_os << out_topology.os_schedulable_cpus[i].cpu_id; + } + LOG_DEBUG( + "aicpu_topology: scenario=%s dies=[%s] clusters=[%s] smt_pairs=[%s] " + "schedulable=[%s] scheduler_smt_enabled=%d logical=%u", + aicpu_scenario_name(out_topology.scenario_type), dies_os.str().c_str(), clusters_os.str().c_str(), + smt_os.str().c_str(), sched_os.str().c_str(), out_topology.scheduler_smt_enabled ? 1 : 0, + out_topology.logical_cpu_count + ); + } + return true; +} + namespace { // Step 1 — return indices into `user_cpus` for n_sched threads placed @@ -430,4 +583,624 @@ bool compute_allowed_cpus( return true; } +namespace { + +auto topology_key(const AicpuLogicalCpu &cpu) { + return std::make_tuple(cpu.die_id, cpu.cluster_id, cpu.phy_cpu_id, cpu.hyperthread_id, cpu.cpu_id); +} + +bool validate_cpu_topology(const std::vector &cpus) { + std::set cpu_ids; + std::set> physical_threads; + for (const auto &cpu : cpus) { + if (cpu.cpu_id < 0 || cpu.phy_cpu_id < 0 || cpu.cluster_id < 0 || cpu.die_id < 0) return false; + if (cpu.hyperthread_id < 0 || cpu.hyperthread_id > 1) return false; + if (!cpu_ids.insert(cpu.cpu_id).second) return false; + if (!physical_threads.emplace(cpu.phy_cpu_id, cpu.hyperthread_id).second) return false; + } + return true; +} + +bool validate_cpu_ids(const std::vector &cpus) { + std::set cpu_ids; + for (const auto &cpu : cpus) { + if (cpu.cpu_id < 0 || !cpu_ids.insert(cpu.cpu_id).second) return false; + } + return true; +} + +std::vector clusters_of(const std::vector &cpus) { + std::set clusters; + for (const auto &cpu : cpus) + clusters.insert(cpu.cluster_id); + return {clusters.begin(), clusters.end()}; +} + +bool has_schedulable_smt_pair(const std::vector &cpus) { + std::set> threads; + for (const auto &cpu : cpus) + threads.emplace(cpu.phy_cpu_id, cpu.hyperthread_id); + for (const auto &cpu : cpus) { + if (threads.count({cpu.phy_cpu_id, 0}) != 0 && threads.count({cpu.phy_cpu_id, 1}) != 0) return true; + } + return false; +} + +std::vector sorted_cpus(const std::vector &cpus) { + std::vector ordered = cpus; + std::sort(ordered.begin(), ordered.end(), [](const auto &a, const auto &b) { + return topology_key(a) < topology_key(b); + }); + return ordered; +} + +int proximity_rank(const AicpuLogicalCpu &cpu, const AicpuLogicalCpu &orch) { + if (cpu.cluster_id == orch.cluster_id) return 0; + if (cpu.die_id == orch.die_id) return 1; + return 2; +} + +// Prefer the die with the most schedulable threads; tie → higher die_id. +int32_t preferred_die_id(const std::vector &pool) { + std::unordered_map counts; + for (const auto &cpu : pool) + ++counts[cpu.die_id]; + int32_t best_die = -1; + int32_t best_count = -1; + for (const auto &entry : counts) { + if (entry.second > best_count || (entry.second == best_count && entry.first > best_die)) { + best_count = entry.second; + best_die = entry.first; + } + } + return best_die; +} + +bool pick_orchestrator_primary(const std::vector &pool, AicpuLogicalCpu &out_orch) { + const int32_t die = preferred_die_id(pool); + if (die < 0) return false; + std::vector primaries; + for (const auto &cpu : pool) { + if (cpu.die_id == die && cpu.hyperthread_id == 0) primaries.push_back(cpu); + } + if (primaries.empty()) return false; + out_orch = sorted_cpus(primaries).back(); + return true; +} + +} // namespace + +AicpuScenarioType classify_aicpu_scenario( + uint32_t logical_cpu_count, const std::vector &all_logical_cpus, + const std::vector &os_schedulable_cpus +) { + // FG/PG1/PG2 come from cluster/die layout only. Scheduler SMT + // availability is an orthogonal topology property, not a scenario. + if (!validate_cpu_topology(all_logical_cpus) || !validate_cpu_topology(os_schedulable_cpus) || + logical_cpu_count != all_logical_cpus.size()) { + return AicpuScenarioType::kUnknown; + } + for (const auto &candidate : os_schedulable_cpus) { + auto it = std::find_if(all_logical_cpus.begin(), all_logical_cpus.end(), [&](const auto &cpu) { + return cpu.cpu_id == candidate.cpu_id && cpu.phy_cpu_id == candidate.phy_cpu_id && + cpu.hyperthread_id == candidate.hyperthread_id; + }); + if (it == all_logical_cpus.end()) return AicpuScenarioType::kUnknown; + } + const auto clusters = clusters_of(all_logical_cpus); + std::set dies; + std::unordered_map clusters_per_die; + for (const auto &cpu : all_logical_cpus) + dies.insert(cpu.die_id); + for (int32_t cluster : clusters) + ++clusters_per_die[cluster / 2]; + + if (clusters.size() == 4 && dies == std::set({0, 1}) && clusters_per_die[0] == 2 && + clusters_per_die[1] == 2) { + return AicpuScenarioType::kFg; + } + if (clusters.size() == 3 && dies == std::set({0, 1}) && + ((clusters_per_die[0] == 2 && clusters_per_die[1] == 1) || + (clusters_per_die[0] == 1 && clusters_per_die[1] == 2))) { + return AicpuScenarioType::kPg1; + } + if (clusters.size() == 2 && dies == std::set({0, 1}) && clusters_per_die[0] == 1 && + clusters_per_die[1] == 1) { + return AicpuScenarioType::kPg2; + } + return AicpuScenarioType::kUnknown; +} + +bool compute_unknown_allowed_cpus( + const AicpuTopology &topology, int32_t active_count, std::vector &out_allowed_cpus +) { + out_allowed_cpus.clear(); + if (active_count < 2 || active_count > 5 || !validate_cpu_ids(topology.os_schedulable_cpus) || + topology.os_schedulable_cpus.size() < static_cast(active_count)) { + return false; + } + const bool has_topology = validate_cpu_topology(topology.os_schedulable_cpus); + auto ordered = has_topology ? sorted_cpus(topology.os_schedulable_cpus) : topology.os_schedulable_cpus; + if (!has_topology) { + std::sort(ordered.begin(), ordered.end(), [](const auto &a, const auto &b) { + return a.cpu_id < b.cpu_id; + }); + } + std::vector selected; + selected.reserve(static_cast(active_count)); + for (int32_t i = 0; i < active_count; ++i) + selected.push_back(ordered[i].cpu_id); + out_allowed_cpus = std::move(selected); + return true; +} + +bool compute_scenario_allowed_cpus( + const AicpuTopology &topology, int32_t active_count, std::vector &out_allowed_cpus +) { + out_allowed_cpus.clear(); + const auto &pool = topology.os_schedulable_cpus; + if (active_count < 2 || active_count > 5 || !validate_cpu_topology(pool) || + pool.size() < static_cast(active_count) || topology.scenario_type == AicpuScenarioType::kUnknown || + topology.scenario_type == AicpuScenarioType::kNotApplicable) { + return false; + } + const size_t scheduler_count = static_cast(active_count - 1); + auto ordered = sorted_cpus(pool); + std::vector selected; + selected.reserve(static_cast(active_count)); + + if (topology.scenario_type == AicpuScenarioType::kPg2) { + const AicpuLogicalCpu orch = ordered.back(); + std::vector sched; + for (const auto &cpu : ordered) + if (cpu.cpu_id != orch.cpu_id) sched.push_back(cpu); + std::sort(sched.begin(), sched.end(), [&](const auto &a, const auto &b) { + return std::make_tuple(proximity_rank(a, orch), topology_key(a)) < + std::make_tuple(proximity_rank(b, orch), topology_key(b)); + }); + if (sched.size() < scheduler_count) return false; + for (size_t i = 0; i < scheduler_count; ++i) + selected.push_back(sched[i].cpu_id); + selected.push_back(orch.cpu_id); + out_allowed_cpus = std::move(selected); + return true; + } + + // FG / PG1: O on the die with the most schedulable threads (ht0 only). + AicpuLogicalCpu orch{}; + if (!pick_orchestrator_primary(pool, orch)) return false; + + if (topology.scenario_type == AicpuScenarioType::kPg1) { + // Minimise scheduler SMT sharing: place one primary per physical CPU + // first, then consume siblings only when the requested scheduler count + // cannot fit on distinct physical CPUs. Never share O's physical CPU. + std::vector sched; + for (const auto &cpu : pool) + if (cpu.phy_cpu_id != orch.phy_cpu_id) sched.push_back(cpu); + std::sort(sched.begin(), sched.end(), [&](const auto &a, const auto &b) { + return std::make_tuple(a.hyperthread_id, proximity_rank(a, orch), topology_key(a)) < + std::make_tuple(b.hyperthread_id, proximity_rank(b, orch), topology_key(b)); + }); + if (sched.size() < scheduler_count) return false; + for (size_t i = 0; i < scheduler_count; ++i) + selected.push_back(sched[i].cpu_id); + } else { + // FG: use N-1 dedicated physical-core primaries near O. + std::vector sched; + for (const auto &cpu : pool) { + if (cpu.hyperthread_id == 0 && cpu.phy_cpu_id != orch.phy_cpu_id) sched.push_back(cpu); + } + std::sort(sched.begin(), sched.end(), [&](const auto &a, const auto &b) { + return std::make_tuple(proximity_rank(a, orch), topology_key(a)) < + std::make_tuple(proximity_rank(b, orch), topology_key(b)); + }); + if (sched.size() < scheduler_count) return false; + for (size_t i = 0; i < scheduler_count; ++i) + selected.push_back(sched[i].cpu_id); + } + selected.push_back(orch.cpu_id); + out_allowed_cpus = std::move(selected); + return true; +} + +bool build_aicpu_launch_plan( + const AicpuTopology &topology, int32_t requested_active_count, AicpuLaunchPlan &out_plan, std::string &out_error +) { + out_plan = {}; + out_error.clear(); + out_plan.requested_active_count = requested_active_count; + const bool automatic = requested_active_count == 0; + if (!automatic && (requested_active_count < 2 || requested_active_count > PLATFORM_MAX_AICPU_THREADS)) { + out_error = "requested active count must be in [2, " + std::to_string(PLATFORM_MAX_AICPU_THREADS) + "]"; + return false; + } + + const size_t stable_reachable = topology.os_schedulable_cpus.size(); + out_plan.stable_reachable_count = static_cast(stable_reachable); + if (stable_reachable < 2) { + out_error = "fewer than two stable reachable AICPU CPUs"; + return false; + } + if (stable_reachable > static_cast(PLATFORM_MAX_AICPU_THREADS_JUST_FOR_LAUNCH)) { + out_error = "stable reachable AICPU count " + std::to_string(stable_reachable) + " exceeds launch capacity " + + std::to_string(PLATFORM_MAX_AICPU_THREADS_JUST_FOR_LAUNCH); + return false; + } + + const int32_t desired = automatic ? PLATFORM_DEFAULT_AICPU_THREAD_NUM : requested_active_count; + const int32_t effective = automatic ? std::min(desired, out_plan.stable_reachable_count) : desired; + if (!automatic && stable_reachable < static_cast(effective)) { + out_error = "stable reachable AICPU count " + std::to_string(stable_reachable) + + " cannot satisfy requested active count " + std::to_string(effective); + return false; + } + + out_plan.effective_active_count = effective; + out_plan.launch_count = out_plan.stable_reachable_count; + out_plan.warn_stable_reachable_below_default = + stable_reachable < static_cast(PLATFORM_DEFAULT_AICPU_THREAD_NUM); + out_plan.warn_cpu_topology_unavailable = topology.source != AicpuTopologySource::kDriver; + + bool selected = false; + if (topology.generic_selection_only) { + selected = compute_allowed_cpus(topology.os_schedulable_cpus, effective - 1, 1, out_plan.allowed_cpus); + } else if (topology.scenario_type == AicpuScenarioType::kUnknown) { + selected = compute_unknown_allowed_cpus(topology, effective, out_plan.allowed_cpus); + } else { + selected = compute_scenario_allowed_cpus(topology, effective, out_plan.allowed_cpus); + } + if (!selected) { + out_error = "AICPU selection policy cannot satisfy effective active count " + std::to_string(effective); + out_plan.allowed_cpus.clear(); + return false; + } + return true; +} + +const char *aicpu_scenario_name(AicpuScenarioType scenario) { + switch (scenario) { + case AicpuScenarioType::kNotApplicable: + return "NOT_APPLICABLE"; + case AicpuScenarioType::kFg: + return "FG"; + case AicpuScenarioType::kPg1: + return "PG1"; + case AicpuScenarioType::kPg2: + return "PG2"; + case AicpuScenarioType::kUnknown: + return "UNKNOWN"; + } + return "UNKNOWN"; +} + +const char *aicpu_topology_source_name(AicpuTopologySource source) { + switch (source) { + case AicpuTopologySource::kDriver: + return "driver"; + case AicpuTopologySource::kJsonFallback: + return "json_fallback"; + case AicpuTopologySource::kOccupyFallback: + return "occupy_fallback"; + } + return "unknown"; +} + +namespace { + +constexpr char kCpuTopoFallbackRelativePath[] = "src/a5/platform/onboard/host/aicpu_cpu_topo_fallback.json"; +constexpr char kCpuTopoFallbackFileName[] = "aicpu_cpu_topo_fallback.json"; + +void skip_json_ws(const char *&p) { + while (*p != '\0' && std::isspace(static_cast(*p))) + ++p; +} + +bool parse_json_uint(const char *&p, unsigned int &out) { + skip_json_ws(p); + if (!std::isdigit(static_cast(*p))) return false; + unsigned long v = 0; + while (std::isdigit(static_cast(*p))) { + v = v * 10UL + static_cast(*p - '0'); + if (v > 0xffffffffUL) return false; + ++p; + } + out = static_cast(v); + return true; +} + +bool parse_json_string(const char *&p, std::string &out) { + skip_json_ws(p); + if (*p != '"') return false; + ++p; + out.clear(); + while (*p != '\0' && *p != '"') { + if (*p == '\\') { + ++p; + if (*p == '\0') return false; + } + out.push_back(*p++); + } + if (*p != '"') return false; + ++p; + return true; +} + +// Find `"key"` then `:`, leave `p` on the value. +bool find_json_key(const char *&p, const char *key) { + const std::string needle = std::string("\"") + key + "\""; + const char *found = std::strstr(p, needle.c_str()); + if (found == nullptr) return false; + p = found + needle.size(); + skip_json_ws(p); + if (*p != ':') return false; + ++p; + skip_json_ws(p); + return true; +} + +// Skip one JSON value (number / string / true / false / null). Nested +// objects/arrays are not needed for cpu entries. +bool skip_json_value(const char *&p) { + skip_json_ws(p); + if (*p == '"') { + std::string unused; + return parse_json_string(p, unused); + } + if (std::strncmp(p, "true", 4) == 0) { + p += 4; + return true; + } + if (std::strncmp(p, "false", 5) == 0) { + p += 5; + return true; + } + if (std::strncmp(p, "null", 4) == 0) { + p += 4; + return true; + } + if (*p == '-' || std::isdigit(static_cast(*p))) { + if (*p == '-') ++p; + if (!std::isdigit(static_cast(*p))) return false; + while (std::isdigit(static_cast(*p))) + ++p; + return true; + } + return false; +} + +bool parse_one_cpu_object(const char *&p, DsmiSingleCpu &out) { + skip_json_ws(p); + if (*p != '{') return false; + ++p; + out = {}; + bool got_cpu = false, got_phy = false, got_ht = false; + while (true) { + skip_json_ws(p); + if (*p == '}') { + ++p; + return got_cpu && got_phy && got_ht; + } + std::string key; + if (!parse_json_string(p, key)) return false; + skip_json_ws(p); + if (*p != ':') return false; + ++p; + skip_json_ws(p); + // Topology wire fields are uints; role/available and other annotations + // are documentation-only and must be skippable without failing the load. + if (key == "cpu_id" || key == "phy_cpu_id" || key == "hyperthread_id") { + unsigned int val = 0; + if (!parse_json_uint(p, val) || val > 255U) return false; + if (key == "cpu_id") { + out.cpu_id = static_cast(val); + got_cpu = true; + } else if (key == "phy_cpu_id") { + out.phy_cpu_id = static_cast(val); + got_phy = true; + } else { + out.hyperthread_id = static_cast(val); + got_ht = true; + } + } else if (!skip_json_value(p)) { + return false; + } + skip_json_ws(p); + if (*p == ',') { + ++p; + continue; + } + if (*p == '}') { + ++p; + return got_cpu && got_phy && got_ht; + } + return false; + } +} + +const char *host_arch_name() { +#if defined(__x86_64__) + return "x86_64"; +#elif defined(__aarch64__) + return "aarch64"; +#else + return "unsupported"; +#endif +} + +bool find_json_key_before(const char *start, const char *end, const char *key, const char *&value) { + value = start; + if (!find_json_key(value, key) || value >= end) return false; + return true; +} + +bool parse_cpu_topo_json_for_soc( + const char *text, const char *soc_name, uint64_t occupy, DsmiCpuTopo &out, bool &out_generic_selection_only +) { + if (text == nullptr || soc_name == nullptr || soc_name[0] == '\0') return false; + out_generic_selection_only = false; + const char *p = text; + if (!find_json_key(p, "socs")) return false; + if (*p != '{') return false; + ++p; + + // Walk soc entries until we find soc_name. + while (true) { + skip_json_ws(p); + if (*p == '}') return false; + std::string key; + if (!parse_json_string(p, key)) return false; + skip_json_ws(p); + if (*p != ':') return false; + ++p; + skip_json_ws(p); + if (*p != '{') return false; + + if (key != soc_name) { + // Skip this soc object by brace depth. + int depth = 0; + do { + if (*p == '{') ++depth; + else if (*p == '}') --depth; + else if (*p == '\0') return false; + ++p; + } while (depth > 0); + skip_json_ws(p); + if (*p == ',') { + ++p; + continue; + } + if (*p == '}') return false; + return false; + } + + const char *obj_end = p; + int depth = 0; + do { + if (*obj_end == '{') ++depth; + else if (*obj_end == '}') --depth; + else if (*obj_end == '\0') return false; + ++obj_end; + } while (depth > 0); + + const char *value = nullptr; + if (find_json_key_before(p, obj_end, "host_arch", value)) { + std::string required_arch; + if (!parse_json_string(value, required_arch) || required_arch != host_arch_name()) return false; + } + if (find_json_key_before(p, obj_end, "occupy_mask", value)) { + unsigned int required_occupy = 0; + if (!parse_json_uint(value, required_occupy) || occupy != required_occupy) return false; + } + if (find_json_key_before(p, obj_end, "selection_policy", value)) { + std::string selection_policy; + if (!parse_json_string(value, selection_policy) || selection_policy != "generic") return false; + out_generic_selection_only = true; + } + + // Parse matching soc object: look for "cpus" array inside. + const char *obj = p; + if (!find_json_key_before(obj, obj_end, "cpus", obj)) return false; + if (*obj != '[') return false; + ++obj; + std::memset(&out, 0, sizeof(out)); + uint32_t n = 0; + while (true) { + skip_json_ws(obj); + if (*obj == ']') { + ++obj; + out.total_nums = n; + return n > 0 && n <= kCpuTopoMaxLogical; + } + if (n >= kCpuTopoMaxLogical) return false; + if (!parse_one_cpu_object(obj, out.cpus[n])) return false; + ++n; + skip_json_ws(obj); + if (*obj == ',') { + ++obj; + continue; + } + if (*obj == ']') { + ++obj; + out.total_nums = n; + return n > 0; + } + return false; + } + } +} + +bool read_cpu_topo_json_text(std::string &out_text) { + Dl_info info{}; + if (dladdr(reinterpret_cast(&read_cpu_topo_json_text), &info) == 0 || info.dli_fname == nullptr) { + return false; + } + + std::filesystem::path root = std::filesystem::absolute(info.dli_fname).parent_path(); + { + std::ifstream input(root / kCpuTopoFallbackFileName); + if (input) { + out_text.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + return input.good() || input.eof(); + } + } + while (!root.empty()) { + const std::filesystem::path path = root / kCpuTopoFallbackRelativePath; + std::ifstream input(path); + if (input) { + out_text.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + return input.good() || input.eof(); + } + const std::filesystem::path parent = root.parent_path(); + if (parent == root) break; + root = parent; + } + return false; +} + +bool fill_dsmi_topo_from_json( + const char *soc_name, uint64_t occupy, DsmiCpuTopo &out, bool &out_generic_selection_only +) { + std::string text; + if (!read_cpu_topo_json_text(text)) return false; + return parse_cpu_topo_json_for_soc(text.c_str(), soc_name, occupy, out, out_generic_selection_only); +} + +void dsmi_topo_to_logical(const DsmiCpuTopo &topo, std::vector &out) { + out.clear(); + out.reserve(topo.total_nums); + for (uint32_t i = 0; i < topo.total_nums; ++i) { + const DsmiSingleCpu &cpu = topo.cpus[i]; + AicpuLogicalCpu entry{}; + entry.cpu_id = static_cast(cpu.cpu_id); + entry.phy_cpu_id = static_cast(cpu.phy_cpu_id); + entry.hyperthread_id = static_cast(cpu.hyperthread_id); + entry.cluster_id = entry.phy_cpu_id / 2; + entry.die_id = entry.phy_cpu_id / 4; + out.push_back(entry); + } +} + +} // namespace + +bool load_cpu_topo_from_json( + const char *soc_name, uint64_t occupy, std::vector &out_all_cpus, bool *out_generic_selection_only +) { + out_all_cpus.clear(); + DsmiCpuTopo topo{}; + bool generic_selection_only = false; + if (!fill_dsmi_topo_from_json(soc_name, occupy, topo, generic_selection_only)) return false; + if (out_generic_selection_only != nullptr) *out_generic_selection_only = generic_selection_only; + for (uint32_t i = 0; i < topo.total_nums; ++i) { + if (topo.cpus[i].cpu_id >= kCpuTopoMaxLogical) { + out_all_cpus.clear(); + return false; + } + } + dsmi_topo_to_logical(topo, out_all_cpus); + std::sort(out_all_cpus.begin(), out_all_cpus.end(), [](const AicpuLogicalCpu &a, const AicpuLogicalCpu &b) { + return a.cpu_id < b.cpu_id; + }); + return !out_all_cpus.empty(); +} + } // namespace pto::a5 diff --git a/src/a5/platform/onboard/host/aicpu_topology_probe.h b/src/a5/platform/onboard/host/aicpu_topology_probe.h index f8bbbf799a..9b941da40c 100644 --- a/src/a5/platform/onboard/host/aicpu_topology_probe.h +++ b/src/a5/platform/onboard/host/aicpu_topology_probe.h @@ -13,14 +13,13 @@ #define SRC_A5_PLATFORM_ONBOARD_HOST_AICPU_TOPOLOGY_PROBE_H_ #include +#include #include namespace pto::a5 { -// Per-cpu_id metadata used by the packing algorithm. Filled from the driver -// CPU_TOPO data when available. The verified x86 standard-card fallback -// derives it from the AICPU OCCUPY bitmap. cluster/die ids follow the a5 -// mapping (cluster = phy/2, die = phy/4). +// Per-cpu_id metadata used by the packing algorithm. Filled from CPU_TOPO +// data when available. Topology fields are -1 in the OCCUPY-only fallback. struct AicpuLogicalCpu { int32_t cpu_id; int32_t phy_cpu_id; @@ -29,6 +28,53 @@ struct AicpuLogicalCpu { int32_t die_id; // phy_cpu_id / 4 }; +enum class AicpuScenarioType { + kNotApplicable, + kFg, + kPg1, + kPg2, + kUnknown, +}; + +enum class AicpuTopologySource { + kDriver, + kJsonFallback, + kOccupyFallback, +}; + +struct AicpuDeviceOccupancy { + uint64_t occupy{0}; + uint64_t pf_occupy{0}; + uint64_t os_sched{0}; + bool occupy_valid{false}; + bool pf_occupy_valid{false}; + bool os_sched_valid{false}; +}; + +struct AicpuTopology { + std::string soc_name; + AicpuTopologySource source{AicpuTopologySource::kDriver}; + AicpuScenarioType scenario_type{AicpuScenarioType::kUnknown}; + bool scheduler_smt_enabled{false}; + uint32_t logical_cpu_count{0}; + std::vector surviving_cluster_ids; + std::vector os_schedulable_cpus; + AicpuDeviceOccupancy device_occupancy; + bool generic_selection_only{false}; +}; + +// Complete host decision for one AICPU launch. The affinity convention is +// [scheduler..., orchestrator], so allowed_cpus.back() always carries O. +struct AicpuLaunchPlan { + int32_t requested_active_count{0}; // 0 means automatic + int32_t effective_active_count{0}; + int32_t stable_reachable_count{0}; + int32_t launch_count{0}; + std::vector allowed_cpus; + bool warn_stable_reachable_below_default{false}; + bool warn_cpu_topology_unavailable{false}; +}; + // Probe device-side AICPU topology. Returns true iff the user pool was // successfully resolved (at least one entry in `out_user_cpus`). The output // only contains cpu_ids that are in the device-side OCCUPY bitmap (i.e. @@ -40,19 +86,38 @@ struct AicpuLogicalCpu { // * dsmi_get_device_info(SOC_INFO, CPU_TOPO) (fallback) // // CPU_TOPO-less x86 Ascend950PR_9579 standard cards with OCCUPY=0x3e use -// the verified OCCUPY-only topology below. Other signatures remain -// unsupported. +// the original verified fallback. Other signatures remain unsupported by this +// legacy API; the A5 runtime overload below owns the packaged JSON and generic +// OCCUPY-only fallback chain. // 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 topology metadata only for the verified x86 standard-card signature: -// Ascend950PR_9579 with OCCUPY=0x3e. Every set bit is a distinct non-SMT -// physical CPU and retains the a5 two-physical-CPUs-per-cluster, -// two-clusters-per-die layout. Returns false on all other hosts, SoCs, or -// masks. +// Preserve the original verified x86 standard-card fallback contract. bool derive_topology_from_occupy(const char *soc_name, uint64_t occupy, std::vector &out_user_cpus); +// Merge host CPU_TOPO metadata with the authoritative device-side scheduler +// pool and classify the resulting A5 topology. Returns false only when the +// probe itself is unusable; an unrecognised but internally valid shape is +// returned successfully with scenario_type == kUnknown. +bool probe_aicpu_topology( + uint32_t device_id, const AicpuDeviceOccupancy &device_occupancy, AicpuTopology &out_topology +); + +// Enumerate OCCUPY set bits without inferring any topology relationships. +// Returns false when the mask is empty. +bool enumerate_cpus_from_occupy(uint64_t occupy, std::vector &out_user_cpus); + +// Load the full logical CPU_TOPO for a packaged fallback whose SoC and every +// constraint declared by that entry (host architecture and/or OCCUPY) match. +// Output is not OCCUPY-filtered. `out_generic_selection_only` reports entries +// that must keep using compute_allowed_cpus() instead of scenario selection. +// Returns false when the signature is absent, mismatched, or unusable. +bool load_cpu_topo_from_json( + const char *soc_name, uint64_t occupy, std::vector &out_all_cpus, + bool *out_generic_selection_only = nullptr +); + // Compute the `ALLOWED_CPUS` selection for the surviving threads. // // Inputs: @@ -87,6 +152,41 @@ bool compute_allowed_cpus( std::vector &out_allowed_cpus ); +// Classify an internally valid A5 topology from raw logical topology and the +// driver-filtered scheduler pool. FG / PG1 / PG2 come from the surviving +// cluster/die layout. Scheduler SMT availability is recorded separately in +// AicpuTopology::scheduler_smt_enabled and does not define another scenario. +// Logical CPU count is not used as a scenario gate. +AicpuScenarioType classify_aicpu_scenario( + uint32_t logical_cpu_count, const std::vector &all_logical_cpus, + const std::vector &os_schedulable_cpus +); + +// Compute the documented topology policy for a known A5 scenario with an +// active count in [2, 5]. Output order is [S0, ..., S(active_count-2), O]. +// The output is empty on failure. +bool compute_scenario_allowed_cpus( + const AicpuTopology &topology, int32_t active_count, std::vector &out_allowed_cpus +); + +// Unknown-topology fallback: select exactly active_count CPUs in [2, 5]. Use +// topology order when metadata is valid; OCCUPY-only metadata naturally +// degenerates to cpu_id order. The last selected CPU is O and all preceding +// CPUs are S. The output is empty when capacity is insufficient. +bool compute_unknown_allowed_cpus( + const AicpuTopology &topology, int32_t active_count, std::vector &out_allowed_cpus +); + +// Resolve automatic/manual active count, affinity and physical launch count. +// Automatic mode shrinks when fewer than PLATFORM_DEFAULT_AICPU_THREAD_NUM CPUs are +// stably reachable. Manual mode is exact. Launch coverage is never clamped. +bool build_aicpu_launch_plan( + const AicpuTopology &topology, int32_t requested_active_count, AicpuLaunchPlan &out_plan, std::string &out_error +); + +const char *aicpu_scenario_name(AicpuScenarioType scenario); +const char *aicpu_topology_source_name(AicpuTopologySource source); + } // namespace pto::a5 #endif // SRC_A5_PLATFORM_ONBOARD_HOST_AICPU_TOPOLOGY_PROBE_H_ diff --git a/src/a5/platform/onboard/host/device_runner.cpp b/src/a5/platform/onboard/host/device_runner.cpp index b5027a6127..9e8a5ec611 100644 --- a/src/a5/platform/onboard/host/device_runner.cpp +++ b/src/a5/platform/onboard/host/device_runner.cpp @@ -43,6 +43,10 @@ #include "host/host_regs.h" // Register address retrieval #include "host/raii_scope_guard.h" +namespace { +constexpr const char *kAicpuTopologyQueryName = "simpler_aicpu_query_topology"; +} + // dep_gen_replay_emit_deps_json: strong symbol provided by // runtime/tensormap_and_ringbuffer/host/dep_gen_replay.cpp when that runtime is // linked into host_runtime.so. host_build_graph has no replay implementation @@ -137,6 +141,90 @@ int DeviceRunner::destroy_comm_stream(void *stream) { return 0; } +int DeviceRunner::query_aicpu_device_occupancy(pto::a5::AicpuDeviceOccupancy &out) { + if (aicpu_device_occupancy_cached_) { + out = aicpu_device_occupancy_; + return 0; + } + + void *device_result = mem_alloc_.alloc(sizeof(AicpuTopologyQueryResult)); + if (device_result == nullptr) { + LOG_ERROR("AICPU topology query result allocation failed"); + return -1; + } + auto result_cleanup = RAIIScopeGuard([&]() { + mem_alloc_.free(device_result); + }); + AicpuTopologyQueryResult zero{}; + int rc = rtMemcpy(device_result, sizeof(zero), &zero, sizeof(zero), RT_MEMCPY_HOST_TO_DEVICE); + if (rc != 0) { + LOG_ERROR("AICPU topology query result initialization failed: %d", rc); + return rc; + } + AicpuTopologyQueryArgs args{}; + args.result_addr = reinterpret_cast(device_result); + rc = launch_aicpu_payload(stream_aicpu_, &args, sizeof(args), kAicpuTopologyQueryName, /*aicpu_num=*/1); + if (rc != 0) { + LOG_ERROR("AICPU device occupancy query launch failed: %d", rc); + recover_device_or_mark_unusable(rc); + return rc; + } + rc = aclrtSynchronizeStreamWithTimeout(stream_aicpu_, PLATFORM_STREAM_SYNC_TIMEOUT_MS); + if (rc != 0) { + LOG_ERROR("AICPU device occupancy query sync failed: %d", rc); + recover_device_or_mark_unusable(rc); + return rc; + } + AicpuTopologyQueryResult result{}; + rc = rtMemcpy(&result, sizeof(result), device_result, sizeof(result), RT_MEMCPY_DEVICE_TO_HOST); + if (rc != 0) { + LOG_ERROR("AICPU device occupancy query copy failed: %d", rc); + return rc; + } + if (result.occupy_rc != 0 || result.occupy == 0) { + LOG_ERROR( + "device-side AICPU OCCUPY query failed: rc=%d mask=0x%llx", result.occupy_rc, + static_cast(result.occupy) + ); + return -1; + } + aicpu_device_occupancy_.occupy = result.occupy; + aicpu_device_occupancy_.pf_occupy = result.pf_occupy; + aicpu_device_occupancy_.os_sched = result.os_sched; + aicpu_device_occupancy_.occupy_valid = result.occupy_rc == 0; + aicpu_device_occupancy_.pf_occupy_valid = result.pf_occupy_rc == 0; + aicpu_device_occupancy_.os_sched_valid = result.os_sched_rc == 0; + aicpu_device_occupancy_cached_ = true; + out = aicpu_device_occupancy_; + return 0; +} + +int DeviceRunner::query_aicpu_topology(pto::a5::AicpuTopology &out) { + if (aicpu_topology_cached_) { + out = aicpu_topology_; + return 0; + } + + pto::a5::AicpuDeviceOccupancy occupancy; + int rc = query_aicpu_device_occupancy(occupancy); + if (rc != 0) return rc; + + pto::a5::AicpuTopology topology; + if (!pto::a5::probe_aicpu_topology(static_cast(device_id_), occupancy, topology)) return -1; + + aicpu_topology_ = std::move(topology); + aicpu_topology_cached_ = true; + out = aicpu_topology_; + return 0; +} + +void DeviceRunner::clear_aicpu_topology_cache() { + aicpu_device_occupancy_cached_ = false; + aicpu_device_occupancy_ = {}; + aicpu_topology_cached_ = false; + aicpu_topology_ = {}; +} + int DeviceRunner::enqueue_run(Runtime &runtime, const CallConfig &config, uint32_t pipeline_slot) { if (run_resources_owned_) { LOG_ERROR( @@ -159,7 +247,8 @@ int DeviceRunner::enqueue_run(Runtime &runtime, const CallConfig &config, uint32 // activate_launch_shape() latches this run's geometry onto the runner on the // executor thread immediately before enqueue, so block_dim_ is this run's. const int block_dim = block_dim_; - int launch_aicpu_num = config.aicpu_thread_num; + int requested_aicpu_num = config.aicpu_thread_num; + const bool automatic_aicpu_num = requested_aicpu_num == 0; // A prior AICore launch/sync error poisoned the device context and the // in-place drain could not clear it. Refuse to run rather than cascade // into halResMap rc=62 (init_aicore_register_addresses) or rtMalloc @@ -177,9 +266,9 @@ int DeviceRunner::enqueue_run(Runtime &runtime, const CallConfig &config, uint32 ); return -1; } - if (validate_launch_aicpu_num(launch_aicpu_num) != 0) return -1; - if (launch_aicpu_num == 0) launch_aicpu_num = PLATFORM_DEFAULT_AICPU_THREAD_NUM; - runtime.set_aicpu_thread_num(launch_aicpu_num); + if (validate_launch_aicpu_num(requested_aicpu_num) != 0) return -1; + int active_aicpu_num = automatic_aicpu_num ? PLATFORM_DEFAULT_AICPU_THREAD_NUM : requested_aicpu_num; + runtime.set_aicpu_thread_num(active_aicpu_num); int rc = ensure_device_initialized(); if (rc != 0) { @@ -215,53 +304,76 @@ int DeviceRunner::enqueue_run(Runtime &runtime, const CallConfig &config, uint32 // a5-specific: probe the AICPU topology + compute ALLOWED_CPUS for the // filter-style gate (see src/common/platform/onboard/aicpu/ // platform_aicpu_affinity.cpp::platform_aicpu_affinity_gate_filter). - // Convention: indices 0..n_sched-1 = sched slots, last = orch slot. - // n_sched = launch_aicpu_num - 1 (one orch + the rest sched). + // Convention: indices 0..active-2 are scheduler slots and the last slot + // is the orchestrator. In auto mode only, unknown shapes may reduce the + // active count to the available pool, but execution keeps at least one of + // each role. { - std::vector user_cpus; - std::vector allowed; - const int32_t n_orch = 1; - const int32_t n_sched = (launch_aicpu_num > 1) ? (launch_aicpu_num - n_orch) : 0; + pto::a5::AicpuTopology topology; runtime.set_aicpu_allowed_cpu_count(0); - if (n_sched > 0) { - if (!pto::a5::probe_aicpu_topology(static_cast(device_id_), user_cpus)) { - LOG_ERROR("AICPU topology probe failed; affinity gate will drop all threads"); - return -1; - } - if (!pto::a5::compute_allowed_cpus(user_cpus, n_sched, n_orch, allowed)) { - LOG_ERROR( - "AICPU topology has %zu user cpus, cannot fit %d sched + %d orch", user_cpus.size(), n_sched, n_orch - ); - return -1; - } + if (query_aicpu_topology(topology) != 0) { + LOG_ERROR("AICPU topology probe failed; affinity gate will not launch"); + return -1; + } + pto::a5::AicpuLaunchPlan launch_plan; + std::string plan_error; + if (!pto::a5::build_aicpu_launch_plan(topology, requested_aicpu_num, launch_plan, plan_error)) { + LOG_ERROR( + "cannot build AICPU launch plan: soc=%s scenario=%s occupy=0x%llx reason=%s", + topology.soc_name.empty() ? "(unknown)" : topology.soc_name.c_str(), + pto::a5::aicpu_scenario_name(topology.scenario_type), + static_cast(topology.device_occupancy.occupy), plan_error.c_str() + ); + return -1; + } + const auto &allowed = launch_plan.allowed_cpus; + active_aicpu_num = launch_plan.effective_active_count; + runtime.set_aicpu_thread_num(active_aicpu_num); + { const size_t cap = runtime.aicpu_allowed_cpus_capacity(); if (allowed.size() > cap) { - LOG_ERROR("compute_allowed_cpus returned %zu > cap %zu", allowed.size(), cap); + LOG_ERROR("AICPU selection returned %zu > cap %zu", allowed.size(), cap); return -1; } int32_t *allowed_cpus = runtime.get_aicpu_allowed_cpus(); for (size_t i = 0; i < allowed.size(); ++i) allowed_cpus[i] = allowed[i]; runtime.set_aicpu_allowed_cpu_count(static_cast(allowed.size())); - // Launch one AICPU thread per OCCUPY-visible user cpu so CANN - // spreads exactly across the user pool — over-subscription on a - // SKU with fewer user cpus than the compile-time bound deadlocks - // the production AICPU kernel. Capped by the compile-time array - // sizing in case the SKU exceeds expectation. - int32_t launch_n = static_cast(user_cpus.size()); - if (launch_n > PLATFORM_MAX_AICPU_THREADS_JUST_FOR_LAUNCH) { - launch_n = PLATFORM_MAX_AICPU_THREADS_JUST_FOR_LAUNCH; - } - runtime.set_aicpu_launch_count(launch_n); + runtime.set_aicpu_launch_count(launch_plan.launch_count); std::string dump; for (size_t i = 0; i < allowed.size(); ++i) { if (i) dump += ", "; dump += std::to_string(allowed[i]); if (i + 1 == allowed.size()) dump += "(orch)"; } + if (launch_plan.warn_cpu_topology_unavailable) { + LOG_WARN( + "AICPU CPU_TOPO unavailable; using %s: soc=%s occupy=0x%llx " + "stable_reachable=%d requested=%d effective=%d affinity=[%s]%s", + pto::a5::aicpu_topology_source_name(topology.source), + topology.soc_name.empty() ? "(unknown)" : topology.soc_name.c_str(), + static_cast(topology.device_occupancy.occupy), + launch_plan.stable_reachable_count, requested_aicpu_num, active_aicpu_num, dump.c_str(), + topology.source == pto::a5::AicpuTopologySource::kOccupyFallback ? + "; physical/SMT/cluster/die placement is unknown" : + "" + ); + } + if (launch_plan.warn_stable_reachable_below_default) { + LOG_WARN( + "AICPU stable reachable CPUs below active capacity: soc=%s scenario=%s occupy=0x%llx " + "stable_reachable=%d capacity=%d requested=%d effective=%d affinity=[%s]", + topology.soc_name.empty() ? "(unknown)" : topology.soc_name.c_str(), + pto::a5::aicpu_scenario_name(topology.scenario_type), + static_cast(topology.device_occupancy.occupy), + launch_plan.stable_reachable_count, PLATFORM_DEFAULT_AICPU_THREAD_NUM, requested_aicpu_num, + active_aicpu_num, dump.c_str() + ); + } LOG_INFO( - "AICPU ALLOWED_CPUS = [%s] (n_sched=%d, n_orch=%d, launch=%d, user_cpus=%zu)", dump.c_str(), n_sched, - n_orch, launch_n, user_cpus.size() + "AICPU ALLOWED_CPUS = [%s] (scenario=%s active=%d launch=%d user_cpus=%zu)", dump.c_str(), + pto::a5::aicpu_scenario_name(topology.scenario_type), active_aicpu_num, launch_plan.launch_count, + topology.os_schedulable_cpus.size() ); } } @@ -284,7 +396,7 @@ int DeviceRunner::enqueue_run(Runtime &runtime, const CallConfig &config, uint32 } if (enable_pmu_) { - rc = init_pmu(num_aicore, launch_aicpu_num, make_pmu_csv_path(output_prefix_), pmu_event_type_, device_id_); + rc = init_pmu(num_aicore, active_aicpu_num, make_pmu_csv_path(output_prefix_), pmu_event_type_, device_id_); if (rc != 0) { LOG_ERROR("PMU init failed: %d, disabling PMU for this run", rc); kernel_args_.args.pmu_data_base = 0; @@ -293,7 +405,7 @@ int DeviceRunner::enqueue_run(Runtime &runtime, const CallConfig &config, uint32 } if (enable_dep_gen_) { - rc = init_dep_gen(launch_aicpu_num, device_id_); + rc = init_dep_gen(active_aicpu_num, device_id_); if (rc != 0) { LOG_ERROR("init_dep_gen failed: %d", rc); return rc; @@ -301,7 +413,7 @@ int DeviceRunner::enqueue_run(Runtime &runtime, const CallConfig &config, uint32 } if (enable_scope_stats_) { - rc = init_scope_stats(launch_aicpu_num, device_id_); + rc = init_scope_stats(active_aicpu_num, device_id_); if (rc != 0) { LOG_ERROR("init_scope_stats failed: %d", rc); return rc; @@ -389,9 +501,9 @@ int DeviceRunner::enqueue_run(Runtime &runtime, const CallConfig &config, uint32 // launch_count = popcount(OCCUPY) from the topology probe — one thread // per user-schedulable cpu_id. The filter gate barriers exactly this // many threads (runtime.aicpu_launch_count is read on the device side - // by kernel.cpp). The fallback is defensive; normal runs always publish - // the topology-derived launch count above. - int aicpu_launch_n = (runtime.get_aicpu_launch_count() > 0) ? runtime.get_aicpu_launch_count() : launch_aicpu_num; + // by kernel.cpp). The fallback expression is defensive; normal runs + // always populate the topology-derived launch count above. + int aicpu_launch_n = (runtime.get_aicpu_launch_count() > 0) ? runtime.get_aicpu_launch_count() : active_aicpu_num; rc = launch_aicpu_kernel(stream_aicpu_, &kernel_args_.args, host::KernelNames::RunName, aicpu_launch_n); if (rc != 0) { LOG_ERROR("launch_aicpu_kernel (main) failed: %d", rc); @@ -521,6 +633,7 @@ void DeviceRunner::recover_device_or_mark_unusable(int aicore_rc) { aicore_rc ); } + clear_aicpu_topology_cache(); device_unusable_.store(true, std::memory_order_release); } @@ -592,6 +705,7 @@ class DeviceBindGuard { } // namespace int DeviceRunner::force_reset_device() { + clear_aicpu_topology_cache(); if (device_id_ < 0) { return -1; } @@ -766,6 +880,7 @@ int DeviceRunner::finalize() { } } + clear_aicpu_topology_cache(); device_id_ = -1; // Clear the poison flag only if the force reset actually recovered the card, // so a still-poisoned card stays flagged: a reused DeviceRunner then fails diff --git a/src/a5/platform/onboard/host/device_runner.h b/src/a5/platform/onboard/host/device_runner.h index 4a55780908..913c7b0146 100644 --- a/src/a5/platform/onboard/host/device_runner.h +++ b/src/a5/platform/onboard/host/device_runner.h @@ -55,6 +55,7 @@ #include "host/args_dump_collector.h" #include "aicpu_loader/host/load_aicpu_op.h" #include "runtime.h" +#include "aicpu_topology_probe.h" // KernelArgsHelper is defined in // src/common/platform/onboard/host/device_runner_helpers.h (included above). @@ -269,6 +270,18 @@ class DeviceRunner : public DeviceRunnerBase { // dep_gen enablement is a5-specific (a2a3 carries its own copy). bool enable_dep_gen_{false}; + int query_aicpu_device_occupancy(pto::a5::AicpuDeviceOccupancy &out); + int query_aicpu_topology(pto::a5::AicpuTopology &out); + void clear_aicpu_topology_cache(); + // Device-side occupancy and the merged Host topology are immutable during + // one DeviceRunner attach/reset lifetime. Cache successful probes only; + // allowed CPU selection still runs per call because the requested active + // count may change. Recovery, reset, and finalize clear both values. + bool aicpu_device_occupancy_cached_{false}; + pto::a5::AicpuDeviceOccupancy aicpu_device_occupancy_{}; + bool aicpu_topology_cached_{false}; + pto::a5::AicpuTopology aicpu_topology_{}; + int init_pmu(int num_cores, int num_threads, const std::string &csv_path, PmuEventType event_type, int device_id); int init_scope_stats(int num_threads, int device_id); diff --git a/src/a5/runtime/host_build_graph/host/runtime_maker.cpp b/src/a5/runtime/host_build_graph/host/runtime_maker.cpp index 4486fbaef2..dab4380409 100644 --- a/src/a5/runtime/host_build_graph/host/runtime_maker.cpp +++ b/src/a5/runtime/host_build_graph/host/runtime_maker.cpp @@ -1026,13 +1026,12 @@ extern "C" int validate_runtime_impl(Runtime *runtime, const HostApi *api, int e return rc; } -// host_build_graph resolves orchestration on the host, so it exports no AICPU -// entries beyond the base {simpler_aicpu_exec, simpler_aicpu_init} — in -// particular it does not export simpler_aicpu_register_callable. Reporting an -// empty extra-symbol set keeps the common AICPU loader from looking for it. +// host_build_graph resolves orchestration on the host, but the A5 platform +// exports the topology query entry used before the first normal launch. extern "C" const char *const *runtime_extra_aicpu_symbols(size_t *count) { + static const char *const kExtra[] = {"simpler_aicpu_query_topology"}; if (count != nullptr) { - *count = 0; + *count = sizeof(kExtra) / sizeof(kExtra[0]); } - return nullptr; + return kExtra; } diff --git a/src/a5/runtime/tensormap_and_ringbuffer/docs/RUNTIME_LOGIC.md b/src/a5/runtime/tensormap_and_ringbuffer/docs/RUNTIME_LOGIC.md index 59e08e740f..441cf86164 100644 --- a/src/a5/runtime/tensormap_and_ringbuffer/docs/RUNTIME_LOGIC.md +++ b/src/a5/runtime/tensormap_and_ringbuffer/docs/RUNTIME_LOGIC.md @@ -87,7 +87,7 @@ Two platform implementations exist under `src/platform/`, sharing a common inter | Constant | Value | Description | | -------- | ----- | ----------- | | `PLATFORM_MAX_BLOCKDIM` | 36 | Maximum blocks (each = 1 AIC + 2 AIV) | -| `PLATFORM_MAX_AICPU_THREADS` | 7 | Maximum total AICPU threads (up to 6 schedulers + 1 orchestrator) | +| `PLATFORM_MAX_AICPU_THREADS` | 5 | Maximum active AICPU threads (up to 4 schedulers + 1 orchestrator) | | `PLATFORM_MAX_AIC_PER_THREAD` | 36 | Max AIC cores per scheduler thread | | `PLATFORM_MAX_AIV_PER_THREAD` | 72 | Max AIV cores per scheduler thread | | `PLATFORM_PROF_SYS_CNT_FREQ` | 1000 MHz | System counter frequency for profiling | diff --git a/src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp b/src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp index cded6dc6fe..79137c315c 100644 --- a/src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp +++ b/src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp @@ -1062,7 +1062,7 @@ extern "C" int validate_runtime_impl(Runtime *runtime, const HostApi *api, int e // device, so it exports simpler_aicpu_register_callable; the common AICPU loader // queries this so it carries no runtime-specific symbol knowledge. extern "C" const char *const *runtime_extra_aicpu_symbols(size_t *count) { - static const char *const kExtra[] = {"simpler_aicpu_register_callable"}; + static const char *const kExtra[] = {"simpler_aicpu_register_callable", "simpler_aicpu_query_topology"}; if (count != nullptr) { *count = sizeof(kExtra) / sizeof(kExtra[0]); } diff --git a/tests/ut/cpp/CMakeLists.txt b/tests/ut/cpp/CMakeLists.txt index 64d2bbbc5e..d8b8e8c40b 100644 --- a/tests/ut/cpp/CMakeLists.txt +++ b/tests/ut/cpp/CMakeLists.txt @@ -900,16 +900,22 @@ 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 is -# restricted to the verified x86 Ascend950PR_9579 standard-card signature. +# a5 CPU_TOPO fallback and affinity selection. Pure logic; packaged fallback +# entries enforce their own verified SoC, host-architecture, and OCCUPY constraints. 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 ) +add_custom_command(TARGET test_a5_aicpu_topology_fallback POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${A5_ONBOARD_HOST_DIR}/aicpu_cpu_topo_fallback.json" + "$/aicpu_cpu_topo_fallback.json" +) target_include_directories(test_a5_aicpu_topology_fallback PRIVATE ${GTEST_INCLUDE_DIRS} ${A5_ONBOARD_HOST_DIR} + ${CMAKE_SOURCE_DIR}/../../../src/a5/platform/include ${SIMPLER_LOG_DIR}/include ${SIMPLER_COMMON_PLATFORM_DIR}/include ) diff --git a/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp b/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp index 367620793f..bfd632765a 100644 --- a/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp +++ b/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp @@ -11,10 +11,13 @@ #include +#include #include +#include #include #include "aicpu_topology_probe.h" +#include "common/platform_config.h" extern "C" { void unified_log_error(const char *, const char *, ...) {} @@ -25,24 +28,152 @@ void unified_log_debug(const char *, const char *, ...) {} namespace { +using pto::a5::AicpuLaunchPlan; using pto::a5::AicpuLogicalCpu; +using pto::a5::AicpuScenarioType; +using pto::a5::AicpuTopology; +using pto::a5::AicpuTopologySource; +using pto::a5::build_aicpu_launch_plan; +using pto::a5::classify_aicpu_scenario; using pto::a5::compute_allowed_cpus; +using pto::a5::compute_scenario_allowed_cpus; +using pto::a5::compute_unknown_allowed_cpus; using pto::a5::derive_topology_from_occupy; +using pto::a5::enumerate_cpus_from_occupy; +using pto::a5::load_cpu_topo_from_json; -#if defined(__aarch64__) -TEST(A5AicpuTopologyFallback, RejectsNonX86Host) { +std::vector make_physical_range(int32_t first_phy, int32_t last_phy) { std::vector cpus; + for (int32_t phy = first_phy; phy <= last_phy; ++phy) { + cpus.push_back({2 * phy, phy, 0, phy / 2, phy / 4}); + cpus.push_back({2 * phy + 1, phy, 1, phy / 2, phy / 4}); + } + return cpus; +} - EXPECT_FALSE(derive_topology_from_occupy("Ascend950PR_9579", 0x3e, cpus)); - EXPECT_TRUE(cpus.empty()); +std::vector primaries(const std::vector &cpus) { + std::vector result; + for (const auto &cpu : cpus) + if (cpu.hyperthread_id == 0) result.push_back(cpu); + return result; } -#elif defined(__x86_64__) + TEST(A5AicpuTopologyFallback, EnumeratesEveryOccupiedCpu) { std::vector cpus; + ASSERT_TRUE(enumerate_cpus_from_occupy((1ULL << 1) | (1ULL << 3) | (1ULL << 5), cpus)); + ASSERT_EQ(cpus.size(), 3U); + + for (int32_t i = 0; i < 3; ++i) { + EXPECT_EQ(cpus[i].cpu_id, 2 * i + 1); + EXPECT_EQ(cpus[i].phy_cpu_id, -1); + EXPECT_EQ(cpus[i].hyperthread_id, -1); + EXPECT_EQ(cpus[i].cluster_id, -1); + EXPECT_EQ(cpus[i].die_id, -1); + } +} + +TEST(A5AicpuTopologyFallback, WorksForAnyNonEmptyOccupyMask) { + std::vector cpus; + ASSERT_TRUE(enumerate_cpus_from_occupy((1ULL << 0) | (1ULL << 17) | (1ULL << 63), cpus)); + ASSERT_EQ(cpus.size(), 3U); + EXPECT_EQ(cpus[0].cpu_id, 0); + EXPECT_EQ(cpus[1].cpu_id, 17); + EXPECT_EQ(cpus[2].cpu_id, 63); +} + +TEST(A5AicpuTopologyFallback, RejectsEmptyOccupyMask) { + std::vector cpus = {{1, 1, 0, 0, 0}}; + + EXPECT_FALSE(enumerate_cpus_from_occupy(0, cpus)); + EXPECT_TRUE(cpus.empty()); +} + +TEST(A5AicpuTopologyFallback, SelectsFiveCpuIdsWithoutTopologyRequirements) { + AicpuTopology topology; + ASSERT_TRUE(enumerate_cpus_from_occupy(0x3fU, topology.os_schedulable_cpus)); + + std::reverse(topology.os_schedulable_cpus.begin(), topology.os_schedulable_cpus.end()); + std::vector allowed; + ASSERT_TRUE(compute_unknown_allowed_cpus(topology, 5, allowed)); + EXPECT_EQ(allowed, (std::vector{0, 1, 2, 3, 4})); +} + +#if defined(__x86_64__) +TEST(A5AicpuTopologyFallback, PreservesLegacyVerified9579Fallback) { + std::vector cpus; ASSERT_TRUE(derive_topology_from_occupy("Ascend950PR_9579", 0x3e, cpus)); ASSERT_EQ(cpus.size(), 5U); + EXPECT_EQ(cpus.front().cpu_id, 1); + EXPECT_EQ(cpus.back().cpu_id, 5); +} +#else +TEST(A5AicpuTopologyFallback, LegacyVerified9579FallbackIsX86Only) { + std::vector cpus; + EXPECT_FALSE(derive_topology_from_occupy("Ascend950PR_9579", 0x3e, cpus)); +} +#endif + +TEST(A5AicpuTopologyFallback, LegacyFallbackRejectsUnverifiedSignature) { + std::vector cpus = {{1, 1, 0, 0, 0}}; + EXPECT_FALSE(derive_topology_from_occupy("Ascend950PR_9599", 0x3e, cpus)); + EXPECT_TRUE(cpus.empty()); +} + +TEST(A5AicpuTopologyJsonFallback, Loads9599LayoutFromPackagedJson) { + std::vector cpus; + ASSERT_TRUE(load_cpu_topo_from_json("Ascend950PR_9599", 0x1f8, cpus)); + ASSERT_EQ(cpus.size(), 9U); + EXPECT_EQ(cpus[0].cpu_id, 0); + EXPECT_EQ(cpus[0].phy_cpu_id, 0); + EXPECT_EQ(cpus[0].hyperthread_id, 0); + + EXPECT_EQ(cpus[1].cpu_id, 1); + EXPECT_EQ(cpus[1].phy_cpu_id, 1); + EXPECT_EQ(cpus[1].hyperthread_id, 0); + EXPECT_EQ(cpus[2].cpu_id, 2); + EXPECT_EQ(cpus[2].phy_cpu_id, 1); + EXPECT_EQ(cpus[2].hyperthread_id, 1); + + for (int32_t i = 3; i <= 8; ++i) { + EXPECT_EQ(cpus[i].cpu_id, i); + EXPECT_EQ(cpus[i].phy_cpu_id, i - 1); + EXPECT_EQ(cpus[i].hyperthread_id, 0); + EXPECT_EQ(cpus[i].cluster_id, cpus[i].phy_cpu_id / 2); + EXPECT_EQ(cpus[i].die_id, cpus[i].phy_cpu_id / 4); + } + + // phy 1 is the only SMT pair; remaining phys are unique. + int phy1_count = 0; + std::vector other_phys; + for (const auto &cpu : cpus) { + if (cpu.phy_cpu_id == 1) { + ++phy1_count; + } else { + other_phys.push_back(cpu.phy_cpu_id); + } + } + EXPECT_EQ(phy1_count, 2); + std::sort(other_phys.begin(), other_phys.end()); + other_phys.erase(std::unique(other_phys.begin(), other_phys.end()), other_phys.end()); + EXPECT_EQ(other_phys.size(), 7U); +} + +TEST(A5AicpuTopologyJsonFallback, RejectsUnexpected9599OccupyMask) { + std::vector cpus = {{1, 1, 0, 0, 0}}; + + EXPECT_FALSE(load_cpu_topo_from_json("Ascend950PR_9599", 0x1f0, cpus)); + EXPECT_TRUE(cpus.empty()); +} + +#if defined(__x86_64__) +TEST(A5AicpuTopologyJsonFallback, PreservesVerifiedFallbackSignatureAndSelection) { + std::vector cpus; + bool generic_selection_only = false; + ASSERT_TRUE(load_cpu_topo_from_json("Ascend950PR_9579", 0x3e, cpus, &generic_selection_only)); + ASSERT_TRUE(generic_selection_only); + 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); @@ -50,44 +181,327 @@ TEST(A5AicpuTopologyFallback, EnumeratesEveryOccupiedCpu) { 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("Ascend950PR_9579", 0x3e, cpus)); std::vector allowed; - ASSERT_TRUE(compute_allowed_cpus(cpus, /*n_sched=*/2, /*n_orch=*/1, allowed)); + ASSERT_TRUE(compute_allowed_cpus(cpus, 2, 1, allowed)); EXPECT_EQ(allowed, (std::vector{4, 5, 1})); } - -TEST(A5AicpuTopologyFallback, RejectsNullSocName) { - std::vector cpus = {{1, 1, 0, 0, 0}}; - - EXPECT_FALSE(derive_topology_from_occupy(nullptr, 0x3e, cpus)); - EXPECT_TRUE(cpus.empty()); -} #else -TEST(A5AicpuTopologyFallback, RejectsUnsupportedHost) { +TEST(A5AicpuTopologyJsonFallback, RejectsVerifiedFallbackOnNonX86Host) { std::vector cpus; - - EXPECT_FALSE(derive_topology_from_occupy("Ascend950PR_9579", 0x3e, cpus)); + EXPECT_FALSE(load_cpu_topo_from_json("Ascend950PR_9579", 0x3e, cpus)); EXPECT_TRUE(cpus.empty()); } #endif -TEST(A5AicpuTopologyFallback, RejectsUnverifiedSoc) { +TEST(A5AicpuTopologyJsonFallback, RejectsUnexpectedVerifiedFallbackOccupyMask) { std::vector cpus = {{1, 1, 0, 0, 0}}; - - EXPECT_FALSE(derive_topology_from_occupy("Ascend950PR_9599", 0x3e, cpus)); + EXPECT_FALSE(load_cpu_topo_from_json("Ascend950PR_9579", 0x1f8, cpus)); EXPECT_TRUE(cpus.empty()); } -TEST(A5AicpuTopologyFallback, RejectsUnexpectedOccupyMask) { +TEST(A5AicpuTopologyJsonFallback, RejectsUnknownAndMissingSoc) { std::vector cpus = {{1, 1, 0, 0, 0}}; - EXPECT_FALSE(derive_topology_from_occupy("Ascend950PR_9579", 0x1f8, cpus)); + EXPECT_FALSE(load_cpu_topo_from_json("Ascend950PR_unknown", 0, cpus)); + EXPECT_TRUE(cpus.empty()); + + cpus = {{1, 1, 0, 0, 0}}; + EXPECT_FALSE(load_cpu_topo_from_json("Ascend950PR_missing", 0, cpus)); EXPECT_TRUE(cpus.empty()); + + EXPECT_FALSE(load_cpu_topo_from_json(nullptr, 0, cpus)); + EXPECT_TRUE(cpus.empty()); +} + +TEST(A5AicpuTopologySelection, ClassifiesKnownScenarios) { + const auto fg_all = make_physical_range(0, 7); + // FG classification is independent of Scheduler SMT availability. + EXPECT_EQ(classify_aicpu_scenario(16, fg_all, primaries(fg_all)), AicpuScenarioType::kFg); + EXPECT_EQ(classify_aicpu_scenario(16, fg_all, fg_all), AicpuScenarioType::kFg); + // Logical count is not a gate: 9-CPU layout with 4 clusters is still FG. + EXPECT_EQ( + classify_aicpu_scenario(static_cast(fg_all.size()), fg_all, primaries(fg_all)), AicpuScenarioType::kFg + ); + + const auto pg1_all = make_physical_range(0, 5); + EXPECT_EQ(classify_aicpu_scenario(12, pg1_all, pg1_all), AicpuScenarioType::kPg1); + + auto pg2_all = make_physical_range(0, 1); + auto right_die = make_physical_range(4, 5); + pg2_all.insert(pg2_all.end(), right_die.begin(), right_die.end()); + EXPECT_EQ(classify_aicpu_scenario(8, pg2_all, pg2_all), AicpuScenarioType::kPg2); + // logical_cpu_count must still match all_logical_cpus.size(); a mismatched + // count is Unknown. Non-16/12/8 layouts are covered by Classifies9599Style… + // and by classifying with the vector's actual size above. + EXPECT_EQ(classify_aicpu_scenario(9, pg2_all, pg2_all), AicpuScenarioType::kUnknown); +} + +TEST(A5AicpuTopologySelection, Classifies9599StyleNineLogicalAsFg) { + std::vector all; + ASSERT_TRUE(load_cpu_topo_from_json("Ascend950PR_9599", 0x1f8, all)); + std::vector sched; + for (const auto &cpu : all) { + if (cpu.cpu_id >= 3 && cpu.cpu_id <= 8) sched.push_back(cpu); + } + EXPECT_EQ(classify_aicpu_scenario(static_cast(all.size()), all, sched), AicpuScenarioType::kFg); +} + +TEST(A5AicpuTopologySelection, ComputesFgPolicy) { + const auto all = make_physical_range(0, 7); + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kFg; + topology.os_schedulable_cpus = primaries(all); + std::reverse(topology.os_schedulable_cpus.begin(), topology.os_schedulable_cpus.end()); + + std::vector allowed; + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 2, allowed)); + EXPECT_EQ(allowed, (std::vector{12, 14})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 3, allowed)); + EXPECT_EQ(allowed, (std::vector{12, 8, 14})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 4, allowed)); + EXPECT_EQ(allowed, (std::vector{12, 8, 10, 14})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 5, allowed)); + EXPECT_EQ(allowed, (std::vector{12, 8, 10, 0, 14})); +} + +TEST(A5AicpuTopologySelection, ComputesPg1MinimisesSmtSharing) { + // Die0 has 2 clusters, die1 has 1, so O is on die0. Scheduler + // primaries are exhausted before any SMT sibling is selected. + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kPg1; + topology.os_schedulable_cpus = make_physical_range(0, 5); + + std::vector allowed; + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 2, allowed)); + EXPECT_EQ(allowed, (std::vector{4, 6})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 3, allowed)); + EXPECT_EQ(allowed, (std::vector{4, 0, 6})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 4, allowed)); + EXPECT_EQ(allowed, (std::vector{4, 0, 2, 6})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 5, allowed)); + EXPECT_EQ(allowed, (std::vector{4, 0, 2, 8, 6})); +} + +TEST(A5AicpuTopologySelection, Pg1UsesAvailablePrimaryBeforeSibling) { + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kPg1; + topology.os_schedulable_cpus = make_physical_range(0, 2); + topology.os_schedulable_cpus.erase( + std::remove_if( + topology.os_schedulable_cpus.begin(), topology.os_schedulable_cpus.end(), + [](const AicpuLogicalCpu &cpu) { + return cpu.phy_cpu_id == 1 && cpu.hyperthread_id == 1; + } + ), + topology.os_schedulable_cpus.end() + ); + + std::vector allowed; + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 3, allowed)); + EXPECT_EQ(allowed, (std::vector{0, 2, 4})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 4, allowed)); + EXPECT_EQ(allowed, (std::vector{0, 2, 1, 4})); +} + +TEST(A5AicpuTopologySelection, ComputesPg1SpillsWhenPreferredDieCannotFill) { + // Tie on thread count → higher die_id (die1). O=phy7 ht0. Select the + // remaining three physical primaries before phy6's sibling. + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kPg1; + topology.os_schedulable_cpus = make_physical_range(0, 1); // die0 pairs + auto die1 = make_physical_range(6, 7); // phy6+phy7 on die1 + topology.os_schedulable_cpus.insert(topology.os_schedulable_cpus.end(), die1.begin(), die1.end()); + + std::vector allowed; + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 5, allowed)); + EXPECT_EQ(allowed, (std::vector{12, 0, 2, 13, 14})); +} + +TEST(A5AicpuTopologySelection, ComputesPg1DamagedFirstDiePacksOnSecondDie) { + // First die lost a cluster: phys 2..7 → die0 has 1 cluster, die1 has 2. + // O is on die1; keep schedulers on distinct physical CPUs. + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kPg1; + topology.os_schedulable_cpus = make_physical_range(2, 7); + + std::vector allowed; + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 5, allowed)); + EXPECT_EQ(allowed, (std::vector{12, 8, 10, 4, 14})); +} + +TEST(A5AicpuTopologySelection, ComputesPg2SharingPolicy) { + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kPg2; + topology.os_schedulable_cpus = make_physical_range(0, 1); + auto right_die = make_physical_range(4, 5); + topology.os_schedulable_cpus.insert(topology.os_schedulable_cpus.end(), right_die.begin(), right_die.end()); + + std::vector allowed; + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 2, allowed)); + EXPECT_EQ(allowed, (std::vector{8, 11})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 3, allowed)); + EXPECT_EQ(allowed, (std::vector{8, 9, 11})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 4, allowed)); + EXPECT_EQ(allowed, (std::vector{8, 9, 10, 11})); + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 5, allowed)); + EXPECT_EQ(allowed, (std::vector{8, 9, 10, 0, 11})); +} + +TEST(A5AicpuTopologySelection, MatchesFgComputeOnlyDiagram) { + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kFg; + topology.os_schedulable_cpus = { + {4, 2, 0, 1, 0}, {6, 3, 0, 1, 0}, {8, 4, 0, 2, 1}, {10, 5, 0, 2, 1}, {12, 6, 0, 3, 1}, {14, 7, 0, 3, 1}, + }; + std::reverse(topology.os_schedulable_cpus.begin(), topology.os_schedulable_cpus.end()); + + std::vector allowed; + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 5, allowed)); + EXPECT_EQ(allowed, (std::vector{12, 8, 10, 4, 14})); +} + +TEST(A5AicpuTopologySelection, MatchesPg1ComputeOnlyDiagramWithMinimumSmtSharing) { + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kPg1; + topology.scheduler_smt_enabled = true; + topology.os_schedulable_cpus = { + {4, 2, 0, 1, 0}, {6, 3, 0, 1, 0}, {7, 3, 1, 1, 0}, {8, 4, 0, 2, 1}, + {9, 4, 1, 2, 1}, {10, 5, 0, 2, 1}, {11, 5, 1, 2, 1}, + }; + std::reverse(topology.os_schedulable_cpus.begin(), topology.os_schedulable_cpus.end()); + + std::vector allowed; + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 5, allowed)); + // S=T8,T4,T6,T9; O=T10. T8/T9 is the only scheduler SMT pair. + EXPECT_EQ(allowed, (std::vector{8, 4, 6, 9, 10})); +} + +TEST(A5AicpuTopologySelection, MatchesPg2ComputeOnlyDiagram) { + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kPg2; + topology.scheduler_smt_enabled = true; + topology.os_schedulable_cpus = { + {3, 1, 1, 0, 0}, {4, 2, 0, 2, 1}, {5, 2, 1, 2, 1}, {6, 3, 0, 2, 1}, {7, 3, 1, 2, 1}, + }; + std::reverse(topology.os_schedulable_cpus.begin(), topology.os_schedulable_cpus.end()); + + std::vector allowed; + ASSERT_TRUE(compute_scenario_allowed_cpus(topology, 5, allowed)); + EXPECT_EQ(allowed, (std::vector{4, 5, 6, 3, 7})); +} + +TEST(A5AicpuLaunchPlan, AutoShrinksAndWarnsBelowFiveStableCpus) { + AicpuTopology topology; + topology.source = AicpuTopologySource::kDriver; + topology.scenario_type = AicpuScenarioType::kUnknown; + topology.os_schedulable_cpus = {{1, -1, -1, -1, -1}, {3, -1, -1, -1, -1}, {5, -1, -1, -1, -1}, {7, -1, -1, -1, -1}}; + AicpuLaunchPlan plan; + std::string error; + + ASSERT_TRUE(build_aicpu_launch_plan(topology, /*requested_active_count=*/0, plan, error)) << error; + EXPECT_EQ(plan.effective_active_count, 4); + EXPECT_EQ(plan.launch_count, 4); + EXPECT_TRUE(plan.warn_stable_reachable_below_default); + EXPECT_FALSE(plan.warn_cpu_topology_unavailable); + EXPECT_EQ(plan.allowed_cpus, (std::vector{1, 3, 5, 7})); +} + +TEST(A5AicpuLaunchPlan, WarnsWhenCpuTopologyFallsBackToOccupy) { + AicpuTopology topology; + topology.source = AicpuTopologySource::kOccupyFallback; + topology.scenario_type = AicpuScenarioType::kUnknown; + ASSERT_TRUE(enumerate_cpus_from_occupy(0x1fU, topology.os_schedulable_cpus)); + AicpuLaunchPlan plan; + std::string error; + + ASSERT_TRUE(build_aicpu_launch_plan(topology, 0, plan, error)) << error; + EXPECT_TRUE(plan.warn_cpu_topology_unavailable); + EXPECT_FALSE(plan.warn_stable_reachable_below_default); +} + +TEST(A5AicpuLaunchPlan, WarnsWhenCpuTopologyFallsBackToPackagedJson) { + std::vector all_cpus; + ASSERT_TRUE(load_cpu_topo_from_json("Ascend950PR_9599", 0x1f8, all_cpus)); + + AicpuTopology topology; + topology.source = AicpuTopologySource::kJsonFallback; + topology.scenario_type = AicpuScenarioType::kFg; + for (const auto &cpu : all_cpus) { + if (cpu.cpu_id >= 3 && cpu.cpu_id <= 8) topology.os_schedulable_cpus.push_back(cpu); + } + + AicpuLaunchPlan plan; + std::string error; + ASSERT_TRUE(build_aicpu_launch_plan(topology, 0, plan, error)) << error; + EXPECT_TRUE(plan.warn_cpu_topology_unavailable); + EXPECT_FALSE(plan.warn_stable_reachable_below_default); + EXPECT_EQ(plan.effective_active_count, 5); + EXPECT_EQ(plan.launch_count, 6); +} + +TEST(A5AicpuLaunchPlan, EnforcesManualAndLaunchCapacityWithoutClamping) { + AicpuTopology topology; + topology.source = AicpuTopologySource::kOccupyFallback; + topology.scenario_type = AicpuScenarioType::kUnknown; + ASSERT_TRUE(enumerate_cpus_from_occupy((1ULL << 4) - 1, topology.os_schedulable_cpus)); + AicpuLaunchPlan plan; + std::string error; + EXPECT_FALSE(build_aicpu_launch_plan(topology, 5, plan, error)); + + ASSERT_TRUE(enumerate_cpus_from_occupy((1ULL << 14) - 1, topology.os_schedulable_cpus)); + ASSERT_TRUE(build_aicpu_launch_plan(topology, 5, plan, error)) << error; + EXPECT_EQ(plan.launch_count, 14); + + ASSERT_TRUE(enumerate_cpus_from_occupy((1ULL << 15) - 1, topology.os_schedulable_cpus)); + EXPECT_FALSE(build_aicpu_launch_plan(topology, 5, plan, error)); + EXPECT_NE(error.find("launch capacity"), std::string::npos); +} + +TEST(A5AicpuTopologySelection, UnknownFallbackUsesTopologyOrderAndExactCount) { + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kUnknown; + topology.os_schedulable_cpus = { + {50, 4, 1, 2, 1}, {7, 0, 0, 0, 0}, {8, 0, 1, 0, 0}, {12, 1, 0, 0, 0}, {2, 4, 0, 2, 1}, {99, 5, 0, 2, 1}, + }; + + std::vector allowed; + ASSERT_TRUE(compute_unknown_allowed_cpus(topology, 4, allowed)); + EXPECT_EQ(allowed, (std::vector{7, 8, 12, 2})); + ASSERT_TRUE(compute_unknown_allowed_cpus(topology, 5, allowed)); + EXPECT_EQ(allowed, (std::vector{7, 8, 12, 2, 50})); +} + +TEST(A5AicpuTopologySelection, UnknownFallbackRequiresSchedulerAndOrchestrator) { + AicpuTopology topology; + topology.os_schedulable_cpus = {{7, 0, 0, 0, 0}}; + std::vector allowed = {123}; + EXPECT_FALSE(compute_unknown_allowed_cpus(topology, 2, allowed)); + EXPECT_TRUE(allowed.empty()); +} + +TEST(A5AicpuTopologySelection, UnknownFallbackRejectsInvalidOrInsufficientRequestedCount) { + AicpuTopology topology; + topology.os_schedulable_cpus = {{1, -1, -1, -1, -1}, {3, -1, -1, -1, -1}, {5, -1, -1, -1, -1}}; + std::vector allowed; + + EXPECT_FALSE(compute_unknown_allowed_cpus(topology, 1, allowed)); + EXPECT_FALSE(compute_unknown_allowed_cpus(topology, 4, allowed)); + EXPECT_FALSE(compute_unknown_allowed_cpus(topology, 6, allowed)); + ASSERT_TRUE(compute_unknown_allowed_cpus(topology, 3, allowed)); + EXPECT_EQ(allowed, (std::vector{1, 3, 5})); +} + +TEST(A5AicpuTopologySelection, KnownScenarioRejectsUnsupportedActiveCount) { + AicpuTopology topology; + topology.scenario_type = AicpuScenarioType::kFg; + topology.os_schedulable_cpus = primaries(make_physical_range(0, 7)); + std::vector allowed = {123}; + + EXPECT_FALSE(compute_scenario_allowed_cpus(topology, 1, allowed)); + EXPECT_TRUE(allowed.empty()); + EXPECT_FALSE(compute_scenario_allowed_cpus(topology, 6, allowed)); + EXPECT_TRUE(allowed.empty()); } } // namespace diff --git a/tools/cann-examples/aicpu-device-query/host/query_device_hal.cpp b/tools/cann-examples/aicpu-device-query/host/query_device_hal.cpp index e046ec7882..dece9004d7 100644 --- a/tools/cann-examples/aicpu-device-query/host/query_device_hal.cpp +++ b/tools/cann-examples/aicpu-device-query/host/query_device_hal.cpp @@ -315,10 +315,8 @@ int main(int argc, char **argv) { int device_id = std::atoi(argv[1]); const char *dispatcher_path_env = std::getenv("SIMPLER_DISPATCHER_SO"); - std::string dispatcher_path = dispatcher_path_env ? - dispatcher_path_env : - "/data/wcwxyai/workspace/simpler/.claude/worktrees/parallel-petting-newt/build/" - "lib/a2a3/dispatcher/libsimpler_aicpu_dispatcher.so"; + std::string dispatcher_path = + dispatcher_path_env ? dispatcher_path_env : "build/lib/a2a3/dispatcher/libsimpler_aicpu_dispatcher.so"; const char *inner_path_env = std::getenv("SIMPLER_AICPU_QUERY_SO"); std::string inner_path = inner_path_env ? inner_path_env : "tools/cann-examples/aicpu-device-query/device/build/libaicpu_query.so";