diff --git a/CMakeLists.txt b/CMakeLists.txt index 90f0495fbe..7070217366 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,11 @@ if(SKBUILD_MODE) # sanitizer build is requested. install(DIRECTORY ${CMAKE_SOURCE_DIR}/cmake/ DESTINATION simpler_setup/_assets/cmake) + # The public RTT preflight CLI builds and invokes this small hardware + # backend from either a source checkout or an installed wheel. + install(DIRECTORY ${CMAKE_SOURCE_DIR}/tools/cann-examples/aicpu-device-query/ + DESTINATION simpler_setup/_assets/tools/cann-examples/aicpu-device-query + PATTERN "build" EXCLUDE) install(DIRECTORY ${CMAKE_SOURCE_DIR}/build/lib/ DESTINATION simpler_setup/_assets/build/lib OPTIONAL diff --git a/simpler_setup/tools/README.md b/simpler_setup/tools/README.md index 07f0fc7f4e..50a9097282 100644 --- a/simpler_setup/tools/README.md +++ b/simpler_setup/tools/README.md @@ -18,6 +18,7 @@ no repo checkout required. - **[phase_time_split](#phase_time_split)** — the same `bind phase=` markers split into on-CPU and off-CPU per phase, from per-thread CPU clocks, with cold and warm binds reported separately - **[dump_viewer](#dump_viewer)** — inspect / export args dumps (see [docs/args-dump.md](../../docs/dfx/args-dump.md) for full workflow) - **[deps_viewer](#deps_viewer)** — `deps.json` (dep_gen) → text or pan/zoom HTML dependency graph +- **[rtt_die_preflight](#rtt_die_preflight)** — full AICPU affinity preflight → `aicpu_affinity_plan.json` (authoritative `allowed_cpus`) For CLIs that allow an omitted input, auto-detection paths (`outputs/*/chip_swimlane_records.json`, `outputs/*/args_dump/`) are resolved @@ -828,6 +829,36 @@ For batch-run hardware regression, see the dev-only script --- +## rtt_die_preflight + +Full A5 AICPU affinity preflight. Invokes `aicpu-device-query --rtt-json` to: + +1. enumerate the user AICPU pool (serial `aicpu_num=1`) +2. elect the orchestrator via atomic-flag pairwise handshake (1000 iters) +3. score non-orch threads with COND die sums (100 samples/core) +4. pack physical picks `{die0,die1,die1,die0}` into logical + `[S0,S1,S2,S3,O]` so **S0/S1 own die0 and S2/S3 own die1** + +Atomically merges into `build/config/aicpu_affinity_plan.json` (schema v3) keyed +by `(soc_name, device_id)`. L3 multi-card binds keep one bucket per device. +Pool size `< 5` writes a contiguous shrink plan and skips handshake/COND. + +On first runtime miss, `DeviceRunner` auto-runs this CLI with +`--plan-source auto-first-run` (disable with `SIMPLER_AFFINITY_PREFLIGHT_AUTO=0`). +Subsequent runs load the plan as the authoritative `allowed_cpus` and skip FG/PG +topo selection. Force a refresh with `--probe`. + +```bash +task-submit --device auto --device-num 1 --run \ + 'python -m simpler_setup.tools.rtt_die_preflight --device "$TASK_DEVICE" --probe' + +# Offline authoritative write (plan_source=manual): +python -m simpler_setup.tools.rtt_die_preflight --device 0 \ + --soc Ascend950PR_9599 --allowed-cpus 3,4,5,6,8 +``` + +--- + ## Output File Reference | File | Tool | Purpose | Format | diff --git a/simpler_setup/tools/__init__.py b/simpler_setup/tools/__init__.py index f68fe4505b..647d3cc7f3 100644 --- a/simpler_setup/tools/__init__.py +++ b/simpler_setup/tools/__init__.py @@ -17,4 +17,5 @@ - ``dump_viewer`` : inspect args dumps - ``strace_timing`` : per-stage / per-round timing from [STRACE] log markers - ``hbg_bind_phases`` : per-phase host_build_graph bind statistics from `bind phase=` markers +- ``rtt_die_preflight`` : probe A5 COND RTT and write the scheduler die plan """ diff --git a/simpler_setup/tools/rtt_die_preflight.py b/simpler_setup/tools/rtt_die_preflight.py new file mode 100644 index 0000000000..477ea9845d --- /dev/null +++ b/simpler_setup/tools/rtt_die_preflight.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +# Copyright (c) PyPTO Contributors. +# This program is free software, you can redistribute it and/or modify it under the terms and conditions of +# CANN Open Software License Agreement Version 2.0 (the "License"). +# Please refer to the License for details. You may not use this file except in compliance with the License. +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. +# See LICENSE in the root of the software repository for the full text of the License. +# ----------------------------------------------------------------------------------------------------------- +"""Full A5 AICPU affinity preflight: orch via atomic-flag handshake, sched via COND die scores.""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +import tempfile +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Any + +from simpler_setup.environment import PROJECT_ROOT + +SCHEDULER_COUNT = 4 +PROBE_SCHEMA_VERSION = 3 +PLAN_SCHEMA_VERSION = 3 +MEASUREMENT_METHOD = "atomic-flag-orch+cond-die-v1" +# Physical selection order for the four scheduler picks, then packed so that +# logical S0/S1 own die0 and S2/S3 own die1. +PHYS_PICK_DIES = (0, 1, 1, 0) +PROBE_SAMPLES_PER_CORE = 100 +PROBE_HANDSHAKE_ITERS = 1000 +RELATIVE_PLAN = Path("build/config/aicpu_affinity_plan.json") +# Keep reading the legacy path name as a fallback for older checkouts. +RELATIVE_PLAN_LEGACY = Path("build/config/aicpu_rtt_die_plan.json") +RELATIVE_PROBE_RUNNER = Path("tools/cann-examples/aicpu-device-query/run_query_topo.sh") + + +def default_plan_path() -> Path: + return PROJECT_ROOT / RELATIVE_PLAN + + +def default_probe_runner() -> Path: + return PROJECT_ROOT / RELATIVE_PROBE_RUNNER + + +def _require_int(value: object, field: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{field} must be an integer") + return value + + +def pick_orchestrator(pool: Sequence[Mapping[str, Any]]) -> int: + """Return pool_idx with the smallest avg_handshake_ticks (tie: smaller idx).""" + if not pool: + raise ValueError("empty pool") + best = min(pool, key=lambda entry: (int(entry["avg_handshake_ticks"]), int(entry["pool_idx"]))) + return int(best["pool_idx"]) + + +def pack_schedulers_from_die_scores( + candidates: Sequence[Mapping[str, Any]], +) -> tuple[list[int], list[dict[str, Any]]]: + """ + Phys pick order die0,die1,die1,die0 → pack to logical [S0,S1,S2,S3] + where S0/S1 own die0 and S2/S3 own die1. + + Picks P0..P3 by target die; allowed logical order is [P0, P3, P1, P2]. + """ + if len(candidates) < SCHEDULER_COUNT: + raise ValueError(f"need at least {SCHEDULER_COUNT} non-orch candidates") + + remaining = [dict(entry) for entry in candidates] + picks: list[dict[str, Any]] = [] + for target_die in PHYS_PICK_DIES: + score_key = "die0_sum_ticks" if target_die == 0 else "die1_sum_ticks" + + def sort_key(entry: Mapping[str, Any], key: str = score_key) -> tuple[int, int]: + return (int(entry[key]), int(entry["cpu_id"])) + + remaining.sort(key=sort_key) + chosen = remaining.pop(0) + chosen = dict(chosen) + chosen["assigned_die"] = target_die + picks.append(chosen) + + # picks: P0(die0), P1(die1), P2(die1), P3(die0) → logical [P0, P3, P1, P2] + logical = [picks[0], picks[3], picks[1], picks[2]] + for logical_idx, entry in enumerate(logical): + entry["logical_idx"] = logical_idx + entry["assigned_die"] = 0 if logical_idx < 2 else 1 + allowed_sched = [int(entry["cpu_id"]) for entry in logical] + return allowed_sched, logical + + +def build_allowed_cpus_from_probe(probe: Mapping[str, Any]) -> dict[str, Any]: + """Turn device probe JSON into an authoritative device plan node.""" + if probe.get("pool_too_small"): + pool = [_require_int(cpu, "user_pool_cpus[]") for cpu in probe["user_pool_cpus"]] + if len(pool) < 2: + raise ValueError("pool_too_small requires at least 2 CPUs") + # Shrink: contiguous order, last is orch. No handshake/COND plan. + active = max(2, min(len(pool), SCHEDULER_COUNT + 1)) + allowed = list(pool[:active]) + return { + "soc_name": probe["soc_name"], + "architecture": "a5", + "plan_source": "pool-too-small-contiguous", + "user_pool_cpus": pool, + "allowed_cpus": allowed, + "orch_cpu": allowed[-1], + "schedulers": [ + { + "logical_idx": idx, + "cpu_id": cpu, + "assigned_die": 0 if idx < max(1, (active - 1) // 2) else 1, + "die0_sum_ticks": 0, + "die1_sum_ticks": 0, + } + for idx, cpu in enumerate(allowed[:-1]) + ], + "pool_too_small": True, + } + + pool_raw = probe.get("pool") + if not isinstance(pool_raw, list) or len(pool_raw) < 5: + raise ValueError("probe pool must contain at least 5 entries") + + pool: list[dict[str, Any]] = [] + for raw in pool_raw: + if not isinstance(raw, dict): + raise ValueError("pool entry must be an object") + entry = { + "pool_idx": _require_int(raw.get("pool_idx"), "pool_idx"), + "cpu_id": _require_int(raw.get("cpu_id"), "cpu_id"), + "avg_handshake_ticks": _require_int(raw.get("avg_handshake_ticks"), "avg_handshake_ticks"), + "die0_sum_ticks": _require_int(raw.get("die0_sum_ticks"), "die0_sum_ticks"), + "die1_sum_ticks": _require_int(raw.get("die1_sum_ticks"), "die1_sum_ticks"), + "is_orch": _require_int(raw.get("is_orch"), "is_orch"), + } + pool.append(entry) + + orch_idx = _require_int(probe.get("orch_pool_idx"), "orch_pool_idx") + orch_entries = [e for e in pool if int(e["pool_idx"]) == orch_idx] + if len(orch_entries) != 1: + # Fall back to software election from handshake averages. + orch_idx = pick_orchestrator(pool) + orch_entries = [e for e in pool if int(e["pool_idx"]) == orch_idx] + orch = orch_entries[0] + candidates = [e for e in pool if int(e["pool_idx"]) != orch_idx] + sched_cpus, schedulers = pack_schedulers_from_die_scores(candidates) + allowed = sched_cpus + [int(orch["cpu_id"])] + if len(set(allowed)) != len(allowed): + raise ValueError("allowed_cpus contains duplicates") + + return { + "soc_name": probe["soc_name"], + "architecture": "a5", + "plan_source": MEASUREMENT_METHOD, + "user_pool_cpus": [_require_int(cpu, "user_pool_cpus[]") for cpu in probe.get("user_pool_cpus", [])], + "allowed_cpus": allowed, + "orch_cpu": int(orch["cpu_id"]), + "orch_avg_handshake_ticks": int(orch["avg_handshake_ticks"]), + "schedulers": [ + { + "logical_idx": int(s["logical_idx"]), + "cpu_id": int(s["cpu_id"]), + "assigned_die": int(s["assigned_die"]), + "die0_sum_ticks": int(s["die0_sum_ticks"]), + "die1_sum_ticks": int(s["die1_sum_ticks"]), + } + for s in schedulers + ], + "pool_too_small": False, + } + + +def validate_probe_result(raw: object, expected_device: int) -> dict[str, Any]: + if not isinstance(raw, dict): + raise ValueError("probe output must be a JSON object") + if _require_int(raw.get("schema_version"), "schema_version") != PROBE_SCHEMA_VERSION: + raise ValueError("unsupported affinity probe schema_version") + if raw.get("measurement_method") != MEASUREMENT_METHOD: + raise ValueError("unsupported measurement_method") + soc_name = raw.get("soc_name") + if not isinstance(soc_name, str) or not soc_name.startswith("Ascend950"): + raise ValueError(f"unsupported probe soc_name: {soc_name!r}") + if _require_int(raw.get("device_id"), "device_id") != expected_device: + raise ValueError("probe output device_id does not match --device") + if raw.get("pool_too_small"): + pool = raw.get("user_pool_cpus") + if not isinstance(pool, list) or len(pool) < 2: + raise ValueError("pool_too_small requires user_pool_cpus with >= 2 entries") + return { + "schema_version": PROBE_SCHEMA_VERSION, + "measurement_method": MEASUREMENT_METHOD, + "soc_name": soc_name, + "device_id": expected_device, + "pool_too_small": True, + "user_pool_cpus": [_require_int(cpu, "user_pool_cpus[]") for cpu in pool], + } + if _require_int(raw.get("samples_per_core", PROBE_SAMPLES_PER_CORE), "samples_per_core") != PROBE_SAMPLES_PER_CORE: + raise ValueError("unexpected samples_per_core") + return raw # type: ignore[return-value] + + +def run_affinity_probe(device_id: int, runner: Path | None = None) -> dict[str, Any]: + runner = default_probe_runner() if runner is None else runner + if not runner.is_file(): + raise RuntimeError(f"affinity probe backend is missing: {runner}") + try: + completed = subprocess.run( + ["bash", str(runner), "--device", str(device_id), "--rtt-json"], + cwd=PROJECT_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + ) + except subprocess.CalledProcessError as exc: + raise RuntimeError(f"affinity probe backend failed with exit code {exc.returncode}") from exc + try: + raw = json.loads(completed.stdout) + except json.JSONDecodeError as exc: + raise RuntimeError("affinity probe backend returned invalid JSON") from exc + return validate_probe_result(raw, device_id) + + +# Back-compat alias for older call sites / tests. +run_rtt_probe = run_affinity_probe + + +def load_plan(path: Path) -> dict[str, Any]: + if not path.is_file(): + return {"schema_version": PLAN_SCHEMA_VERSION, "socs": {}} + plan = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(plan, dict) or not isinstance(plan.get("socs"), dict): + raise ValueError(f"existing plan is malformed: {path}") + version = _require_int(plan.get("schema_version"), "schema_version") + if version != PLAN_SCHEMA_VERSION: + raise ValueError(f"existing plan uses an unsupported schema_version; regenerate it: {path}") + return plan + + +def merge_device_plan( + plan: dict[str, Any], *, soc_name: str, device_id: int, device_node: dict[str, Any] +) -> dict[str, Any]: + devices = plan.setdefault("socs", {}).setdefault(soc_name, {}).setdefault("devices", {}) + devices[str(device_id)] = device_node + plan["schema_version"] = PLAN_SCHEMA_VERSION + plan["_comment"] = ( + "A5 AICPU affinity plan (authoritative allowed_cpus). " + "Logical S0/S1 own die0; S2/S3 own die1. Generated by simpler_setup.tools.rtt_die_preflight." + ) + return plan + + +def atomic_write_plan(path: Path, plan: Mapping[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary_name: str | None = None + try: + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", delete=False + ) as temporary: + temporary_name = temporary.name + json.dump(plan, temporary, indent=2) + temporary.write("\n") + temporary.flush() + os.fsync(temporary.fileno()) + os.replace(temporary_name, path) + finally: + if temporary_name is not None: + Path(temporary_name).unlink(missing_ok=True) + + +def build_device_node_from_allowed( + *, + soc_name: str, + allowed_cpus: Sequence[int], + plan_source: str = "manual", +) -> dict[str, Any]: + if len(allowed_cpus) < 2 or len(set(allowed_cpus)) != len(allowed_cpus): + raise ValueError("allowed_cpus must be unique and contain at least 1S+1O") + schedulers = [ + { + "logical_idx": idx, + "cpu_id": int(cpu), + "assigned_die": 0 if idx < 2 else 1, + "die0_sum_ticks": 0, + "die1_sum_ticks": 0, + } + for idx, cpu in enumerate(allowed_cpus[:-1]) + ] + return { + "soc_name": soc_name, + "architecture": "a5", + "plan_source": plan_source, + "allowed_cpus": list(map(int, allowed_cpus)), + "orch_cpu": int(allowed_cpus[-1]), + "schedulers": schedulers, + "pool_too_small": len(allowed_cpus) < SCHEDULER_COUNT + 1, + } + + +def parse_int_list(text: str) -> list[int]: + return [int(part.strip()) for part in text.replace(";", ",").split(",") if part.strip()] + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Probe A5 AICPU affinity and write the authoritative allowed_cpus plan." + ) + parser.add_argument("--device", type=int, default=0, help="Logical ACL device id") + parser.add_argument("--out", type=Path, default=None, help=f"Output JSON path (default: {default_plan_path()})") + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--probe", action="store_true", help="Run the full affinity preflight on device") + mode.add_argument( + "--allowed-cpus", + help="Offline authoritative allowed_cpus (S0,S1,S2,S3,O); requires --soc", + ) + parser.add_argument("--soc", help="SoC name for offline mode") + parser.add_argument( + "--plan-source", + default=None, + help="Optional plan_source override (e.g. auto-first-run for DeviceRunner miss path)", + ) + args = parser.parse_args(argv) + + out_path = args.out if args.out is not None else default_plan_path() + try: + if args.probe: + if args.soc is not None: + parser.error("--probe obtains --soc from hardware") + probe = run_affinity_probe(args.device) + device_node = build_allowed_cpus_from_probe(probe) + if args.plan_source is not None: + device_node["plan_source"] = args.plan_source + soc_name = device_node["soc_name"] + else: + if args.soc is None or args.allowed_cpus is None: + parser.error("offline mode requires --soc and --allowed-cpus") + soc_name = args.soc + allowed = parse_int_list(args.allowed_cpus) + device_node = build_device_node_from_allowed( + soc_name=soc_name, + allowed_cpus=allowed, + plan_source=args.plan_source if args.plan_source is not None else "manual", + ) + + plan = merge_device_plan(load_plan(out_path), soc_name=soc_name, device_id=args.device, device_node=device_node) + atomic_write_plan(out_path, plan) + except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as exc: + print(f"rtt_die_preflight: {exc}", file=sys.stderr) + return 1 + print( + f"Wrote affinity plan: {out_path} soc={soc_name} device={args.device} " + f"allowed_cpus={device_node['allowed_cpus']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/a5/docs/hardware.md b/src/a5/docs/hardware.md index de87c51237..94d84677b9 100644 --- a/src/a5/docs/hardware.md +++ b/src/a5/docs/hardware.md @@ -264,18 +264,40 @@ Scenario A (OCCUPY=0x1f8, 6 user cpus): the failure as `aclrtSynchronizeStream rc=507000` (runtime internal) after the launch. -The runtime implements the safe choice: a one-thread preflight AICPU query -reads device-side OCCUPY, then the host topology probe sets +The runtime implements the safe choice: its one-thread topology 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: -- `src/a5/platform/onboard/host/aicpu_topology_probe.{h,cpp}` — probe + - cluster-first packing -- `src/a5/platform/onboard/host/device_runner.cpp` — fills - `aicpu_allowed_cpus[]` + `aicpu_launch_count` in Runtime, launches - with that count +The separate, explicit affinity preflight replaces FG/PG topo selection as the +authoritative source of `allowed_cpus` when a validated plan exists for the +`(soc, device_id)` pair. `aicpu-device-query --rtt-json` (and +`python -m simpler_setup.tools.rtt_die_preflight --probe`) serialize: + +1. single-thread enumeration of the user AICPU pool +2. multi-thread atomic-flag pairwise handshake (1000 iters) to elect the + orchestrator (minimum average latency to peers) +3. per non-orch thread COND LDR sums across each die (100 samples/core) +4. physical picks in die order `{0,1,1,0}`, then pack to logical + `[S0,S1,S2,S3,O]` so **S0/S1 own die0 and S2/S3 own die1** + +Results land in `build/config/aicpu_affinity_plan.json` (schema v3). On first +runtime miss for a card, `DeviceRunner` auto-runs the preflight (disable with +`SIMPLER_AFFINITY_PREFLIGHT_AUTO=0`). L3 multi-device binds preflight each +`device_id` independently. Pool size `< 5` skips handshake/COND measurement and +falls back to contiguous OCCUPY order (at least 1S+1O) with a warning. + +Users see only the logical sched↔die contract; physical `cpu_id` values remain +diagnostic. Contiguous AICore ownership still follows logical S0–S3. + +- `src/a5/platform/onboard/host/aicpu_topology_probe.{h,cpp}` — OCCUPY/topo + helpers retained for launch_count and fail-soft fallback +- `src/a5/platform/onboard/host/aicpu_rtt_die_plan_reader.*` — authoritative + per-device `allowed_cpus` reader +- `src/a5/platform/onboard/host/device_runner.cpp` — plan hit / first-run miss / + topo fallback; fills `aicpu_allowed_cpus[]` + `aicpu_launch_count` - `src/common/platform/onboard/aicpu/platform_aicpu_affinity.cpp` — `platform_aicpu_affinity_gate_filter()` (the post-hoc classifier) diff --git a/src/a5/platform/include/common/sched_aicore_assignment.h b/src/a5/platform/include/common/sched_aicore_assignment.h new file mode 100644 index 0000000000..db67e42256 --- /dev/null +++ b/src/a5/platform/include/common/sched_aicore_assignment.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ + +#pragma once + +#include + +constexpr int32_t kRttDieSchedSlots = 4; + +// Schema-v3 affinity plans already pack logical order into allowed_cpus +// ([S0,S1,S2,S3,O] with S0/S1->die0 and S2/S3->die1). Remap is therefore +// identity whenever a plan applies; the helper remains for call-site ABI. +inline bool rtt_die_plan_applies(bool plan_valid, int32_t active_threads) { + return plan_valid && active_threads == kRttDieSchedSlots; +} + +inline int32_t +logical_sched_for_affinity(bool plan_valid, int32_t active_threads, int32_t affinity_idx, int32_t mapped_logical_idx) { + if (!rtt_die_plan_applies(plan_valid, active_threads) || affinity_idx < 0 || affinity_idx >= active_threads || + mapped_logical_idx < 0 || mapped_logical_idx >= active_threads) { + return affinity_idx; + } + return mapped_logical_idx; +} + +// Balanced contiguous partition. Scheduler t owns the cluster interval +// [ceil(t*N/A), ceil((t+1)*N/A)). With four schedulers this yields +// S0/S1 on the first half (die0) and S2/S3 on the second half (die1) for the +// usual 2-die A5 layouts. +inline int32_t contiguous_sched_for_cluster(int32_t cluster_idx, int32_t cluster_count, int32_t active_threads) { + if (cluster_idx < 0 || cluster_count <= 0 || active_threads <= 0) return 0; + int64_t sched = static_cast(cluster_idx) * active_threads / cluster_count; + if (sched >= active_threads) sched = active_threads - 1; + return static_cast(sched); +} + +inline int32_t contiguous_cluster_begin(int32_t sched_idx, int32_t cluster_count, int32_t active_threads) { + if (sched_idx <= 0 || cluster_count <= 0 || active_threads <= 0) return 0; + if (sched_idx >= active_threads) return cluster_count; + const int64_t numerator = static_cast(sched_idx) * cluster_count; + return static_cast((numerator + active_threads - 1) / active_threads); +} + +inline bool cluster_owned_by_contiguous_sched( + int32_t cluster_idx, int32_t sched_idx, int32_t cluster_count, int32_t active_threads +) { + return contiguous_sched_for_cluster(cluster_idx, cluster_count, active_threads) == sched_idx; +} + +// Pack physical picks P0..P3 chosen in die order {0,1,1,0} into logical +// allowed_cpus order [P0, P3, P1, P2] so S0/S1 own die0 and S2/S3 own die1. +inline void pack_phys_picks_to_logical_cpus( + const int32_t phys_cpu_by_pick[kRttDieSchedSlots], int32_t logical_cpus_out[kRttDieSchedSlots] +) { + logical_cpus_out[0] = phys_cpu_by_pick[0]; + logical_cpus_out[1] = phys_cpu_by_pick[3]; + logical_cpus_out[2] = phys_cpu_by_pick[1]; + logical_cpus_out[3] = phys_cpu_by_pick[2]; +} diff --git a/src/a5/platform/onboard/host/CMakeLists.txt b/src/a5/platform/onboard/host/CMakeLists.txt index 978afcfe4b..113d4dc983 100644 --- a/src/a5/platform/onboard/host/CMakeLists.txt +++ b/src/a5/platform/onboard/host/CMakeLists.txt @@ -60,6 +60,7 @@ list(APPEND CMAKE_CUSTOM_INCLUDE_DIRS "${PTO_ISA_ROOT}/include") set(HOST_RUNTIME_SOURCES "") list(APPEND HOST_RUNTIME_SOURCES ${SIMPLER_HOST_LOG_SOURCES}) list(APPEND HOST_RUNTIME_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/aicpu_rtt_die_plan_reader.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/aicpu_topology_probe.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/device_runner.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/memory_allocator.cpp" diff --git a/src/a5/platform/onboard/host/aicpu_rtt_die_plan_reader.cpp b/src/a5/platform/onboard/host/aicpu_rtt_die_plan_reader.cpp new file mode 100644 index 0000000000..262f6a017b --- /dev/null +++ b/src/a5/platform/onboard/host/aicpu_rtt_die_plan_reader.cpp @@ -0,0 +1,315 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ + +#include "aicpu_rtt_die_plan_reader.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pto::a5 { +namespace { + +constexpr char kAffinityPlanRelativePath[] = "build/config/aicpu_affinity_plan.json"; +constexpr char kLegacyPlanRelativePath[] = "build/config/aicpu_rtt_die_plan.json"; +constexpr char kProjectSourceMarker[] = "src/a5/platform/onboard/host/aicpu_rtt_die_plan_reader.cpp"; +constexpr char kProjectToolMarker[] = "tools/cann-examples/aicpu-device-query/run_query_topo.sh"; +constexpr int32_t kAffinityPlanSchemaVersion = 3; +constexpr char kMeasuredPlanSource[] = "atomic-flag-orch+cond-die-v1"; +constexpr char kManualPlanSource[] = "manual"; +constexpr char kAutoFirstRunSource[] = "auto-first-run"; +constexpr char kPoolTooSmallSource[] = "pool-too-small-contiguous"; + +void skip_json_ws(const char *&p) { + while (*p != '\0' && std::isspace(static_cast(*p))) + ++p; +} + +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; +} + +bool find_json_object_end(const char *start, const char *&end) { + if (start == nullptr || *start != '{') return false; + const char *cursor = start; + int depth = 0; + do { + if (*cursor == '\0') return false; + if (*cursor == '"') { + std::string unused; + if (!parse_json_string(cursor, unused)) return false; + continue; + } + if (*cursor == '{') { + ++depth; + } else if (*cursor == '}') { + --depth; + } + ++cursor; + } while (depth > 0); + end = cursor; + return depth == 0; +} + +bool parse_json_int(const char *&p, int32_t &out) { + skip_json_ws(p); + char *end = nullptr; + const long value = std::strtol(p, &end, 10); + if (end == p || value < std::numeric_limits::min() || value > std::numeric_limits::max()) { + return false; + } + out = static_cast(value); + p = end; + return true; +} + +bool parse_json_bool(const char *&p, bool &out) { + skip_json_ws(p); + if (std::strncmp(p, "true", 4) == 0) { + out = true; + p += 4; + return true; + } + if (std::strncmp(p, "false", 5) == 0) { + out = false; + p += 5; + return true; + } + return false; +} + +const char *find_key_object(const char *text, const char *key) { + const std::string needle = std::string("\"") + key + "\""; + const char *cursor = text; + while ((cursor = std::strstr(cursor, needle.c_str())) != nullptr) { + const char *p = cursor + needle.size(); + skip_json_ws(p); + if (*p == ':') { + ++p; + skip_json_ws(p); + if (*p == '{') return p; + } + cursor += needle.size(); + } + return nullptr; +} + +const char *find_key_object_before(const char *start, const char *end, const char *key) { + const std::string needle = std::string("\"") + key + "\""; + const char *cursor = start; + while (cursor < end && (cursor = std::strstr(cursor, needle.c_str())) != nullptr && cursor < end) { + const char *p = cursor + needle.size(); + skip_json_ws(p); + if (p < end && *p == ':') { + ++p; + skip_json_ws(p); + if (p < end && *p == '{') return p; + } + cursor += needle.size(); + } + return nullptr; +} + +const char *find_key_value(const char *object_start, const char *key) { + const char *end = object_start; + int depth = 0; + do { + if (*end == '\0') return nullptr; + if (*end == '"') { + std::string unused; + const char *next = end; + if (!parse_json_string(next, unused)) return nullptr; + end = next; + continue; + } + if (*end == '{') { + ++depth; + } else if (*end == '}') { + --depth; + if (depth == 0) { + ++end; + break; + } + } + ++end; + } while (depth > 0); + + const std::string needle = std::string("\"") + key + "\""; + const char *cursor = object_start; + while (cursor < end && (cursor = std::strstr(cursor, needle.c_str())) != nullptr && cursor < end) { + const char *p = cursor + needle.size(); + skip_json_ws(p); + if (*p == ':') { + ++p; + skip_json_ws(p); + return p; + } + cursor += needle.size(); + } + return nullptr; +} + +bool parse_json_int_array(const char *p, std::vector &out) { + out.clear(); + skip_json_ws(p); + if (*p != '[') return false; + ++p; + skip_json_ws(p); + if (*p == ']') return true; + while (*p != '\0') { + int32_t value = 0; + if (!parse_json_int(p, value)) return false; + out.push_back(value); + skip_json_ws(p); + if (*p == ',') { + ++p; + skip_json_ws(p); + continue; + } + return *p == ']'; + } + return false; +} + +bool is_supported_plan_source(const std::string &source) { + return source == kMeasuredPlanSource || source == kManualPlanSource || source == kAutoFirstRunSource || + source == kPoolTooSmallSource; +} + +bool read_plan_json(std::string &out_text) { + Dl_info info{}; + if (dladdr(reinterpret_cast(&read_plan_json), &info) == 0 || info.dli_fname == nullptr) { + return false; + } + + std::filesystem::path candidate = std::filesystem::absolute(info.dli_fname).parent_path(); + while (!candidate.empty()) { + if (std::filesystem::is_regular_file(candidate / kProjectSourceMarker) && + std::filesystem::is_regular_file(candidate / kProjectToolMarker)) { + for (const char *relative : {kAffinityPlanRelativePath, kLegacyPlanRelativePath}) { + std::ifstream input(candidate / relative); + if (!input) continue; + out_text.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + if (input.good() || input.eof()) return true; + } + return false; + } + const std::filesystem::path parent = candidate.parent_path(); + if (parent == candidate) break; + candidate = parent; + } + return false; +} + +} // namespace + +bool parse_rtt_die_plan_for_device( + const char *json_text, const char *soc_name, int32_t device_id, AicpuRttDiePlan &out_plan +) { + out_plan = AicpuRttDiePlan{}; + if (json_text == nullptr || soc_name == nullptr || soc_name[0] == '\0' || device_id < 0) return false; + + const char *root = json_text; + skip_json_ws(root); + if (*root != '{') return false; + const char *schema_value = find_key_value(root, "schema_version"); + int32_t schema_version = 0; + if (schema_value == nullptr || !parse_json_int(schema_value, schema_version) || + schema_version != kAffinityPlanSchemaVersion) { + return false; + } + + const char *socs = find_key_object(json_text, "socs"); + if (socs == nullptr) return false; + const char *socs_end = nullptr; + if (!find_json_object_end(socs, socs_end)) return false; + const char *soc_object = find_key_object_before(socs, socs_end, soc_name); + if (soc_object == nullptr) return false; + const char *soc_end = nullptr; + if (!find_json_object_end(soc_object, soc_end)) return false; + const char *devices = find_key_object_before(soc_object, soc_end, "devices"); + if (devices == nullptr) return false; + + const std::string device_key = std::to_string(device_id); + const char *devices_end = nullptr; + if (!find_json_object_end(devices, devices_end)) return false; + const char *device_object = find_key_object_before(devices, devices_end, device_key.c_str()); + if (device_object == nullptr) return false; + + out_plan.soc_name = soc_name; + out_plan.device_id = device_id; + const char *source_value = find_key_value(device_object, "plan_source"); + if (source_value == nullptr || !parse_json_string(source_value, out_plan.plan_source) || + !is_supported_plan_source(out_plan.plan_source)) { + return false; + } + const char *allowed = find_key_value(device_object, "allowed_cpus"); + if (allowed == nullptr || !parse_json_int_array(allowed, out_plan.allowed_cpus)) return false; + if (out_plan.allowed_cpus.size() < 2) return false; + { + std::vector unique = out_plan.allowed_cpus; + std::sort(unique.begin(), unique.end()); + if (std::unique(unique.begin(), unique.end()) != unique.end()) return false; + } + + const char *too_small = find_key_value(device_object, "pool_too_small"); + if (too_small != nullptr) { + (void)parse_json_bool(too_small, out_plan.pool_too_small); + } else { + out_plan.pool_too_small = out_plan.plan_source == kPoolTooSmallSource; + } + + // Schema v3 packs logical order into allowed_cpus; remap is identity. + for (int i = 0; i < kRttDieSchedSlots; ++i) + out_plan.affinity_to_logical[i] = i; + + out_plan.valid = true; + return true; +} + +bool load_affinity_plan_for_device(const char *soc_name, int32_t device_id, AicpuRttDiePlan &out_plan) { + out_plan = AicpuRttDiePlan{}; + std::string text; + if (!read_plan_json(text) || !parse_rtt_die_plan_for_device(text.c_str(), soc_name, device_id, out_plan)) { + out_plan = AicpuRttDiePlan{}; + return false; + } + return true; +} + +bool load_rtt_die_plan( + const char *soc_name, int32_t device_id, const std::vector & /*allowed_cpus*/, AicpuRttDiePlan &out_plan +) { + return load_affinity_plan_for_device(soc_name, device_id, out_plan); +} + +} // namespace pto::a5 diff --git a/src/a5/platform/onboard/host/aicpu_rtt_die_plan_reader.h b/src/a5/platform/onboard/host/aicpu_rtt_die_plan_reader.h new file mode 100644 index 0000000000..f09508f044 --- /dev/null +++ b/src/a5/platform/onboard/host/aicpu_rtt_die_plan_reader.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ + +#pragma once + +#include "common/sched_aicore_assignment.h" + +#include +#include +#include + +namespace pto::a5 { + +// Authoritative per-device AICPU affinity plan. allowed_cpus is ordered as +// [S0..Sk, O] with logical S0/S1 owning die0 and S2/S3 owning die1 when k==4. +// affinity_to_logical is identity for schema v3 (kept for ABI compatibility). +struct AicpuRttDiePlan { + std::string soc_name; + std::string plan_source; + int32_t device_id{-1}; + std::vector allowed_cpus; + int32_t affinity_to_logical[kRttDieSchedSlots]{0, 1, 2, 3}; + bool pool_too_small{false}; + bool valid{false}; +}; + +bool parse_rtt_die_plan_for_device( + const char *json_text, const char *soc_name, int32_t device_id, AicpuRttDiePlan &out_plan +); + +// Load by (soc, device_id). allowed_cpus argument is ignored for matching in +// schema v3 (plan is authoritative); retained for call-site compatibility. +bool load_rtt_die_plan( + const char *soc_name, int32_t device_id, const std::vector &allowed_cpus, AicpuRttDiePlan &out_plan +); + +bool load_affinity_plan_for_device(const char *soc_name, int32_t device_id, AicpuRttDiePlan &out_plan); + +} // namespace pto::a5 diff --git a/src/a5/platform/onboard/host/aicpu_topology_probe.cpp b/src/a5/platform/onboard/host/aicpu_topology_probe.cpp index 56c16e627d..4f1725a9b2 100644 --- a/src/a5/platform/onboard/host/aicpu_topology_probe.cpp +++ b/src/a5/platform/onboard/host/aicpu_topology_probe.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -764,6 +765,7 @@ bool build_aicpu_launch_plan( out_plan.allowed_cpus.clear(); return false; } + return true; } diff --git a/src/a5/platform/onboard/host/aicpu_topology_probe.h b/src/a5/platform/onboard/host/aicpu_topology_probe.h index 44a436617e..fe7d1573ea 100644 --- a/src/a5/platform/onboard/host/aicpu_topology_probe.h +++ b/src/a5/platform/onboard/host/aicpu_topology_probe.h @@ -113,11 +113,10 @@ bool load_cpu_topo_from_json( // * `n_orch` — number of orchestrator threads (currently always 1) // // Output: -// * `out_allowed_cpus` — n_sched + n_orch cpu_ids, ordered as -// [sched 0..n_sched-1, orch 0..n_orch-1]. The on-device gate uses -// this as `ALLOWED_CPUS[]`; the index in this array IS the deterministic -// `exec_idx` the surviving thread receives, so the role assignment in -// `aicpu_executor.cpp` (sched / orch) is fully driven by the order here. +// * `out_allowed_cpus` — n_sched + n_orch cpu_ids as [selected sched..., orch]. +// Affinity array position is the initial exec_idx. Host may then apply a +// preflight-generated affinity→logical map before balanced contiguous +// AICore ownership on device. // // Placement policy: // Step 1 (sched): smallest containing unit wins — diff --git a/src/a5/platform/onboard/host/device_runner.cpp b/src/a5/platform/onboard/host/device_runner.cpp index c8259fe927..e6c7e99a39 100644 --- a/src/a5/platform/onboard/host/device_runner.cpp +++ b/src/a5/platform/onboard/host/device_runner.cpp @@ -29,12 +29,15 @@ #include #include +#include +#include #include #include #include #include #include "aicpu_topology_probe.h" +#include "aicpu_rtt_die_plan_reader.h" #include "callable.h" #include "callable_protocol.h" #include "call_config.h" @@ -46,7 +49,42 @@ namespace { constexpr const char *kAicpuTopologyQueryName = "simpler_aicpu_query_topology"; + +bool affinity_auto_preflight_enabled() { + const char *env = std::getenv("SIMPLER_AFFINITY_PREFLIGHT_AUTO"); + if (env == nullptr || env[0] == '\0') return true; + return std::strcmp(env, "0") != 0 && std::strcmp(env, "false") != 0 && std::strcmp(env, "off") != 0; +} + +bool run_auto_affinity_preflight(int device_id) { + if (!affinity_auto_preflight_enabled()) return false; + char command[512]; + std::snprintf( + command, sizeof(command), + "python -m simpler_setup.tools.rtt_die_preflight --device %d --probe --plan-source auto-first-run", device_id + ); + LOG_INFO("Affinity plan miss for device %d; running first-run preflight: %s", device_id, command); + const int rc = std::system(command); + if (rc != 0) { + LOG_WARN("Affinity first-run preflight failed for device %d (rc=%d)", device_id, rc); + return false; + } + return true; +} + +int popcount_u64(uint64_t value) { +#if defined(__GNUC__) || defined(__clang__) + return __builtin_popcountll(value); +#else + int count = 0; + while (value != 0) { + count += static_cast(value & 1u); + value >>= 1; + } + return count; +#endif } +} // namespace // dep_gen has two shapes, one per orchestration site, and each runtime provides // the strong symbols for the one it uses: @@ -234,11 +272,48 @@ int DeviceRunner::query_aicpu_topology(pto::a5::AicpuTopology &out) { return 0; } +bool DeviceRunner::load_cached_rtt_die_plan( + const char *soc_name, const std::vector & /*allowed_cpus*/, pto::a5::AicpuRttDiePlan &out +) { + const std::string soc = soc_name == nullptr ? std::string{} : std::string(soc_name); + const bool cache_matches = rtt_die_plan_cache_checked_ && rtt_die_plan_cache_soc_ == soc && + rtt_die_plan_cache_device_id_ == device_id_; + if (!cache_matches) { + rtt_die_plan_cache_checked_ = true; + rtt_die_plan_cache_soc_ = soc; + rtt_die_plan_cache_device_id_ = device_id_; + rtt_die_plan_cache_allowed_cpus_.clear(); + rtt_die_plan_cache_ = {}; + if (!soc.empty()) { + pto::a5::load_affinity_plan_for_device(soc.c_str(), device_id_, rtt_die_plan_cache_); + if (!rtt_die_plan_cache_.valid && run_auto_affinity_preflight(device_id_)) { + pto::a5::load_affinity_plan_for_device(soc.c_str(), device_id_, rtt_die_plan_cache_); + } + } + if (rtt_die_plan_cache_.valid) { + rtt_die_plan_cache_allowed_cpus_ = rtt_die_plan_cache_.allowed_cpus; + } else { + LOG_WARN( + "Affinity plan missing/invalid for soc=%s device=%d; falling back to OCCUPY/topo contiguous " + "selection. Run: python -m simpler_setup.tools.rtt_die_preflight --device %d --probe", + soc.empty() ? "(unknown)" : soc.c_str(), device_id_, device_id_ + ); + } + } + out = rtt_die_plan_cache_; + return out.valid; +} + void DeviceRunner::clear_aicpu_topology_cache() { aicpu_device_occupancy_cached_ = false; aicpu_device_occupancy_ = {}; aicpu_topology_cached_ = false; aicpu_topology_ = {}; + rtt_die_plan_cache_checked_ = false; + rtt_die_plan_cache_soc_.clear(); + rtt_die_plan_cache_device_id_ = -1; + rtt_die_plan_cache_allowed_cpus_.clear(); + rtt_die_plan_cache_ = {}; } void DeviceRunner::set_dep_gen_enabled(bool enable) { @@ -320,13 +395,12 @@ int DeviceRunner::prepare_execution( resolve_task_binary_addrs(runtime); - // 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). + // a5-specific: prefer the authoritative per-device affinity plan + // (handshake orch + COND die packing). On miss, attempt first-run + // preflight; on failure fall back to OCCUPY/topo contiguous selection. // 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. + // is the orchestrator. Logical S0/S1 own die0 and S2/S3 own die1 when + // active==5. { pto::a5::AicpuTopology topology; runtime.set_aicpu_allowed_cpu_count(0); @@ -334,19 +408,86 @@ int DeviceRunner::prepare_execution( LOG_ERROR("AICPU topology probe failed; affinity gate will not launch"); return PTO_RUNTIME_ERR_INTERNAL; } - 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() + + const char *soc = topology.soc_name.empty() ? nullptr : topology.soc_name.c_str(); + std::vector allowed; + int32_t launch_count = 0; + bool used_affinity_plan = false; + runtime.set_rtt_die_plan_valid(false); + for (int i = 0; i < kRttDieSchedSlots; ++i) + runtime.set_rtt_affinity_to_logical(i, i); + + const bool want_default_five = + (automatic_aicpu_num || requested_aicpu_num == PLATFORM_DEFAULT_AICPU_THREAD_NUM); + pto::a5::AicpuRttDiePlan affinity_plan; + if (want_default_five && load_cached_rtt_die_plan(soc, /*allowed_cpus=*/{}, affinity_plan) && + affinity_plan.allowed_cpus.size() >= 2) { + allowed = affinity_plan.allowed_cpus; + used_affinity_plan = true; + // Identity remap: plan already packs logical order into allowed_cpus. + for (int i = 0; i < kRttDieSchedSlots; ++i) + runtime.set_rtt_affinity_to_logical(i, i); + const int32_t sched_count = + static_cast(allowed.size()) > 1 ? static_cast(allowed.size()) - 1 : 1; + runtime.set_rtt_die_plan_valid(sched_count == kRttDieSchedSlots && !affinity_plan.pool_too_small); + std::string plan_dump; + for (size_t i = 0; i < allowed.size(); ++i) { + if (i) plan_dump += ", "; + plan_dump += std::to_string(allowed[i]); + if (i + 1 == allowed.size()) plan_dump += "(orch)"; + } + LOG_INFO( + "Affinity plan loaded: soc=%s device=%d source=%s allowed=[%s] " + "(logical S0/S1->die0, S2/S3->die1)", + soc == nullptr ? "(unknown)" : soc, device_id_, affinity_plan.plan_source.c_str(), plan_dump.c_str() ); - return PTO_RUNTIME_ERR_INTERNAL; } - const auto &allowed = launch_plan.allowed_cpus; - active_aicpu_num = launch_plan.effective_active_count; + + if (!used_affinity_plan) { + 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 PTO_RUNTIME_ERR_INTERNAL; + } + allowed = launch_plan.allowed_cpus; + launch_count = launch_plan.launch_count; + if (launch_plan.warn_cpu_topology_unavailable) { + LOG_WARN( + "AICPU CPU_TOPO unavailable; using %s contiguous fallback: soc=%s occupy=0x%llx " + "stable_reachable=%d requested=%d effective=%d", + 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, launch_plan.effective_active_count + ); + } + if (launch_plan.warn_stable_reachable_below_default) { + LOG_WARN( + "AICPU stable reachable CPUs below active capacity: soc=%s stable_reachable=%d capacity=%d", + topology.soc_name.empty() ? "(unknown)" : topology.soc_name.c_str(), + launch_plan.stable_reachable_count, PLATFORM_DEFAULT_AICPU_THREAD_NUM + ); + } + } + + if (launch_count <= 0) { + if (topology.device_occupancy.occupy_valid) { + launch_count = popcount_u64(topology.device_occupancy.occupy); + } else { + launch_count = static_cast(topology.os_schedulable_cpus.size()); + } + if (launch_count < static_cast(allowed.size())) { + launch_count = static_cast(allowed.size()); + } + } + + active_aicpu_num = static_cast(allowed.size()); runtime.set_aicpu_thread_num(active_aicpu_num); { const size_t cap = runtime.aicpu_allowed_cpus_capacity(); @@ -358,41 +499,17 @@ int DeviceRunner::prepare_execution( for (size_t i = 0; i < allowed.size(); ++i) allowed_cpus[i] = allowed[i]; runtime.set_aicpu_allowed_cpu_count(static_cast(allowed.size())); - runtime.set_aicpu_launch_count(launch_plan.launch_count); + runtime.set_aicpu_launch_count(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] (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() + "AICPU ALLOWED_CPUS = [%s] (source=%s active=%d launch=%d user_cpus=%zu)", dump.c_str(), + used_affinity_plan ? "affinity-plan" : pto::a5::aicpu_scenario_name(topology.scenario_type), + active_aicpu_num, launch_count, topology.os_schedulable_cpus.size() ); } } diff --git a/src/a5/platform/onboard/host/device_runner.h b/src/a5/platform/onboard/host/device_runner.h index 075b0dbd9c..81f6c3f6a2 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_rtt_die_plan_reader.h" #include "aicpu_topology_probe.h" // KernelArgsHelper is defined in @@ -281,15 +282,25 @@ class DeviceRunner : public DeviceRunnerBase { int query_aicpu_device_occupancy(pto::a5::AicpuDeviceOccupancy &out); int query_aicpu_topology(pto::a5::AicpuTopology &out); + bool load_cached_rtt_die_plan( + const char *soc_name, const std::vector &allowed_cpus, pto::a5::AicpuRttDiePlan &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. + // one DeviceRunner attach/reset lifetime. Affinity plan lookup caches both + // hits and misses for one exact (soc, device_id) key; on miss the runner + // may auto-run first-time preflight. Allowed CPU selection prefers the + // authoritative plan, then falls back to OCCUPY/topo contiguous packing. + // Recovery, reset, and finalize clear all three values. bool aicpu_device_occupancy_cached_{false}; pto::a5::AicpuDeviceOccupancy aicpu_device_occupancy_{}; bool aicpu_topology_cached_{false}; pto::a5::AicpuTopology aicpu_topology_{}; + bool rtt_die_plan_cache_checked_{false}; + std::string rtt_die_plan_cache_soc_; + int32_t rtt_die_plan_cache_device_id_{-1}; + std::vector rtt_die_plan_cache_allowed_cpus_; + pto::a5::AicpuRttDiePlan rtt_die_plan_cache_{}; int init_pmu( int num_cores, int num_threads, const std::string &csv_path, PmuEventType event_type, int device_id, diff --git a/src/a5/runtime/host_build_graph/aicpu/aicpu_executor.cpp b/src/a5/runtime/host_build_graph/aicpu/aicpu_executor.cpp index bdb0bc0fe0..04ddde5993 100644 --- a/src/a5/runtime/host_build_graph/aicpu/aicpu_executor.cpp +++ b/src/a5/runtime/host_build_graph/aicpu/aicpu_executor.cpp @@ -36,6 +36,7 @@ #include "aicpu/platform_aicpu_affinity.h" #include "aicpu/platform_regs.h" #include "common/platform_config.h" +#include "common/sched_aicore_assignment.h" #include "common/memory_barrier.h" #include "utils/thread_completion_gate.h" @@ -236,6 +237,19 @@ int32_t AicpuExecutor::init(Runtime *runtime) { aicore_lifecycle_.handshake_partition(runtime, tidx, nthreads); } + // Optional Host JSON plan remaps affinity→logical for subsequent dispatch + // (balanced contiguous ownership when the plan is missing/invalid). + const int32_t sched_thread_count = nthreads > 1 ? nthreads - 1 : nthreads; + const int32_t mapped_logical = + tidx >= 0 && tidx < kRttDieSchedSlots ? runtime->get_rtt_affinity_to_logical(tidx) : tidx; + const int32_t dispatch_tidx = + logical_sched_for_affinity(runtime->get_rtt_die_plan_valid(), sched_thread_count, tidx, mapped_logical); + if (!init_failed_.load(std::memory_order_acquire) && tidx < sched_thread_count && dispatch_tidx != tidx) { + platform_aicpu_affinity_set_thread_idx(dispatch_tidx); + } + // `tidx` remains the affinity identity for the full-core publish/release + // partitions below; only the thread-local dispatch identity is remapped. + // Barrier 1: every thread discovers its core slice before the leader builds // and publishes the topology-dependent execution configuration. hs_arrived_.fetch_add(1, std::memory_order_acq_rel); diff --git a/src/a5/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp b/src/a5/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp index 80f894434f..a32fe9bd23 100644 --- a/src/a5/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp +++ b/src/a5/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp @@ -23,6 +23,7 @@ #include "common/memory_barrier.h" #include "common/chip_swimlane_profiling.h" #include "common/platform_config.h" +#include "common/sched_aicore_assignment.h" #include "host_build_graph/runtime_status.h" #include "host_build_graph/runtime_core.h" #include "host_build_graph/shared_memory.h" @@ -295,9 +296,8 @@ void SchedulerContext::log_stall_diagnostics( } // CLUSTER lines: one per cluster this thread owns. - // cluster_id = local_cluster_idx * active_sched_threads_ + thread_idx, matching the - // round-robin assignment in assign_cores_to_threads. int32_t ast = active_sched_threads_ > 0 ? active_sched_threads_ : aicpu_thread_num_; + const int32_t cluster_begin = contiguous_cluster_begin(thread_idx, aic_count_, ast); for (int32_t cli = 0; cli < tracker.get_cluster_count() && cli < STALL_DUMP_CORE_MAX; cli++) { int32_t offset = cli * 3; int32_t aic_id = tracker.get_aic_core_id(offset); @@ -306,7 +306,7 @@ void SchedulerContext::log_stall_diagnostics( bool aic_idle = tracker.is_aic_core_idle(offset); bool aiv0_idle = tracker.is_aiv0_core_idle(offset); bool aiv1_idle = tracker.is_aiv1_core_idle(offset); - int32_t cluster_id = cli * ast + thread_idx; + int32_t cluster_id = cluster_begin + cli; char aic_buf[192], aiv0_buf[192], aiv1_buf[192]; format_core_status( aic_buf, sizeof(aic_buf), aic_id, aic_idle, &core_exec_states_[aic_id], core_exec_states_[aic_id].reg_addr @@ -688,11 +688,10 @@ void SchedulerContext::handshake_partition(Runtime *runtime, int32_t tidx, int32 } // ============================================================================= -// Assign discovered cores to scheduler threads (cluster-aligned round-robin). +// Assign discovered cores to scheduler threads (balanced contiguous ranges). // ============================================================================= bool SchedulerContext::assign_cores_to_threads() { - // Cluster-aligned round-robin assignment: cluster ci -> sched thread ci % active_sched_threads_. - // Each cluster = 1 AIC + 2 adjacent AIV; the triple is always kept together. + // Cluster-aligned assignment: each cluster = 1 AIC + 2 adjacent AIV. // // 3S+1P: the last AICPU thread is the core-less resolution thread (P); cores // partition across the remaining (aicpu_thread_num_ - 1) scheduler threads @@ -720,17 +719,17 @@ bool SchedulerContext::assign_cores_to_threads() { } LOG_INFO( - "Assigning cores (round-robin): %d clusters across %d sched threads (%d AIC, %d AIV)", cluster_count, - active_sched_threads_, aic_count_, aiv_count_ + "Assigning cores (balanced contiguous): %d clusters across %d sched threads " + "(%d AIC, %d AIV)", + cluster_count, active_sched_threads_, aic_count_, aiv_count_ ); // running_reg_task_id / pending_reg_task_id for every serviced core are reset // in handshake_partition's sweep. - // Count clusters per thread first (round-robin may distribute unevenly) int32_t clusters_per_thread[MAX_AICPU_THREADS] = {}; for (int32_t ci = 0; ci < cluster_count; ci++) { - clusters_per_thread[ci % active_sched_threads_]++; + clusters_per_thread[contiguous_sched_for_cluster(ci, cluster_count, active_sched_threads_)]++; } for (int32_t i = 0; i < active_sched_threads_; i++) { core_trackers_[i].init(clusters_per_thread[i]); @@ -739,7 +738,7 @@ bool SchedulerContext::assign_cores_to_threads() { int32_t cluster_idx_per_thread[MAX_AICPU_THREADS] = {}; for (int32_t ci = 0; ci < cluster_count; ci++) { - int32_t t = ci % active_sched_threads_; + const int32_t t = contiguous_sched_for_cluster(ci, cluster_count, active_sched_threads_); int32_t aic_wid = aic_worker_ids_[ci]; int32_t aiv0_wid = aiv_worker_ids_[2 * ci]; diff --git a/src/a5/runtime/host_build_graph/runtime/scheduler/scheduler_context.h b/src/a5/runtime/host_build_graph/runtime/scheduler/scheduler_context.h index 6ea49fea24..b40e45bc89 100644 --- a/src/a5/runtime/host_build_graph/runtime/scheduler/scheduler_context.h +++ b/src/a5/runtime/host_build_graph/runtime/scheduler/scheduler_context.h @@ -270,7 +270,7 @@ class SchedulerContext { // Core management (scheduler_cold_path.cpp) // ========================================================================= - // Assign discovered cores (cluster = 1 AIC + 2 AIV) round-robin across scheduler threads. + // Assign discovered cores (cluster = 1 AIC + 2 AIV) in balanced contiguous segments. bool assign_cores_to_threads(); // Emergency shutdown: broadcast exit signal to every handshake'd core and diff --git a/src/a5/runtime/tensormap_and_ringbuffer/aicpu/aicpu_executor.cpp b/src/a5/runtime/tensormap_and_ringbuffer/aicpu/aicpu_executor.cpp index 0698651a86..cc69205901 100644 --- a/src/a5/runtime/tensormap_and_ringbuffer/aicpu/aicpu_executor.cpp +++ b/src/a5/runtime/tensormap_and_ringbuffer/aicpu/aicpu_executor.cpp @@ -50,6 +50,7 @@ #include "aicpu/platform_aicpu_affinity.h" #include "aicpu/platform_regs.h" #include "common/platform_config.h" +#include "common/sched_aicore_assignment.h" // Core type definitions #include "common/core_type.h" @@ -273,16 +274,19 @@ int32_t AicpuExecutor::init(Runtime *runtime) { } const int32_t hs_nthreads = decouple_orch ? (nthreads - 1) : nthreads; - // Barrier-free scheduler init (the decoupled default). Each scheduler thread - // handshakes exactly the clusters it will dispatch to (blocked-layout - // ownership: cluster ci = {ci, N/3+2ci, N/3+2ci+1}, owned by ci % hs_nthreads) - // and self-assigns them, then returns straight to run(). With no all-thread - // barrier a thread starts dispatching to its own cores as soon as they come - // up, independent of peers still handshaking. hs_nthreads == active_sched_threads_ - // in this branch, so handshake ownership matches assign_own_clusters'. + // Barrier-free scheduler init (the decoupled default). Optional Host JSON + // plan remaps affinity->logical before contiguous ownership; without a plan + // ownership is balanced contiguous (no per-run RTT probe). if (decouple_orch) { - sched_ctx_.handshake_owned_clusters(runtime, tidx, hs_nthreads); - sched_ctx_.assign_own_clusters(tidx); + const int32_t mapped_logical = + tidx >= 0 && tidx < kRttDieSchedSlots ? runtime->get_rtt_affinity_to_logical(tidx) : tidx; + const int32_t ownership_tidx = + logical_sched_for_affinity(runtime->get_rtt_die_plan_valid(), hs_nthreads, tidx, mapped_logical); + if (ownership_tidx != tidx) { + platform_aicpu_affinity_set_thread_idx(ownership_tidx); + } + sched_ctx_.handshake_owned_clusters(runtime, ownership_tidx, hs_nthreads); + sched_ctx_.assign_own_clusters(ownership_tidx); #if SIMPLER_DFX // Profiling subsystems (pmu/dump/dep) need every core's physical_core_id, // so gate their one-time leader init behind a barrier — DFX builds only. @@ -328,7 +332,30 @@ int32_t AicpuExecutor::init(Runtime *runtime) { hs_arrived_.fetch_add(1, std::memory_order_acq_rel); if (is_leader) { while (hs_arrived_.load(std::memory_order_acquire) < hs_nthreads) {} + if (sched_ctx_.handshake_failed()) { + sched_ctx_.abort_and_shutdown(runtime); + init_failed_.store(true, std::memory_order_release); + init_done_.store(true, std::memory_order_release); + return -1; + } finished_count_.store(0, std::memory_order_release); + } else { + while (hs_arrived_.load(std::memory_order_acquire) < hs_nthreads) {} + if (sched_ctx_.handshake_failed()) { + init_failed_.store(true, std::memory_order_release); + return -1; + } + } + + const int32_t mapped_logical = + tidx >= 0 && tidx < kRttDieSchedSlots ? runtime->get_rtt_affinity_to_logical(tidx) : tidx; + const int32_t dispatch_tidx = + logical_sched_for_affinity(runtime->get_rtt_die_plan_valid(), sched_thread_num_, tidx, mapped_logical); + if (dispatch_tidx != tidx) { + platform_aicpu_affinity_set_thread_idx(dispatch_tidx); + } + + if (is_leader) { if (sched_ctx_.post_handshake_init(runtime) != 0) { init_failed_.store(true, std::memory_order_release); init_done_.store(true, std::memory_order_release); diff --git a/src/a5/runtime/tensormap_and_ringbuffer/runtime/runtime.h b/src/a5/runtime/tensormap_and_ringbuffer/runtime/runtime.h index d2a2428981..989c793036 100644 --- a/src/a5/runtime/tensormap_and_ringbuffer/runtime/runtime.h +++ b/src/a5/runtime/tensormap_and_ringbuffer/runtime/runtime.h @@ -41,6 +41,7 @@ #include "common/core_type.h" #include "common/host_api.h" #include "common/platform_config.h" +#include "common/sched_aicore_assignment.h" #include "aicpu/platform_aicpu_affinity.h" // MAX_GATE_THREADS (aicpu_allowed_cpus bound) #include "dispatch_payload.h" #include "task_args.h" @@ -57,6 +58,7 @@ // Default ready queue shards: one shard per worker thread (total minus orchestrator) constexpr int RUNTIME_DEFAULT_READY_QUEUE_SHARDS = PLATFORM_MAX_AICPU_THREADS - 1; +constexpr int RUNTIME_RTT_DIE_SCHED_SLOTS = kRttDieSchedSlots; // ============================================================================= // Data Structures @@ -192,6 +194,12 @@ struct alignas(64) DeviceRuntimeLaunchDesc { // popcount(OCCUPY) via the topology probe. See the matching field in // src/common/host_build_graph/runtime.h for rationale. int32_t aicpu_launch_count; + // When non-zero, Host loaded a valid aicpu_rtt_die_plan.json for this + // soc/device; device adopts rtt_affinity_to_logical[affinity] as public + // thread_idx then uses balanced contiguous AICore ranges. When zero, + // device still uses balanced contiguous assignment (no per-run RTT probe). + int32_t rtt_die_plan_valid; + int32_t rtt_affinity_to_logical[RUNTIME_RTT_DIE_SCHED_SLOTS]; // kernel binary resolution: kernel_id -> GM function_bin_addr mapping uint64_t func_id_to_addr_[RUNTIME_MAX_FUNC_ID]; @@ -261,6 +269,16 @@ class Runtime { void set_aicpu_allowed_cpu_count(int32_t n) { dev.aicpu_allowed_cpu_count = n; } int32_t get_aicpu_launch_count() const { return dev.aicpu_launch_count; } void set_aicpu_launch_count(int32_t n) { dev.aicpu_launch_count = n; } + bool get_rtt_die_plan_valid() const { return dev.rtt_die_plan_valid != 0; } + void set_rtt_die_plan_valid(bool valid) { dev.rtt_die_plan_valid = valid ? 1 : 0; } + int32_t get_rtt_affinity_to_logical(int32_t affinity_idx) const { + if (affinity_idx < 0 || affinity_idx >= RUNTIME_RTT_DIE_SCHED_SLOTS) return affinity_idx; + return dev.rtt_affinity_to_logical[affinity_idx]; + } + void set_rtt_affinity_to_logical(int32_t affinity_idx, int32_t logical_idx) { + if (affinity_idx < 0 || affinity_idx >= RUNTIME_RTT_DIE_SCHED_SLOTS) return; + dev.rtt_affinity_to_logical[affinity_idx] = logical_idx; + } int32_t *get_aicpu_allowed_cpus() { return dev.aicpu_allowed_cpus; } size_t aicpu_allowed_cpus_capacity() const { return sizeof(dev.aicpu_allowed_cpus) / sizeof(dev.aicpu_allowed_cpus[0]); diff --git a/src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp b/src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp index 76991f2af0..e76e9603a2 100644 --- a/src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp +++ b/src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp @@ -24,6 +24,7 @@ #include "common/memory_barrier.h" #include "common/chip_swimlane_profiling.h" #include "common/platform_config.h" +#include "common/sched_aicore_assignment.h" #include "runtime_core.h" #include "shared_memory.h" #include "runtime.h" @@ -312,9 +313,8 @@ void SchedulerContext::log_stall_diagnostics( } // CLUSTER lines: one per cluster this thread owns. - // cluster_id = local_cluster_idx * active_sched_threads_ + thread_idx, matching the - // round-robin assignment in assign_cores_to_threads. int32_t ast = active_sched_threads_ > 0 ? active_sched_threads_ : aicpu_thread_num_; + const int32_t cluster_begin = contiguous_cluster_begin(thread_idx, aic_count_, ast); for (int32_t cli = 0; cli < tracker.get_cluster_count() && cli < STALL_DUMP_CORE_MAX; cli++) { int32_t offset = cli * PLATFORM_CORES_PER_BLOCKDIM; int32_t aic_id = tracker.get_aic_core_id(offset); @@ -323,7 +323,7 @@ void SchedulerContext::log_stall_diagnostics( bool aic_idle = tracker.is_aic_core_idle(offset); bool aiv0_idle = tracker.is_aiv0_core_idle(offset); bool aiv1_idle = tracker.is_aiv1_core_idle(offset); - int32_t cluster_id = cli * ast + thread_idx; + int32_t cluster_id = cluster_begin + cli; char aic_buf[128], aiv0_buf[128], aiv1_buf[128]; format_core_status( aic_buf, sizeof(aic_buf), aic_id, aic_idle, &core_exec_states_[aic_id], core_exec_states_[aic_id].reg_addr @@ -796,8 +796,8 @@ void SchedulerContext::handshake_partition(Runtime *runtime, int32_t tidx, int32 // Handshake exactly the cores this scheduler thread will later manage. Blocked // core layout ([0,N/3) AIC, [N/3,N) AIV) makes ownership predictable before -// handshake: cluster ci = {ci, N/3+2ci, N/3+2ci+1}, assigned to thread -// ci % active_threads. Same protocol as handshake_partition, but over the owned +// handshake: cluster ci = {ci, N/3+2ci, N/3+2ci+1}, assigned to thread via +// balanced contiguous ranges. Same protocol as handshake_partition, but over the owned // set instead of a contiguous slice. void SchedulerContext::handshake_owned_clusters(Runtime *runtime, int32_t tidx, int32_t active_threads) { Handshake *all_handshakes = reinterpret_cast(runtime->dev.workers); @@ -805,7 +805,8 @@ void SchedulerContext::handshake_owned_clusters(Runtime *runtime, int32_t tidx, int32_t owned[RUNTIME_MAX_WORKER]; int32_t own_n = 0; - for (int32_t ci = tidx; ci < aic_n; ci += active_threads) { + for (int32_t ci = 0; ci < aic_n; ++ci) { + if (!cluster_owned_by_contiguous_sched(ci, tidx, aic_n, active_threads)) continue; owned[own_n++] = ci; // AIC owned[own_n++] = aic_n + 2 * ci; // AIV0 owned[own_n++] = aic_n + 2 * ci + 1; // AIV1 @@ -894,11 +895,11 @@ void SchedulerContext::handshake_owned_clusters(Runtime *runtime, int32_t tidx, } // ============================================================================= -// Per-thread self-assignment (barrier-free init). Thread tidx owns the clusters -// ci with ci % active_sched_threads_ == tidx (same round-robin as -// assign_cores_to_threads), and the blocked layout gives their worker ids -// directly, so a thread populates its own CoreTracker + per-core sub_block_id -// right after handshaking its own clusters, with no all-thread barrier. +// Per-thread self-assignment (barrier-free init). Thread tidx owns the +// balanced contiguous range of clusters (same as assign_cores_to_threads), and the +// blocked layout gives their worker ids directly, so a thread populates its own +// CoreTracker + per-core sub_block_id right after handshaking its own clusters, +// with no all-thread barrier. // ============================================================================= void SchedulerContext::assign_own_clusters(int32_t tidx) { const int32_t aic_n = cores_total_num_ / PLATFORM_CORES_PER_BLOCKDIM; @@ -906,8 +907,10 @@ void SchedulerContext::assign_own_clusters(int32_t tidx) { CoreTracker &tracker = core_trackers_[tidx]; int32_t own_n = 0; - for (int32_t ci = tidx; ci < aic_n; ci += active) + for (int32_t ci = 0; ci < aic_n; ++ci) { + if (!cluster_owned_by_contiguous_sched(ci, tidx, aic_n, active)) continue; own_n++; + } // Mirrors the check assign_cores_to_threads() makes on the serial path. A // thread owning more clusters than CoreTracker can hold used to write past // core_id_map_ into the next tracker, which is the orchestrator's on the @@ -924,7 +927,8 @@ void SchedulerContext::assign_own_clusters(int32_t tidx) { tracker.init(own_n); int32_t local = 0; - for (int32_t ci = tidx; ci < aic_n; ci += active) { + for (int32_t ci = 0; ci < aic_n; ++ci) { + if (!cluster_owned_by_contiguous_sched(ci, tidx, aic_n, active)) continue; tracker.set_cluster(local++, ci, aic_n + 2 * ci, aic_n + 2 * ci + 1); } @@ -989,10 +993,9 @@ void SchedulerContext::post_handshake_profiling_init() { } // ============================================================================= -// Assign discovered cores to scheduler threads (cluster-aligned round-robin). +// Assign discovered cores to scheduler threads (balanced contiguous ranges). // ============================================================================= bool SchedulerContext::assign_cores_to_threads() { - // Cluster-aligned round-robin assignment: cluster ci -> sched thread ci % active_sched_threads_. // Each cluster = 1 AIC + 2 adjacent AIV; the triple is always kept together. active_sched_threads_ = (sched_thread_num_ > 0) ? sched_thread_num_ : aicpu_thread_num_; int32_t cluster_count = aic_count_; @@ -1007,33 +1010,29 @@ bool SchedulerContext::assign_cores_to_threads() { } LOG_INFO( - "Assigning cores (round-robin): %d clusters across %d sched threads (%d AIC, %d AIV)", cluster_count, - active_sched_threads_, aic_count_, aiv_count_ + "Assigning cores (balanced contiguous): %d clusters across %d sched threads " + "(%d AIC, %d AIV)", + cluster_count, active_sched_threads_, aic_count_, aiv_count_ ); // running_reg_task_id / pending_reg_task_id for every serviced core are reset // in handshake_partition's sweep. - // Count clusters per thread first (round-robin may distribute unevenly) int32_t clusters_per_thread[MAX_AICPU_THREADS] = {}; for (int32_t ci = 0; ci < cluster_count; ci++) { - clusters_per_thread[ci % active_sched_threads_]++; + clusters_per_thread[contiguous_sched_for_cluster(ci, cluster_count, active_sched_threads_)]++; } for (int32_t i = 0; i < active_sched_threads_; i++) { core_trackers_[i].init(clusters_per_thread[i]); } int32_t cluster_idx_per_thread[MAX_AICPU_THREADS] = {}; - for (int32_t ci = 0; ci < cluster_count; ci++) { - int32_t t = ci % active_sched_threads_; - + int32_t t = contiguous_sched_for_cluster(ci, cluster_count, active_sched_threads_); int32_t aic_wid = aic_worker_ids_[ci]; int32_t aiv0_wid = aiv_worker_ids_[2 * ci]; int32_t aiv1_wid = aiv_worker_ids_[2 * ci + 1]; - core_trackers_[t].set_cluster(cluster_idx_per_thread[t]++, aic_wid, aiv0_wid, aiv1_wid); - LOG_DEBUG("Thread %d: cluster %d (AIC=%d, AIV0=%d, AIV1=%d)", t, ci, aic_wid, aiv0_wid, aiv1_wid); } diff --git a/src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h b/src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h index cdaca3caa5..e69d3b27b9 100644 --- a/src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h +++ b/src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h @@ -71,10 +71,8 @@ class SchedulerContext { // (partitioned by tidx/nthreads). Each core is touched by exactly one thread. void handshake_partition(Runtime *runtime, int32_t tidx, int32_t nthreads); // Handshake exactly the cores this scheduler thread will later manage: - // clusters {tidx, tidx+active, ...}, cluster ci = - // {ci, N/3+2ci, N/3+2ci+1} (blocked layout: [0,N/3) AIC, [N/3,N) AIV). Matches - // assign_cores_to_threads' round-robin so handshake warms the same - // core_exec_states_ the thread later dispatches from. + // balanced contiguous range of clusters for tidx (after optional Host JSON adopt). + // cluster ci = {ci, N/3+2ci, N/3+2ci+1} (blocked layout). void handshake_owned_clusters(Runtime *runtime, int32_t tidx, int32_t active_threads); // Barrier-free counterpart of assign_cores_to_threads: thread tidx populates // its own CoreTracker + per-core sub_block_id for the clusters it owns, right @@ -219,7 +217,7 @@ class SchedulerContext { // Core management (scheduler_cold_path.cpp) // ========================================================================= - // Assign discovered cores (cluster = 1 AIC + 2 AIV) round-robin across scheduler threads. + // Assign discovered cores (cluster = 1 AIC + 2 AIV) as balanced contiguous ranges. bool assign_cores_to_threads(); // Emergency shutdown: broadcast exit signal to every handshake'd core and diff --git a/src/a5/runtime/tensormap_and_ringbuffer/runtime/shared/runtime.cpp b/src/a5/runtime/tensormap_and_ringbuffer/runtime/shared/runtime.cpp index cbc6fa1b62..97730bbd5b 100644 --- a/src/a5/runtime/tensormap_and_ringbuffer/runtime/shared/runtime.cpp +++ b/src/a5/runtime/tensormap_and_ringbuffer/runtime/shared/runtime.cpp @@ -37,6 +37,10 @@ Runtime::Runtime() { memset(dev.aicpu_allowed_cpus, 0, sizeof(dev.aicpu_allowed_cpus)); dev.aicpu_allowed_cpu_count = 0; dev.aicpu_launch_count = 0; + dev.rtt_die_plan_valid = 0; + for (int i = 0; i < RUNTIME_RTT_DIE_SCHED_SLOTS; ++i) { + dev.rtt_affinity_to_logical[i] = i; + } dev.serial_orch_sched = false; dev.gm_sm_ptr_ = nullptr; dev.orch_args_storage_.clear(); diff --git a/src/common/host_build_graph/runtime.h b/src/common/host_build_graph/runtime.h index 9f7db8f963..bce7a9d512 100644 --- a/src/common/host_build_graph/runtime.h +++ b/src/common/host_build_graph/runtime.h @@ -51,6 +51,7 @@ // Default number of ready-queue shards. constexpr int RUNTIME_DEFAULT_READY_QUEUE_SHARDS = PLATFORM_MAX_AICPU_THREADS - 1; +constexpr int RUNTIME_RTT_DIE_SCHED_SLOTS = 4; // ============================================================================= // Data Structures @@ -159,6 +160,10 @@ class Runtime { int32_t aicpu_allowed_cpus[MAX_GATE_THREADS]; int32_t aicpu_allowed_cpu_count; int32_t aicpu_launch_count; + // a5: non-zero when Host loaded a valid aicpu_rtt_die_plan.json; device + // adopts rtt_affinity_to_logical then balanced contiguous AICore ranges. + int32_t rtt_die_plan_valid; + int32_t rtt_affinity_to_logical[RUNTIME_RTT_DIE_SCHED_SLOTS]; // kernel binary resolution: kernel_id -> GM function_bin_addr mapping // NOTE: Made public for direct access from aicore code @@ -226,6 +231,16 @@ class Runtime { void set_aicpu_allowed_cpu_count(int32_t n) { aicpu_allowed_cpu_count = n; } int32_t get_aicpu_launch_count() const { return aicpu_launch_count; } void set_aicpu_launch_count(int32_t n) { aicpu_launch_count = n; } + bool get_rtt_die_plan_valid() const { return rtt_die_plan_valid != 0; } + void set_rtt_die_plan_valid(bool valid) { rtt_die_plan_valid = valid ? 1 : 0; } + int32_t get_rtt_affinity_to_logical(int32_t affinity_idx) const { + if (affinity_idx < 0 || affinity_idx >= RUNTIME_RTT_DIE_SCHED_SLOTS) return affinity_idx; + return rtt_affinity_to_logical[affinity_idx]; + } + void set_rtt_affinity_to_logical(int32_t affinity_idx, int32_t logical_idx) { + if (affinity_idx < 0 || affinity_idx >= RUNTIME_RTT_DIE_SCHED_SLOTS) return; + rtt_affinity_to_logical[affinity_idx] = logical_idx; + } int32_t *get_aicpu_allowed_cpus() { return aicpu_allowed_cpus; } size_t aicpu_allowed_cpus_capacity() const { return sizeof(aicpu_allowed_cpus) / sizeof(aicpu_allowed_cpus[0]); } diff --git a/src/common/host_build_graph/shared/runtime.cpp b/src/common/host_build_graph/shared/runtime.cpp index 94f6a252b5..c6acebf80b 100644 --- a/src/common/host_build_graph/shared/runtime.cpp +++ b/src/common/host_build_graph/shared/runtime.cpp @@ -34,6 +34,10 @@ Runtime::Runtime() { std::memset(aicpu_allowed_cpus, 0, sizeof(aicpu_allowed_cpus)); aicpu_allowed_cpu_count = 0; aicpu_launch_count = 0; + rtt_die_plan_valid = 0; + for (int i = 0; i < RUNTIME_RTT_DIE_SCHED_SLOTS; ++i) { + rtt_affinity_to_logical[i] = i; + } host_total_tasks = 0; sm_image_bytes = 0; diff --git a/tests/ut/cpp/CMakeLists.txt b/tests/ut/cpp/CMakeLists.txt index 77a9d9d5fc..e7be656a64 100644 --- a/tests/ut/cpp/CMakeLists.txt +++ b/tests/ut/cpp/CMakeLists.txt @@ -1070,6 +1070,7 @@ add_a2a3_runtime_test(test_a2a3_scope_stats_collector a2a3/test_scope_stats_coll # A5 tests (src/a5/runtime/tensormap_and_ringbuffer/) # --------------------------------------------------------------------------- add_a5_test(test_a5_fatal a5/test_a5_fatal.cpp) +add_a5_test(test_a5_sched_aicore_assignment a5/test_sched_aicore_assignment.cpp) # A5 trb runtime UTs — mirror of a2a3 trb runtime UTs, link against a5_rt_objs. # Target names carry the a5_ prefix because hierarchical/test_tensormap (and @@ -1324,6 +1325,7 @@ set_tests_properties(test_a2a3_aicpu_affinity_select PROPERTIES LABELS "no_hardw 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_rtt_die_plan_reader.cpp ${A5_ONBOARD_HOST_DIR}/aicpu_topology_probe.cpp ) add_custom_command(TARGET test_a5_aicpu_topology_fallback POST_BUILD diff --git a/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp b/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp index ef642c80ab..675a465aaf 100644 --- a/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp +++ b/tests/ut/cpp/a5/test_aicpu_topology_fallback.cpp @@ -16,6 +16,7 @@ #include #include +#include "aicpu_rtt_die_plan_reader.h" #include "aicpu_topology_probe.h" #include "common/platform_config.h" @@ -529,4 +530,117 @@ TEST(A5AicpuTopologySelection, KnownScenarioRejectsUnsupportedActiveCount) { EXPECT_TRUE(allowed.empty()); } +TEST(A5AicpuLaunchPlan, FiveThreadFgHasNoRuntimeRttToggle) { + const auto all = make_physical_range(0, 7); + AicpuTopology topology; + topology.source = AicpuTopologySource::kDriver; + topology.scenario_type = AicpuScenarioType::kFg; + topology.os_schedulable_cpus = primaries(all); + std::reverse(topology.os_schedulable_cpus.begin(), topology.os_schedulable_cpus.end()); + uint64_t occupy = 0; + for (const auto &cpu : topology.os_schedulable_cpus) + occupy |= 1ULL << cpu.cpu_id; + set_device_occupy(topology, occupy); + + AicpuLaunchPlan plan; + std::string error; + ASSERT_TRUE(build_aicpu_launch_plan(topology, 0, plan, error)) << error; + EXPECT_EQ(plan.effective_active_count, 5); + EXPECT_EQ(plan.allowed_cpus.size(), 5u); + const std::string json = format_aicpu_topology_json(topology, AicpuSelectionPolicy::kScenario, plan); + EXPECT_EQ(json.find("rtt_die_preflight"), std::string::npos); + EXPECT_EQ(json.find("sched_aicore_assignment_mode"), std::string::npos); + EXPECT_EQ(json.find("mode 3"), std::string::npos); +} + +TEST(A5AicpuLaunchPlan, OccupyOnlyStillBuildsFiveWhenMaskIsFive) { + AicpuTopology topology; + topology.source = AicpuTopologySource::kOccupyFallback; + topology.scenario_type = AicpuScenarioType::kUnknown; + ASSERT_TRUE(enumerate_cpus_from_occupy(0x3eU, topology.os_schedulable_cpus)); + set_device_occupy(topology, 0x3eU); + std::reverse(topology.os_schedulable_cpus.begin(), topology.os_schedulable_cpus.end()); + + AicpuLaunchPlan plan; + std::string error; + ASSERT_TRUE(build_aicpu_launch_plan(topology, 0, plan, error)) << error; + EXPECT_EQ(plan.effective_active_count, 5); + EXPECT_EQ( + std::vector(plan.allowed_cpus.begin(), plan.allowed_cpus.end() - 1), (std::vector{1, 2, 3, 4}) + ); +} + +TEST(A5AicpuLaunchPlan, EffectiveCountFourStillBuilds) { + AicpuTopology topology; + topology.source = AicpuTopologySource::kDriver; + topology.scenario_type = AicpuScenarioType::kUnknown; + topology.os_schedulable_cpus = {{1, 0, 0, 0, 0}, {3, 2, 0, 1, 0}, {5, 4, 0, 2, 1}, {7, 6, 0, 3, 1}}; + set_device_occupy(topology, (1ULL << 1) | (1ULL << 3) | (1ULL << 5) | (1ULL << 7)); + + AicpuLaunchPlan plan; + std::string error; + ASSERT_TRUE(build_aicpu_launch_plan(topology, 0, plan, error)) << error; + EXPECT_EQ(plan.effective_active_count, 4); +} + +TEST(A5AicpuRttDiePlan, ParsesSelectedSocAndDevice) { + constexpr const char *text = R"json({ + "schema_version": 3, + "socs": { + "decoy": {"devices": {"0": { + "plan_source": "manual", + "allowed_cpus": [99, 98] + }}}, + "Ascend950PR_9599": {"devices": { + "1": {"plan_source": "manual", "allowed_cpus": [8, 9]}, + "0": { + "plan_source": "atomic-flag-orch+cond-die-v1", + "allowed_cpus": [3, 4, 5, 6, 8], + "pool_too_small": false, + "schedulers": [ + {"logical_idx": 0, "cpu_id": 3, "assigned_die": 0}, + {"logical_idx": 1, "cpu_id": 4, "assigned_die": 0}, + {"logical_idx": 2, "cpu_id": 5, "assigned_die": 1}, + {"logical_idx": 3, "cpu_id": 6, "assigned_die": 1} + ] + } + }} + } + })json"; + pto::a5::AicpuRttDiePlan plan; + ASSERT_TRUE(pto::a5::parse_rtt_die_plan_for_device(text, "Ascend950PR_9599", 0, plan)); + EXPECT_TRUE(plan.valid); + EXPECT_EQ(plan.plan_source, "atomic-flag-orch+cond-die-v1"); + EXPECT_EQ(plan.allowed_cpus, (std::vector{3, 4, 5, 6, 8})); + EXPECT_EQ(plan.affinity_to_logical[0], 0); + EXPECT_EQ(plan.affinity_to_logical[1], 1); + EXPECT_EQ(plan.affinity_to_logical[2], 2); + EXPECT_EQ(plan.affinity_to_logical[3], 3); + EXPECT_FALSE(plan.pool_too_small); +} + +TEST(A5AicpuRttDiePlan, RejectsDuplicateAllowedCpus) { + constexpr const char *text = R"json({"schema_version":3,"socs":{"soc":{"devices":{"0":{ + "plan_source":"manual", + "allowed_cpus":[1,2,2,4,5] + }}}}})json"; + pto::a5::AicpuRttDiePlan plan; + EXPECT_FALSE(pto::a5::parse_rtt_die_plan_for_device(text, "soc", 0, plan)); + EXPECT_FALSE(plan.valid); +} + +TEST(A5AicpuRttDiePlan, RejectsLegacySchemaAndUntrustedSource) { + constexpr const char *legacy = R"json({"schema_version":2,"socs":{}})json"; + constexpr const char *missing_source = R"json({"schema_version":3,"socs":{"soc":{"devices":{"0":{ + "allowed_cpus":[1,2,3,4,5] + }}}}})json"; + constexpr const char *unknown_source = R"json({"schema_version":3,"socs":{"soc":{"devices":{"0":{ + "plan_source":"die-id-guess","allowed_cpus":[1,2,3,4,5] + }}}}})json"; + pto::a5::AicpuRttDiePlan plan; + EXPECT_FALSE(pto::a5::parse_rtt_die_plan_for_device(legacy, "soc", 0, plan)); + EXPECT_FALSE(pto::a5::parse_rtt_die_plan_for_device(missing_source, "soc", 0, plan)); + EXPECT_FALSE(pto::a5::parse_rtt_die_plan_for_device(unknown_source, "soc", 0, plan)); +} + } // namespace diff --git a/tests/ut/cpp/a5/test_sched_aicore_assignment.cpp b/tests/ut/cpp/a5/test_sched_aicore_assignment.cpp new file mode 100644 index 0000000000..ce8bd08472 --- /dev/null +++ b/tests/ut/cpp/a5/test_sched_aicore_assignment.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ + +#include "common/sched_aicore_assignment.h" + +#include + +#include + +TEST(SchedAicoreAssignment, ContiguousPartitionCoversWithoutOverlap) { + for (int32_t cluster_count : {28, 36}) { + constexpr int32_t kActive = 4; + std::vector owned(kActive, 0); + for (int32_t ci = 0; ci < cluster_count; ++ci) { + const int32_t sched = contiguous_sched_for_cluster(ci, cluster_count, kActive); + ASSERT_GE(sched, 0); + ASSERT_LT(sched, kActive); + ++owned[sched]; + EXPECT_TRUE(cluster_owned_by_contiguous_sched(ci, sched, cluster_count, kActive)); + } + int sum = 0; + for (int n : owned) + sum += n; + EXPECT_EQ(sum, cluster_count); + // S0/S1 take the first half, S2/S3 the second half for even splits. + EXPECT_EQ(contiguous_sched_for_cluster(0, cluster_count, kActive), 0); + EXPECT_EQ(contiguous_sched_for_cluster(cluster_count - 1, cluster_count, kActive), 3); + } +} + +TEST(SchedAicoreAssignment, ContiguousBeginIsInverse) { + constexpr int32_t kClusters = 36; + constexpr int32_t kActive = 4; + for (int32_t sched = 0; sched < kActive; ++sched) { + const int32_t begin = contiguous_cluster_begin(sched, kClusters, kActive); + const int32_t end = contiguous_cluster_begin(sched + 1, kClusters, kActive); + for (int32_t ci = begin; ci < end; ++ci) { + EXPECT_EQ(contiguous_sched_for_cluster(ci, kClusters, kActive), sched); + } + } +} + +TEST(SchedAicoreAssignment, IdentityRemapWhenPlanPacked) { + // Schema v3 packs logical order into allowed_cpus; remap stays identity. + for (int32_t i = 0; i < kRttDieSchedSlots; ++i) { + EXPECT_EQ(logical_sched_for_affinity(true, 4, i, i), i); + } + EXPECT_EQ(logical_sched_for_affinity(false, 4, 1, 3), 1); + EXPECT_EQ(logical_sched_for_affinity(true, 3, 1, 2), 1); +} + +TEST(SchedAicoreAssignment, PackPhysPicksToLogicalDieContract) { + // Phys pick order die0,die1,die1,die0 → logical [P0,P3,P1,P2] + const int32_t phys[kRttDieSchedSlots] = {10, 20, 21, 11}; + int32_t logical[kRttDieSchedSlots] = {}; + pack_phys_picks_to_logical_cpus(phys, logical); + EXPECT_EQ(logical[0], 10); + EXPECT_EQ(logical[1], 11); + EXPECT_EQ(logical[2], 20); + EXPECT_EQ(logical[3], 21); +} + +TEST(SchedAicoreAssignment, FourSchedDieHalfOwnership) { + constexpr int32_t kClusters = 36; + constexpr int32_t kActive = 4; + // First half of clusters owned by S0/S1 (die0 managers); second by S2/S3. + for (int32_t ci = 0; ci < kClusters / 2; ++ci) { + const int32_t sched = contiguous_sched_for_cluster(ci, kClusters, kActive); + EXPECT_LT(sched, 2); + } + for (int32_t ci = kClusters / 2; ci < kClusters; ++ci) { + const int32_t sched = contiguous_sched_for_cluster(ci, kClusters, kActive); + EXPECT_GE(sched, 2); + } +} diff --git a/tests/ut/py/test_rtt_die_preflight.py b/tests/ut/py/test_rtt_die_preflight.py new file mode 100644 index 0000000000..45ce61de65 --- /dev/null +++ b/tests/ut/py/test_rtt_die_preflight.py @@ -0,0 +1,186 @@ +# Copyright (c) PyPTO Contributors. +# This program is free software, you can redistribute it and/or modify it under the terms and conditions of +# CANN Open Software License Agreement Version 2.0 (the "License"). +# Please refer to the License for details. You may not use this file except in compliance with the License. +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. +# See LICENSE in the root of the software repository for the full text of the License. +# ----------------------------------------------------------------------------------------------------------- +"""Unit tests for A5 AICPU affinity preflight ranking and plan writes.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from simpler_setup.tools import rtt_die_preflight as preflight + + +def test_pick_orchestrator_min_avg_tiebreak(): + pool = [ + {"pool_idx": 0, "avg_handshake_ticks": 100}, + {"pool_idx": 1, "avg_handshake_ticks": 50}, + {"pool_idx": 2, "avg_handshake_ticks": 50}, + {"pool_idx": 3, "avg_handshake_ticks": 80}, + ] + assert preflight.pick_orchestrator(pool) == 1 + + +def test_pack_schedulers_phys_order_to_logical_die_contract(): + # Candidates after orch removal. Scores chosen so phys picks are: + # die0 best=cpu10, die1 best=cpu20, die1 2nd=cpu21, die0 2nd=cpu11 + candidates = [ + {"cpu_id": 10, "die0_sum_ticks": 100, "die1_sum_ticks": 900}, + {"cpu_id": 11, "die0_sum_ticks": 200, "die1_sum_ticks": 800}, + {"cpu_id": 20, "die0_sum_ticks": 900, "die1_sum_ticks": 100}, + {"cpu_id": 21, "die0_sum_ticks": 800, "die1_sum_ticks": 150}, + ] + allowed, schedulers = preflight.pack_schedulers_from_die_scores(candidates) + assert allowed == [10, 11, 20, 21] + assert [s["assigned_die"] for s in schedulers] == [0, 0, 1, 1] + assert [s["logical_idx"] for s in schedulers] == [0, 1, 2, 3] + + +def test_build_allowed_cpus_from_probe_full(): + probe = { + "schema_version": 3, + "measurement_method": preflight.MEASUREMENT_METHOD, + "soc_name": "Ascend950PR_9599", + "device_id": 0, + "user_pool_cpus": [3, 4, 5, 6, 7, 8], + "orch_pool_idx": 5, + "pool": [ + { + "pool_idx": 0, + "cpu_id": 3, + "avg_handshake_ticks": 40, + "die0_sum_ticks": 100, + "die1_sum_ticks": 900, + "is_orch": 0, + }, + { + "pool_idx": 1, + "cpu_id": 4, + "avg_handshake_ticks": 41, + "die0_sum_ticks": 200, + "die1_sum_ticks": 800, + "is_orch": 0, + }, + { + "pool_idx": 2, + "cpu_id": 5, + "avg_handshake_ticks": 42, + "die0_sum_ticks": 900, + "die1_sum_ticks": 100, + "is_orch": 0, + }, + { + "pool_idx": 3, + "cpu_id": 6, + "avg_handshake_ticks": 43, + "die0_sum_ticks": 800, + "die1_sum_ticks": 150, + "is_orch": 0, + }, + { + "pool_idx": 4, + "cpu_id": 7, + "avg_handshake_ticks": 44, + "die0_sum_ticks": 700, + "die1_sum_ticks": 700, + "is_orch": 0, + }, + { + "pool_idx": 5, + "cpu_id": 8, + "avg_handshake_ticks": 10, + "die0_sum_ticks": 0, + "die1_sum_ticks": 0, + "is_orch": 1, + }, + ], + } + node = preflight.build_allowed_cpus_from_probe(probe) + assert node["orch_cpu"] == 8 + assert node["allowed_cpus"][-1] == 8 + assert len(node["allowed_cpus"]) == 5 + assert node["schedulers"][0]["assigned_die"] == 0 + assert node["schedulers"][1]["assigned_die"] == 0 + assert node["schedulers"][2]["assigned_die"] == 1 + assert node["schedulers"][3]["assigned_die"] == 1 + assert node["plan_source"] == preflight.MEASUREMENT_METHOD + + +def test_build_allowed_cpus_pool_too_small(): + probe = { + "schema_version": 3, + "measurement_method": preflight.MEASUREMENT_METHOD, + "soc_name": "Ascend950PR_9599", + "device_id": 0, + "pool_too_small": True, + "user_pool_cpus": [3, 4, 5], + } + node = preflight.build_allowed_cpus_from_probe(probe) + assert node["pool_too_small"] is True + assert node["allowed_cpus"] == [3, 4, 5] + assert node["orch_cpu"] == 5 + assert node["plan_source"] == "pool-too-small-contiguous" + + +def test_validate_probe_rejects_old_schema(): + with pytest.raises(ValueError, match="schema_version"): + preflight.validate_probe_result( + { + "schema_version": 2, + "measurement_method": "serialized-cond-rtt-v2", + "soc_name": "Ascend950PR_9599", + "device_id": 0, + }, + 0, + ) + + +def test_atomic_write_and_device_buckets(tmp_path: Path): + out = tmp_path / "aicpu_affinity_plan.json" + node0 = preflight.build_device_node_from_allowed( + soc_name="Ascend950PR_9599", allowed_cpus=[3, 4, 5, 6, 7], plan_source="manual" + ) + plan = preflight.merge_device_plan( + preflight.load_plan(out), soc_name="Ascend950PR_9599", device_id=0, device_node=node0 + ) + preflight.atomic_write_plan(out, plan) + + node1 = preflight.build_device_node_from_allowed( + soc_name="Ascend950PR_9599", allowed_cpus=[10, 11, 12, 13, 14], plan_source="manual" + ) + plan = preflight.merge_device_plan( + preflight.load_plan(out), soc_name="Ascend950PR_9599", device_id=1, device_node=node1 + ) + preflight.atomic_write_plan(out, plan) + + loaded = json.loads(out.read_text(encoding="utf-8")) + assert loaded["schema_version"] == 3 + devices = loaded["socs"]["Ascend950PR_9599"]["devices"] + assert devices["0"]["allowed_cpus"] == [3, 4, 5, 6, 7] + assert devices["1"]["allowed_cpus"] == [10, 11, 12, 13, 14] + + +def test_cli_offline_allowed_cpus(tmp_path: Path): + out = tmp_path / "plan.json" + rc = preflight.main( + [ + "--device", + "2", + "--soc", + "Ascend950PR_9599", + "--allowed-cpus", + "3,4,5,6,7", + "--out", + str(out), + ] + ) + assert rc == 0 + loaded = json.loads(out.read_text(encoding="utf-8")) + assert loaded["socs"]["Ascend950PR_9599"]["devices"]["2"]["allowed_cpus"] == [3, 4, 5, 6, 7] diff --git a/tools/README.md b/tools/README.md index ab06345482..1ba51289ae 100644 --- a/tools/README.md +++ b/tools/README.md @@ -32,6 +32,23 @@ the workloads shared by both runtimes plus its matching Qwen case: `StressBatch16Seq3500` for TMR and `GraphExecutionBatch16Seq3500` for HBG. SPMD paged attention is not part of the benchmark sweep. +## rtt_die_preflight.sh + +Full AICPU affinity preflight wrapper: enumerate the user pool, elect orch via +atomic-flag handshake, score dies with COND, and atomically write/merge +`build/config/aicpu_affinity_plan.json` (schema v3). On first miss, +`DeviceRunner` auto-runs the same CLI per `device_id`; subsequent runs load the +authoritative `allowed_cpus` (logical S0/S1→die0, S2/S3→die1). Prefer the Python +module on board: + +```bash +task-submit --device auto --device-num 1 --run \ + 'python -m simpler_setup.tools.rtt_die_preflight --device "$TASK_DEVICE" --probe' + +# Business / benchmark: Host auto-loads the JSON (or auto-probes once) +bash tools/benchmark_rounds.sh -p a5 -d 0 -r tensormap_and_ringbuffer -n 100 +``` + ## verify_packaging.sh Exercises all 5 install paths × 2 entry points from a fully clean state. @@ -75,7 +92,11 @@ Runs `halGetDeviceInfo` queries from **inside an AICPU OS process** — resolves the "used in device" HAL queries (`AICPU + OS_SCHED`, `AICPU + PF_*`, etc.) that always fail from host code. Uploads a small inner SO via the same dispatcher bootstrap path the production runtime -uses; results come back through GM. Documents the resolution of the +uses; results come back through GM. Its A5 `--rtt-json` mode also launches one +kernel on every reachable AICPU, records schema-v2 raw COND RTT rounds to both +AICore dies from the four selected scheduler CPUs, and is the backend for +`rtt_die_preflight`. +Documents the resolution of the a3 AICPU 8 → 6 split and the a5 AICPU 9 → 6 split — see the tool's own [README](./cann-examples/aicpu-device-query/README.md) for build/run instructions and what it confirmed. diff --git a/tools/cann-examples/aicpu-device-query/README.md b/tools/cann-examples/aicpu-device-query/README.md index 2afe3605aa..2114ed8291 100644 --- a/tools/cann-examples/aicpu-device-query/README.md +++ b/tools/cann-examples/aicpu-device-query/README.md @@ -1,6 +1,6 @@ # aicpu-device-query -Runs `halGetDeviceInfo` queries from **inside an AICPU OS process** on the +Runs `halGetDeviceInfo` queries and the A5 full AICPU affinity preflight from **inside AICPU OS processes** on the device, using the same dispatcher bootstrap path as the production `simpler` runtime. Resolves the queries that CANN's header flags as "used in device" — `AICPU + OS_SCHED`, `AICPU + PF_*`, `CCPU/DCPU/TSCPU + @@ -74,9 +74,9 @@ broken Path B (`KERNEL_TYPE_AICPU_CUSTOM`, issue #822). | rtsBinaryLoadFromFile (JSON), rtsFuncGetByName, rtsLaunchCpuKernel v +---------------------+ -| libaicpu_query.so | inside AICPU OS process: -| (inner SO) | for each (module, infoType) request -> halGetDeviceInfo -+---------------------+ -> writes QueryResult[] to GM +| libaicpu_query.so | query: halGetDeviceInfo -> QueryResult[] +| (inner SO) | affinity: enum / handshake orch / COND die scores ++---------------------+ -> writes pool metrics to GM | | D2H aclrtMemcpy v @@ -127,17 +127,37 @@ export SIMPLER_AICPU_QUERY_SO=$REPO/tools/cann-examples/aicpu-device-query/devic task-submit --device auto --device-num 1 \ --run "$REPO/tools/cann-examples/aicpu-device-query/host/build/query_device_hal \$TASK_DEVICE" -# A5 only: merge Host CPU_TOPO with the queried device masks, classify FG/PG, -# resolve the automatic affinity plan, and write machine-readable JSON. +# A5 only: topology JSON. task-submit --device auto --device-num 1 \ --run "$REPO/tools/cann-examples/aicpu-device-query/host/build/query_device_hal \$TASK_DEVICE --json" + +# A5 only: public affinity preflight. The Python CLI invokes this tool's +# --rtt-json mode, validates every measurement, and atomically writes the plan. +task-submit --device auto --device-num 1 --run \ + 'python -m simpler_setup.tools.rtt_die_preflight --device "$TASK_DEVICE" --probe' ``` +The public Python command uses `run_query_topo.sh`, which builds both helper +artifacts under `build/cache/aicpu-device-query/`; it does not write into the +packaged tool-source directory. The explicit paths above remain useful when +developing the standalone tool directly. + `--json` keeps diagnostics on stderr and writes only the JSON document to stdout. The document includes topology source, FG/PG classification, all logical-CPU metadata, device masks, selection policy, active count, full OCCUPY launch count, and `[S..., O]` affinity. +`--rtt-json` runs the full affinity preflight (schema v3): + +1. serial `aicpu_num=1` enumeration of the user pool +2. multi-thread atomic-flag pairwise handshake (1000 iters) to elect orch +3. serialized COND die scoring for non-orch threads (100 samples/core) +4. emit raw pool metrics; Python packs phys `{die0,die1,die1,die0}` into + logical `[S0,S1,S2,S3,O]` (S0/S1→die0, S2/S3→die1) + +Pool size `< 5` emits `pool_too_small` and skips handshake/COND. Launch uses +the OCCUPY popcount so the gate sees the full user pool. + ## Running on each arch The same CMakeLists builds for a3 and a5 — only the dispatcher SO you @@ -158,6 +178,8 @@ have been validated with this tool — see "What it answered" above. and virtualization questions plus A5 classification. Extending the list to other modules / infoTypes is a small edit — `requests` vector + the `kModuleName`/`kInfoName` switches. +- The RTT mode is A5-only: it relies on the A5 per-core `halResMap` layout, + two dies of 18 AICores, and `REG_SPR_COND_OFFSET` from platform config. - The inner SO uses local device id 0 (`self_did = 0`) — validated as the correct convention from inside an AICPU OS process. Passing the host's logical device id from inside the kernel returns rc=1 for all diff --git a/tools/cann-examples/aicpu-device-query/device/CMakeLists.txt b/tools/cann-examples/aicpu-device-query/device/CMakeLists.txt index 6c0927f2ce..4183835e63 100644 --- a/tools/cann-examples/aicpu-device-query/device/CMakeLists.txt +++ b/tools/cann-examples/aicpu-device-query/device/CMakeLists.txt @@ -14,6 +14,8 @@ cmake_minimum_required(VERSION 3.16) project(aicpu_query LANGUAGES C CXX) +get_filename_component(SIMPLER_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../.." ABSOLUTE) + set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) @@ -27,6 +29,7 @@ add_library(aicpu_query SHARED aicpu_query.cpp) target_include_directories(aicpu_query PRIVATE ${ASCEND_HOME_PATH}/include + ${SIMPLER_ROOT}/src/a5/platform/include ) target_compile_options(aicpu_query PRIVATE diff --git a/tools/cann-examples/aicpu-device-query/device/aicpu_query.cpp b/tools/cann-examples/aicpu-device-query/device/aicpu_query.cpp index f7cca70a16..a80891da02 100644 --- a/tools/cann-examples/aicpu-device-query/device/aicpu_query.cpp +++ b/tools/cann-examples/aicpu-device-query/device/aicpu_query.cpp @@ -9,48 +9,30 @@ * ----------------------------------------------------------------------------------------------------------- */ // -// aicpu_query.cpp — device-side AICPU SO that runs HAL queries. -// -// Two exports: -// simpler_aicpu_init — no-op, present because LoadAicpuOp::Init resolves it -// via rtsFuncGetByName. Returns 0. -// simpler_aicpu_query — the actual workhorse: reads (module, infoType) pairs -// from a GM input buffer, calls halGetDeviceInfo for -// each, writes results to a GM output buffer. -// -// I/O contract (matches host_main.cpp's struct layout): -// KernelArgs.device_args -> &DeviceArgs (in GM) -// DeviceArgs.q_input_addr (offset 96) -> &QueryRequest[] (in GM) -// DeviceArgs.q_input_count (offset 104) -> count of requests -// DeviceArgs.q_output_addr (offset 112) -> &QueryResult[] (in GM) -// -// QueryRequest { int32 module_type; int32 info_type; } // 8 B -// QueryResult { int32 rc; int32 _pad; int64 value; } // 16 B -// -// The dispatcher bootstrap path already lands this SO at the preinstall -// directory; the host registers it via rtsBinaryLoadFromFile + invokes -// `simpler_aicpu_query` via rtsLaunchCpuKernel. +// aicpu_query.cpp — device-side AICPU SO that runs HAL queries plus the full +// affinity preflight (serial enum helper, atomic-flag handshake orch election, +// COND die scoring for non-orchestrator threads). #include #include +#include #include +#include "common/platform_config.h" +#include "../shared/rtt_probe_types.h" + namespace { constexpr uint32_t kHalSuccess = 0; -// Layout of the device_args struct we share with host. Only the three -// query-related qwords beyond the dispatcher's existing layout matter here. struct DeviceArgs { - uint64_t reserved_pre[12]; // 0..95 — unused on this path - uint64_t q_input_addr; // 96 - uint64_t q_input_count; // 104 - uint64_t q_output_addr; // 112 + uint64_t reserved_pre[12]; + uint64_t q_input_addr; + uint64_t q_input_count; + uint64_t q_output_addr; }; -// KernelArgs is the standard envelope CANN passes to AICPU kernels. -// device_args is the only field we care about. struct KernelArgs { uint64_t _pad[5]; void *device_args; @@ -77,14 +59,102 @@ constexpr int kDlogLevelError = 3; void DiagLog(const char *msg) { DlogRecord(kDlogModuleCcecpu, kDlogLevelError, "[aicpu-query] %s", msg); } +inline uint64_t SysCntAicpu() { + uint64_t value; + __asm__ volatile("mrs %0, cntvct_el0" : "=r"(value)); + return value; +} + +inline uint64_t SysCntFrequencyAicpu() { + uint64_t value; + __asm__ volatile("mrs %0, cntfrq_el0" : "=r"(value)); + return value; +} + +void TouchDie(const uint64_t *aicore_regs, uint32_t first_core, uint32_t cores_per_die, uint32_t samples_per_core) { + volatile uint32_t sink = 0; + for (uint32_t core = first_core; core < first_core + cores_per_die; ++core) { + auto *cond = reinterpret_cast(aicore_regs[core] + REG_SPR_COND_OFFSET); + for (uint32_t sample = 0; sample < samples_per_core; ++sample) { + sink = *cond; + } + } + (void)sink; +} + +uint64_t MeasureDieTotalTicks( + const uint64_t *aicore_regs, uint32_t first_core, uint32_t cores_per_die, uint32_t samples_per_core +) { + const uint64_t begin = SysCntAicpu(); + TouchDie(aicore_regs, first_core, cores_per_die, samples_per_core); + return SysCntAicpu() - begin; +} + +void SetProbeError(AffinityPreflightOutput *output, AffinityProbeError error) { + uint32_t expected = static_cast(AffinityProbeError::kNone); + (void)__atomic_compare_exchange_n( + &output->error_code, &expected, static_cast(error), false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE + ); +} + +bool WaitForProbeValue(AffinityPreflightOutput *output, uint32_t *value, uint32_t target) { + const uint64_t begin = SysCntAicpu(); + const uint64_t timeout_ticks = SysCntFrequencyAicpu() * kAffinityBarrierTimeoutSeconds; + while (__atomic_load_n(value, __ATOMIC_ACQUIRE) < target) { + if (__atomic_load_n(&output->error_code, __ATOMIC_ACQUIRE) != + static_cast(AffinityProbeError::kNone)) { + return false; + } + if (SysCntAicpu() - begin >= timeout_ticks) { + SetProbeError(output, AffinityProbeError::kBarrierTimeout); + return false; + } + __asm__ volatile("yield"); + } + return true; +} + +bool WaitEq(AffinityPreflightOutput *output, volatile uint32_t *cell, uint32_t expected) { + const uint64_t begin = SysCntAicpu(); + const uint64_t timeout_ticks = SysCntFrequencyAicpu() * kAffinityBarrierTimeoutSeconds; + while (__atomic_load_n(cell, __ATOMIC_ACQUIRE) != expected) { + if (__atomic_load_n(&output->error_code, __ATOMIC_ACQUIRE) != + static_cast(AffinityProbeError::kNone)) { + return false; + } + if (SysCntAicpu() - begin >= timeout_ticks) { + SetProbeError(output, AffinityProbeError::kBarrierTimeout); + return false; + } + __asm__ volatile("yield"); + } + return true; +} + +uint64_t HandshakePair( + AffinityPreflightOutput *output, uint32_t initiator, uint32_t responder, uint32_t my_idx, uint32_t iters +) { + volatile uint32_t *req = &output->handshake_req[initiator][responder]; + volatile uint32_t *ack = &output->handshake_ack[responder][initiator]; + const uint64_t begin = SysCntAicpu(); + for (uint32_t iter = 1; iter <= iters; ++iter) { + if (my_idx == initiator) { + __atomic_store_n(req, iter, __ATOMIC_RELEASE); + if (!WaitEq(output, ack, iter)) return 0; + } else if (my_idx == responder) { + if (!WaitEq(output, req, iter)) return 0; + __atomic_store_n(ack, iter, __ATOMIC_RELEASE); + } + } + return SysCntAicpu() - begin; +} + } // namespace extern "C" { __attribute__((visibility("default"))) int simpler_aicpu_init(void *args) { (void)args; - // No-op. LoadAicpuOp::Init resolves this symbol via rtsFuncGetByName - // and treats failure to resolve as fatal. return 0; } @@ -107,9 +177,6 @@ __attribute__((visibility("default"))) int simpler_aicpu_query(void *args) { auto *requests = reinterpret_cast(d->q_input_addr); auto *results = reinterpret_cast(d->q_output_addr); const uint64_t n = d->q_input_count; - - // Device-side HAL uses local device id 0 to mean "myself" (validated via - // the earlier kernel.cpp probe — using host's logical did fails rc=1). const uint32_t self_did = 0; for (uint64_t i = 0; i < n; ++i) { @@ -123,4 +190,176 @@ __attribute__((visibility("default"))) int simpler_aicpu_query(void *args) { return 0; } +// Phase 1 helper: launched with aicpu_num=1; records sched_getcpu(). +__attribute__((visibility("default"))) int simpler_aicpu_affinity_enum(void *args) { + if (args == nullptr) { + DiagLog("simpler_aicpu_affinity_enum: args==nullptr"); + return 1; + } + auto *kernel_args = reinterpret_cast(args); + auto *device_args = reinterpret_cast(kernel_args->device_args); + if (device_args == nullptr || device_args->output_addr == 0) { + DiagLog("simpler_aicpu_affinity_enum: invalid buffers"); + return 1; + } + auto *output = reinterpret_cast(device_args->output_addr); + output->cpu_id = sched_getcpu(); + __atomic_store_n(&output->ready, 1u, __ATOMIC_RELEASE); + return 0; +} + +__attribute__((visibility("default"))) int simpler_aicpu_affinity_preflight(void *args) { + if (args == nullptr) { + DiagLog("simpler_aicpu_affinity_preflight: args==nullptr"); + return 1; + } + auto *kernel_args = reinterpret_cast(args); + auto *device_args = reinterpret_cast(kernel_args->device_args); + if (device_args == nullptr || device_args->output_addr == 0 || device_args->aicore_regs_addr == 0 || + device_args->user_pool_addr == 0) { + DiagLog("simpler_aicpu_affinity_preflight: invalid buffers"); + return 1; + } + const uint32_t pool_count = device_args->pool_count; + if (pool_count < 2 || pool_count > kAffinityMaxPool || device_args->aicore_count == 0 || + device_args->aicore_count % PLATFORM_NUM_DIES != 0 || device_args->handshake_iters == 0 || + device_args->samples_per_core == 0) { + DiagLog("simpler_aicpu_affinity_preflight: invalid topology"); + return 1; + } + + const int cpu_id = sched_getcpu(); + const auto *user_pool = reinterpret_cast(device_args->user_pool_addr); + int32_t pool_idx = -1; + for (uint32_t idx = 0; idx < pool_count; ++idx) { + if (user_pool[idx] == cpu_id) { + pool_idx = static_cast(idx); + break; + } + } + if (pool_idx < 0) return 0; + + auto *output = reinterpret_cast(device_args->output_addr); + const auto *aicore_regs = reinterpret_cast(device_args->aicore_regs_addr); + const uint32_t cores_per_die = device_args->aicore_count / PLATFORM_NUM_DIES; + const uint32_t iters = device_args->handshake_iters; + + const uint32_t bit = 1u << static_cast(pool_idx); + const uint32_t previous = __atomic_fetch_or(&output->claimed_mask, bit, __ATOMIC_ACQ_REL); + if ((previous & bit) != 0) { + SetProbeError(output, AffinityProbeError::kDuplicatePool); + return 1; + } + + AffinityPoolSlot &slot = output->slots[pool_idx]; + slot.pool_idx = pool_idx; + slot.cpu_id = cpu_id; + + __atomic_add_fetch(&output->ready_count, 1u, __ATOMIC_ACQ_REL); + if (!WaitForProbeValue(output, &output->ready_count, pool_count)) return 1; + if (pool_idx == 0) { + __atomic_store_n(&output->pool_count, pool_count, __ATOMIC_RELEASE); + } + + // Phase 2: pairwise atomic-flag handshake for every unordered pair. + uint64_t handshake_sum = 0; + uint32_t handshake_pairs = 0; + for (uint32_t i = 0; i < pool_count; ++i) { + for (uint32_t j = i + 1; j < pool_count; ++j) { + const uint64_t ticks = HandshakePair(output, i, j, static_cast(pool_idx), iters); + if (__atomic_load_n(&output->error_code, __ATOMIC_ACQUIRE) != + static_cast(AffinityProbeError::kNone)) { + return 1; + } + if (static_cast(pool_idx) == i || static_cast(pool_idx) == j) { + const uint64_t avg = ticks / static_cast(iters); + handshake_sum += avg; + ++handshake_pairs; + if (static_cast(pool_idx) == i) { + // Store avg+1 so a true zero average still publishes. + const uint64_t published_avg = avg + 1; + __atomic_store_n( + &output->handshake_pair_ticks[i * kAffinityMaxPool + j], published_avg, __ATOMIC_RELEASE + ); + __atomic_store_n( + &output->handshake_pair_ticks[j * kAffinityMaxPool + i], published_avg, __ATOMIC_RELEASE + ); + } + } + // All threads wait until the pair's matrix entry is published. + volatile uint64_t *published = &output->handshake_pair_ticks[i * kAffinityMaxPool + j]; + const uint64_t begin = SysCntAicpu(); + const uint64_t timeout_ticks = SysCntFrequencyAicpu() * kAffinityBarrierTimeoutSeconds; + while (__atomic_load_n(published, __ATOMIC_ACQUIRE) == 0) { + if (__atomic_load_n(&output->error_code, __ATOMIC_ACQUIRE) != + static_cast(AffinityProbeError::kNone)) { + return 1; + } + if (SysCntAicpu() - begin >= timeout_ticks) { + SetProbeError(output, AffinityProbeError::kBarrierTimeout); + return 1; + } + __asm__ volatile("yield"); + } + } + } + + slot.avg_handshake_ticks = + handshake_pairs > 0 ? handshake_sum / static_cast(handshake_pairs) : UINT64_MAX; + __atomic_store_n(&slot.handshake_valid, 1u, __ATOMIC_RELEASE); + + for (uint32_t idx = 0; idx < pool_count; ++idx) { + if (!WaitForProbeValue(output, &output->slots[idx].handshake_valid, 1u)) return 1; + } + + // Elect orch: minimum average handshake latency; tie-break on smaller pool_idx. + if (pool_idx == 0) { + uint32_t best = 0; + uint64_t best_avg = output->slots[0].avg_handshake_ticks; + for (uint32_t idx = 1; idx < pool_count; ++idx) { + const uint64_t avg = output->slots[idx].avg_handshake_ticks; + if (avg < best_avg || (avg == best_avg && idx < best)) { + best = idx; + best_avg = avg; + } + } + output->slots[best].is_orch = 1; + __atomic_store_n(&output->orch_pool_idx, best, __ATOMIC_RELEASE); + __atomic_store_n(&output->handshake_done, 1u, __ATOMIC_RELEASE); + } + if (!WaitForProbeValue(output, &output->handshake_done, 1u)) return 1; + const uint32_t orch_idx = __atomic_load_n(&output->orch_pool_idx, __ATOMIC_ACQUIRE); + + // Phase 3: serial COND die scoring for non-orch threads. + for (uint32_t turn = 0; turn < pool_count; ++turn) { + if (turn == orch_idx) { + if (static_cast(pool_idx) == turn) { + slot.die0_sum_ticks = 0; + slot.die1_sum_ticks = 0; + __atomic_store_n(&slot.die_valid, 1u, __ATOMIC_RELEASE); + } + } else if (static_cast(pool_idx) == turn) { + TouchDie(aicore_regs, 0, cores_per_die, kAffinityWarmupSamplesPerCore); + TouchDie(aicore_regs, cores_per_die, cores_per_die, kAffinityWarmupSamplesPerCore); + slot.die0_sum_ticks = + MeasureDieTotalTicks(aicore_regs, 0, cores_per_die, device_args->samples_per_core); + slot.die1_sum_ticks = + MeasureDieTotalTicks(aicore_regs, cores_per_die, cores_per_die, device_args->samples_per_core); + __atomic_store_n(&slot.die_valid, 1u, __ATOMIC_RELEASE); + } + if (!WaitForProbeValue(output, &output->slots[turn].die_valid, 1u)) return 1; + } + + if (pool_idx == 0) { + __atomic_store_n(&output->die_done, 1u, __ATOMIC_RELEASE); + } + if (!WaitForProbeValue(output, &output->die_done, 1u)) return 1; + return 0; +} + +// Back-compat alias used by older host descriptors. +__attribute__((visibility("default"))) int simpler_aicpu_rtt_probe(void *args) { + return simpler_aicpu_affinity_preflight(args); +} + } // extern "C" diff --git a/tools/cann-examples/aicpu-device-query/host/CMakeLists.txt b/tools/cann-examples/aicpu-device-query/host/CMakeLists.txt index f38813bae2..0922acc0d6 100644 --- a/tools/cann-examples/aicpu-device-query/host/CMakeLists.txt +++ b/tools/cann-examples/aicpu-device-query/host/CMakeLists.txt @@ -18,7 +18,7 @@ if(NOT DEFINED ENV{ASCEND_HOME_PATH}) endif() set(ASCEND_HOME_PATH $ENV{ASCEND_HOME_PATH}) -add_executable(query_device_hal query_device_hal.cpp) +add_executable(query_device_hal query_device_hal.cpp rtt_probe_host.cpp) get_filename_component(SIMPLER_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../.." ABSOLUTE) set(A5_TOPOLOGY_DIR "${SIMPLER_ROOT}/src/a5/platform/onboard/host") 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 2505e80f3a..2c9ae01d00 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 @@ -25,8 +25,7 @@ // libaicpu_extend_kernels dlopens dispatcher, runs DynInit which writes // inner SO bytes to /usr/lib64/aicpu_kernels/0/aicpu_kernels_device/simpler_inner__.so // 6. aclrtSynchronizeStream -// 7. generate JSON descriptor pointing at preinstall path with two ops -// (simpler_aicpu_init + simpler_aicpu_query), fingerprint-suffixed opTypes +// 7. generate JSON descriptor pointing at the preinstall path // 8. rtsBinaryLoadFromFile(json_path, cpuKernelMode=0) // 9. rtsFuncGetByName("simpler_aicpu_query_") -> query func handle // 10. populate DeviceArgs with q_input_addr/count/output_addr (and reset header) @@ -44,6 +43,7 @@ #include "aicpu_topology_probe.h" #include "host_log.h" +#include "rtt_probe_host.h" #include #include @@ -276,7 +276,7 @@ std::string MakePreinstallPath(uint64_t fp, int device_id) { return buf; } -std::string MakeJsonDescriptor(uint64_t fp, const std::string &so_basename) { +std::string MakeJsonDescriptor(uint64_t fp, const std::string &so_basename, bool include_rtt_probe) { char init_op[128], query_op[128]; std::snprintf(init_op, sizeof(init_op), "simpler_aicpu_init_%016lx", fp); std::snprintf(query_op, sizeof(query_op), "simpler_aicpu_query_%016lx", fp); @@ -304,6 +304,14 @@ std::string MakeJsonDescriptor(uint64_t fp, const std::string &so_basename) { s += entry(init_op, "simpler_aicpu_init"); s += ",\n"; s += entry(query_op, "simpler_aicpu_query"); + if (include_rtt_probe) { + s += ",\n"; + s += aicpu_device_query::MakeAffinityEnumDescriptorEntry(fp, so_basename); + s += ",\n"; + s += aicpu_device_query::MakeAffinityPreflightDescriptorEntry(fp, so_basename); + s += ",\n"; + s += aicpu_device_query::MakeRttDescriptorEntry(fp, so_basename); + } s += "\n}\n"; return s; } @@ -311,15 +319,18 @@ std::string MakeJsonDescriptor(uint64_t fp, const std::string &so_basename) { } // namespace int main(int argc, char **argv) { - if (argc < 2 || argc > 3 || (argc == 3 && std::strcmp(argv[2], "--json") != 0)) { - std::fprintf(stderr, "usage: %s [--json]\n", argv[0]); + if (argc < 2 || argc > 3 || + (argc == 3 && std::strcmp(argv[2], "--json") != 0 && std::strcmp(argv[2], "--rtt-json") != 0)) { + std::fprintf(stderr, "usage: %s [--json|--rtt-json]\n", argv[0]); return 1; } // This executable owns its logger state; no loader binds it. HostLogger::get_instance().set_level(simpler::log::LogLevel::TIMING); int device_id = std::atoi(argv[1]); - const bool json_output = argc == 3; + const bool json_output = argc == 3 && std::strcmp(argv[2], "--json") == 0; + const bool rtt_output = argc == 3 && std::strcmp(argv[2], "--rtt-json") == 0; + const bool machine_output = json_output || rtt_output; const char *dispatcher_path_env = std::getenv("SIMPLER_DISPATCHER_SO"); std::string dispatcher_path = @@ -424,13 +435,13 @@ int main(int argc, char **argv) { return std::string(b); }(); std::string preinstall_path = MakePreinstallPath(fp, device_id); - if (!json_output) std::printf("[bootstrap] inner SO at %s (fp=%016lx)\n", preinstall_path.c_str(), fp); + if (!machine_output) std::printf("[bootstrap] inner SO at %s (fp=%016lx)\n", preinstall_path.c_str(), fp); char json_path_buf[128]; std::snprintf(json_path_buf, sizeof(json_path_buf), "/tmp/simpler_inner_%016lx_%d.json", fp, getpid()); std::string json_path = json_path_buf; { - std::string json = MakeJsonDescriptor(fp, so_basename); + std::string json = MakeJsonDescriptor(fp, so_basename, rtt_output); std::ofstream f(json_path); if (!f.is_open()) { std::fprintf(stderr, "open %s failed\n", json_path.c_str()); @@ -502,7 +513,7 @@ int main(int argc, char **argv) { "D2H QueryResult" ); - if (json_output) { + if (machine_output) { pto::a5::AicpuDeviceOccupancy occupancy; occupancy.os_sched = static_cast(results[kOsSchedRequestIndex].value); occupancy.os_sched_valid = results[kOsSchedRequestIndex].rc == 0; @@ -517,29 +528,29 @@ int main(int argc, char **argv) { return 1; } if (topology.soc_name.find("Ascend950") == std::string::npos) { - std::fprintf(stderr, "--json A5 classification is unsupported for SoC %s\n", topology.soc_name.c_str()); + std::fprintf(stderr, "A5 classification is unsupported for SoC %s\n", topology.soc_name.c_str()); return 1; } - pto::a5::AicpuLaunchPlan launch_plan; - std::string plan_error; - if (!pto::a5::build_aicpu_launch_plan(topology, 0, launch_plan, plan_error)) { - std::fprintf(stderr, "A5 AICPU launch planning failed: %s\n", plan_error.c_str()); + if (json_output) { + pto::a5::AicpuLaunchPlan launch_plan; + std::string plan_error; + if (!pto::a5::build_aicpu_launch_plan(topology, 0, launch_plan, plan_error)) { + std::fprintf(stderr, "A5 AICPU launch planning failed: %s\n", plan_error.c_str()); + return 1; + } + pto::a5::AicpuSelectionPolicy policy = pto::a5::AicpuSelectionPolicy::kScenario; + if (topology.generic_selection_only) { + policy = pto::a5::AicpuSelectionPolicy::kGeneric; + } else if (topology.scenario_type == pto::a5::AicpuScenarioType::kUnknown) { + policy = pto::a5::AicpuSelectionPolicy::kSequentialFallback; + } + std::fputs(pto::a5::format_aicpu_topology_json(topology, policy, launch_plan).c_str(), stdout); + } else if (!aicpu_device_query::RunAffinityPreflight( + device_id, binary_handle, fp, dev_args.ptr, kDeviceArgsBytes, stream, topology + )) { return 1; } - - pto::a5::AicpuSelectionPolicy policy = pto::a5::AicpuSelectionPolicy::kScenario; - if (topology.generic_selection_only) { - policy = pto::a5::AicpuSelectionPolicy::kGeneric; - } else if (topology.scenario_type == pto::a5::AicpuScenarioType::kUnknown) { - policy = pto::a5::AicpuSelectionPolicy::kSequentialFallback; - std::fprintf( - stderr, - "WARNING: AICPU topology UNKNOWN; selected %d active roles from %d stable CPUs; launch count=%d\n", - launch_plan.effective_active_count, launch_plan.stable_reachable_count, launch_plan.launch_count - ); - } - std::fputs(pto::a5::format_aicpu_topology_json(topology, policy, launch_plan).c_str(), stdout); } else { // ---- Pretty-print ---- std::printf("\n=== device=%d device-side HAL view (via dispatcher + inner SO) ===\n", device_id); diff --git a/tools/cann-examples/aicpu-device-query/host/rtt_probe_host.cpp b/tools/cann-examples/aicpu-device-query/host/rtt_probe_host.cpp new file mode 100644 index 0000000000..ed6f6e9a27 --- /dev/null +++ b/tools/cann-examples/aicpu-device-query/host/rtt_probe_host.cpp @@ -0,0 +1,373 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ + +#include "rtt_probe_host.h" + +#include "aicpu_topology_probe.h" +#include "common/acl_hal_device.h" +#include "common/platform_config.h" +#include "host/host_regs.h" +#include "../shared/rtt_probe_types.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace aicpu_device_query { +namespace { + +class DeviceBuffer { +public: + DeviceBuffer() = default; + DeviceBuffer(const DeviceBuffer &) = delete; + DeviceBuffer &operator=(const DeviceBuffer &) = delete; + + ~DeviceBuffer() { + if (ptr_ != nullptr) aclrtFree(ptr_); + } + + bool Allocate(size_t bytes, const char *description) { + const aclError rc = aclrtMalloc(&ptr_, bytes, ACL_MEM_MALLOC_HUGE_FIRST); + if (rc == ACL_SUCCESS) return true; + ptr_ = nullptr; + std::fprintf(stderr, "aclrtMalloc failed: %d (%s)\n", static_cast(rc), description); + return false; + } + + void *get() const { return ptr_; } + +private: + void *ptr_{nullptr}; +}; + +bool CheckAcl(aclError rc, const char *call, const char *description) { + if (rc == ACL_SUCCESS) return true; + std::fprintf(stderr, "%s failed: %d (%s)\n", call, static_cast(rc), description); + return false; +} + +bool MapAicoreRegisters(int device_id, std::vector *registers) { + using HalResMapFn = int (*)(uint32_t, struct res_map_info *, uint64_t *, uint32_t *); + auto hal_res_map = reinterpret_cast(dlsym(nullptr, "halResMap")); + if (hal_res_map == nullptr) { + std::fprintf(stderr, "halResMap not found: %s\n", dlerror()); + return false; + } + + const auto physical_device_id = static_cast(pto::acl_to_hal_device_id(device_id)); + struct res_map_info map_info = {}; + map_info.target_proc_type = PROCESS_CP1; + map_info.res_type = RES_AICORE; + registers->assign(DAV_3510::PLATFORM_MAX_PHYSICAL_CORES, 0); + for (uint32_t core = 0; core < DAV_3510::PLATFORM_MAX_PHYSICAL_CORES; ++core) { + map_info.res_id = core; + uint32_t length = REG_AICORE_MAP_SIZE; + const int rc = hal_res_map(physical_device_id, &map_info, &(*registers)[core], &length); + if (rc != 0) { + std::fprintf(stderr, "halResMap failed for AICore %u: %d\n", core, rc); + return false; + } + } + return true; +} + +bool ResolveNamed(void *binary_handle, uint64_t fingerprint, const char *base_name, rtFuncHandle *handle) { + char op_type[128]; + std::snprintf(op_type, sizeof(op_type), "%s_%016lx", base_name, fingerprint); + const rtError_t rc = rtsFuncGetByName(binary_handle, op_type, handle); + if (rc == RT_ERROR_NONE) return true; + std::fprintf(stderr, "rtsFuncGetByName %s failed: %d\n", base_name, static_cast(rc)); + return false; +} + +bool LaunchCpu(rtFuncHandle handle, uint32_t block_dim, void *device_args, aclrtStream stream) { + struct LaunchArgs { + uint64_t pad[5] = {0}; + uint64_t device_args_ptr = 0; + uint64_t reserved[20] = {0}; + } launch_args = {}; + launch_args.device_args_ptr = reinterpret_cast(device_args); + + rtCpuKernelArgs_t cpu_args = {}; + cpu_args.baseArgs.args = &launch_args; + cpu_args.baseArgs.argsSize = sizeof(launch_args); + rtLaunchKernelAttr_t attr = {}; + rtKernelLaunchCfg_t cfg = {&attr, 0}; + const rtError_t rc = rtsLaunchCpuKernel(handle, block_dim, stream, &cfg, &cpu_args); + if (rc == RT_ERROR_NONE) return true; + std::fprintf(stderr, "rtsLaunchCpuKernel failed: %d\n", static_cast(rc)); + return false; +} + +std::string MakeDescriptorEntry( + uint64_t fingerprint, const std::string &so_basename, const char *op_type_base, const char *function_name +) { + char op_type[128]; + std::snprintf(op_type, sizeof(op_type), "%s_%016lx", op_type_base, fingerprint); + std::string descriptor = " \""; + descriptor += op_type; + descriptor += "\": {\n \"opInfo\": {\n"; + descriptor += " \"functionName\": \""; + descriptor += function_name; + descriptor += "\",\n \"kernelSo\": \""; + descriptor += so_basename; + descriptor += "\",\n \"opKernelLib\": \"AICPUKernel\",\n"; + descriptor += " \"computeCost\": \"100\",\n \"engine\": \"DNN_VM_AICPU\",\n"; + descriptor += " \"flagAsync\": \"False\",\n \"flagPartial\": \"False\",\n"; + descriptor += " \"userDefined\": \"False\"\n }\n }"; + return descriptor; +} + +std::vector EnumerateUserPool( + rtFuncHandle enum_handle, void *device_args, size_t device_args_bytes, aclrtStream stream, + const pto::a5::AicpuTopology &topology +) { + std::vector occupy_cpus; + for (const auto &cpu : topology.os_schedulable_cpus) { + occupy_cpus.push_back(cpu.cpu_id); + } + std::sort(occupy_cpus.begin(), occupy_cpus.end()); + occupy_cpus.erase(std::unique(occupy_cpus.begin(), occupy_cpus.end()), occupy_cpus.end()); + + DeviceBuffer enum_out; + if (!enum_out.Allocate(sizeof(AffinityEnumOutput), "affinity enum output")) return {}; + + std::set seen; + const int attempts = static_cast(occupy_cpus.size()) * 3 + 4; + for (int attempt = 0; attempt < attempts; ++attempt) { + AffinityEnumOutput host_out = {}; + if (!CheckAcl( + aclrtMemcpy( + enum_out.get(), sizeof(host_out), &host_out, sizeof(host_out), ACL_MEMCPY_HOST_TO_DEVICE + ), + "aclrtMemcpy", "zero enum output" + )) { + return {}; + } + std::vector host_args(device_args_bytes, 0); + auto *args = reinterpret_cast(host_args.data()); + args->output_addr = reinterpret_cast(enum_out.get()); + if (!CheckAcl( + aclrtMemcpy( + device_args, device_args_bytes, host_args.data(), device_args_bytes, ACL_MEMCPY_HOST_TO_DEVICE + ), + "aclrtMemcpy", "enum device args" + )) { + return {}; + } + if (!LaunchCpu(enum_handle, 1u, device_args, stream)) return {}; + if (!CheckAcl(aclrtSynchronizeStream(stream), "aclrtSynchronizeStream", "enum sync")) return {}; + if (!CheckAcl( + aclrtMemcpy( + &host_out, sizeof(host_out), enum_out.get(), sizeof(host_out), ACL_MEMCPY_DEVICE_TO_HOST + ), + "aclrtMemcpy", "enum D2H" + )) { + return {}; + } + if (host_out.ready == 0 || host_out.cpu_id < 0) continue; + seen.insert(host_out.cpu_id); + if (!occupy_cpus.empty() && seen.size() >= occupy_cpus.size()) break; + } + + std::vector pool(seen.begin(), seen.end()); + if (pool.empty()) pool = occupy_cpus; + return pool; +} + +std::string FormatAffinityJson( + int device_id, const pto::a5::AicpuTopology &topology, const std::vector &user_pool, + const AffinityPreflightOutput &output +) { + std::ostringstream json; + json << "{\n \"schema_version\": 3,\n" + << " \"measurement_method\": \"atomic-flag-orch+cond-die-v1\",\n" + << " \"soc_name\": \"" << topology.soc_name << "\",\n" + << " \"device_id\": " << device_id << ",\n" + << " \"handshake_iters\": " << kAffinityHandshakeIters << ",\n" + << " \"samples_per_core\": " << kAffinitySamplesPerCore << ",\n" + << " \"user_pool_cpus\": ["; + for (size_t i = 0; i < user_pool.size(); ++i) { + if (i) json << ", "; + json << user_pool[i]; + } + json << "],\n \"orch_pool_idx\": " << output.orch_pool_idx << ",\n \"pool\": [\n"; + for (uint32_t idx = 0; idx < output.pool_count; ++idx) { + const AffinityPoolSlot &slot = output.slots[idx]; + if (idx) json << ",\n"; + json << " {\"pool_idx\": " << slot.pool_idx << ", \"cpu_id\": " << slot.cpu_id + << ", \"avg_handshake_ticks\": " << slot.avg_handshake_ticks + << ", \"die0_sum_ticks\": " << slot.die0_sum_ticks << ", \"die1_sum_ticks\": " << slot.die1_sum_ticks + << ", \"is_orch\": " << slot.is_orch << "}"; + } + json << "\n ]\n}\n"; + return json.str(); +} + +} // namespace + +std::string MakeRttDescriptorEntry(uint64_t fingerprint, const std::string &so_basename) { + return MakeDescriptorEntry(fingerprint, so_basename, "simpler_aicpu_rtt_probe", "simpler_aicpu_rtt_probe"); +} + +std::string MakeAffinityEnumDescriptorEntry(uint64_t fingerprint, const std::string &so_basename) { + return MakeDescriptorEntry(fingerprint, so_basename, "simpler_aicpu_affinity_enum", "simpler_aicpu_affinity_enum"); +} + +std::string MakeAffinityPreflightDescriptorEntry(uint64_t fingerprint, const std::string &so_basename) { + return MakeDescriptorEntry( + fingerprint, so_basename, "simpler_aicpu_affinity_preflight", "simpler_aicpu_affinity_preflight" + ); +} + +bool RunAffinityPreflight( + int device_id, void *binary_handle, uint64_t fingerprint, void *device_args, size_t device_args_bytes, + aclrtStream stream, const pto::a5::AicpuTopology &topology +) { + rtFuncHandle enum_handle = nullptr; + rtFuncHandle preflight_handle = nullptr; + if (!ResolveNamed(binary_handle, fingerprint, "simpler_aicpu_affinity_enum", &enum_handle)) return false; + if (!ResolveNamed(binary_handle, fingerprint, "simpler_aicpu_affinity_preflight", &preflight_handle)) { + // Fall back to legacy symbol name if present. + if (!ResolveNamed(binary_handle, fingerprint, "simpler_aicpu_rtt_probe", &preflight_handle)) return false; + } + + const std::vector user_pool = + EnumerateUserPool(enum_handle, device_args, device_args_bytes, stream, topology); + if (user_pool.size() < 2) { + std::fprintf(stderr, "affinity preflight needs at least 2 user CPUs; got %zu\n", user_pool.size()); + return false; + } + if (user_pool.size() > kAffinityMaxPool) { + std::fprintf(stderr, "affinity preflight pool %zu exceeds max %u\n", user_pool.size(), kAffinityMaxPool); + return false; + } + if (user_pool.size() < 5) { + // Emit a shrink-only probe result so Python can skip full packing. + std::ostringstream json; + json << "{\n \"schema_version\": 3,\n" + << " \"measurement_method\": \"atomic-flag-orch+cond-die-v1\",\n" + << " \"soc_name\": \"" << topology.soc_name << "\",\n" + << " \"device_id\": " << device_id << ",\n" + << " \"pool_too_small\": true,\n" + << " \"user_pool_cpus\": ["; + for (size_t i = 0; i < user_pool.size(); ++i) { + if (i) json << ", "; + json << user_pool[i]; + } + json << "]\n}\n"; + std::fputs(json.str().c_str(), stdout); + return true; + } + + std::vector aicore_registers; + if (!MapAicoreRegisters(device_id, &aicore_registers)) return false; + + DeviceBuffer pool_buf, regs_buf, out_buf; + if (!pool_buf.Allocate(user_pool.size() * sizeof(int32_t), "user pool") || + !regs_buf.Allocate(aicore_registers.size() * sizeof(uint64_t), "aicore regs") || + !out_buf.Allocate(sizeof(AffinityPreflightOutput), "preflight output")) { + return false; + } + AffinityPreflightOutput zero_out = {}; + if (!CheckAcl( + aclrtMemcpy( + pool_buf.get(), user_pool.size() * sizeof(int32_t), user_pool.data(), + user_pool.size() * sizeof(int32_t), ACL_MEMCPY_HOST_TO_DEVICE + ), + "aclrtMemcpy", "H2D pool" + ) || + !CheckAcl( + aclrtMemcpy( + regs_buf.get(), aicore_registers.size() * sizeof(uint64_t), aicore_registers.data(), + aicore_registers.size() * sizeof(uint64_t), ACL_MEMCPY_HOST_TO_DEVICE + ), + "aclrtMemcpy", "H2D regs" + ) || + !CheckAcl( + aclrtMemcpy( + out_buf.get(), sizeof(zero_out), &zero_out, sizeof(zero_out), ACL_MEMCPY_HOST_TO_DEVICE + ), + "aclrtMemcpy", "H2D output" + )) { + return false; + } + + std::vector host_args(device_args_bytes, 0); + auto *args = reinterpret_cast(host_args.data()); + args->output_addr = reinterpret_cast(out_buf.get()); + args->aicore_regs_addr = reinterpret_cast(regs_buf.get()); + args->user_pool_addr = reinterpret_cast(pool_buf.get()); + args->pool_count = static_cast(user_pool.size()); + args->aicore_count = DAV_3510::PLATFORM_MAX_PHYSICAL_CORES; + args->handshake_iters = kAffinityHandshakeIters; + args->samples_per_core = kAffinitySamplesPerCore; + if (!CheckAcl( + aclrtMemcpy( + device_args, device_args_bytes, host_args.data(), device_args_bytes, ACL_MEMCPY_HOST_TO_DEVICE + ), + "aclrtMemcpy", "preflight device args" + )) { + return false; + } + + const uint32_t launch_count = static_cast( + topology.device_occupancy.occupy_valid ? + __builtin_popcountll(topology.device_occupancy.occupy) : + user_pool.size() + ); + if (!LaunchCpu(preflight_handle, launch_count, device_args, stream)) return false; + if (!CheckAcl(aclrtSynchronizeStream(stream), "aclrtSynchronizeStream", "preflight sync")) return false; + + AffinityPreflightOutput output = {}; + if (!CheckAcl( + aclrtMemcpy(&output, sizeof(output), out_buf.get(), sizeof(output), ACL_MEMCPY_DEVICE_TO_HOST), + "aclrtMemcpy", "preflight D2H" + )) { + return false; + } + if (output.error_code != static_cast(AffinityProbeError::kNone)) { + std::fprintf(stderr, "affinity preflight device error_code=%u\n", output.error_code); + return false; + } + if (output.pool_count != user_pool.size() || output.handshake_done == 0 || output.die_done == 0) { + std::fprintf(stderr, "affinity preflight incomplete output\n"); + return false; + } + // Normalize pair ticks (device stored avg+1). + for (uint32_t i = 0; i < output.pool_count; ++i) { + for (uint32_t j = 0; j < output.pool_count; ++j) { + uint64_t &cell = output.handshake_pair_ticks[i * kAffinityMaxPool + j]; + if (cell > 0) --cell; + } + } + + std::fputs(FormatAffinityJson(device_id, topology, user_pool, output).c_str(), stdout); + return true; +} + +bool RunRttProbe( + int device_id, void *binary_handle, uint64_t fingerprint, void *device_args, size_t device_args_bytes, + aclrtStream stream, const pto::a5::AicpuTopology &topology, const pto::a5::AicpuLaunchPlan & +) { + return RunAffinityPreflight( + device_id, binary_handle, fingerprint, device_args, device_args_bytes, stream, topology + ); +} + +} // namespace aicpu_device_query diff --git a/tools/cann-examples/aicpu-device-query/host/rtt_probe_host.h b/tools/cann-examples/aicpu-device-query/host/rtt_probe_host.h new file mode 100644 index 0000000000..b79cb7f2ac --- /dev/null +++ b/tools/cann-examples/aicpu-device-query/host/rtt_probe_host.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace pto::a5 { +struct AicpuLaunchPlan; +struct AicpuTopology; +} // namespace pto::a5 + +namespace aicpu_device_query { + +std::string MakeRttDescriptorEntry(uint64_t fingerprint, const std::string &so_basename); +std::string MakeAffinityEnumDescriptorEntry(uint64_t fingerprint, const std::string &so_basename); +std::string MakeAffinityPreflightDescriptorEntry(uint64_t fingerprint, const std::string &so_basename); + +// Full affinity preflight: serial enum of the user pool, atomic-flag orch +// election, COND die scoring. Emits schema_version=3 JSON on stdout. +bool RunAffinityPreflight( + int device_id, void *binary_handle, uint64_t fingerprint, void *device_args, size_t device_args_bytes, + aclrtStream stream, const pto::a5::AicpuTopology &topology +); + +// Back-compat name used by query_device_hal.cpp. +bool RunRttProbe( + int device_id, void *binary_handle, uint64_t fingerprint, void *device_args, size_t device_args_bytes, + aclrtStream stream, const pto::a5::AicpuTopology &topology, const pto::a5::AicpuLaunchPlan &launch_plan +); + +} // namespace aicpu_device_query diff --git a/tools/cann-examples/aicpu-device-query/run_query_topo.sh b/tools/cann-examples/aicpu-device-query/run_query_topo.sh new file mode 100755 index 0000000000..b4a902d84a --- /dev/null +++ b/tools/cann-examples/aicpu-device-query/run_query_topo.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Copyright (c) PyPTO Contributors. +# This program is free software, you can redistribute it and/or modify it under the terms and conditions of +# CANN Open Software License Agreement Version 2.0 (the "License"). +# Please refer to the License for details. You may not use this file except in compliance with the License. +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. +# See LICENSE in the root of the software repository for the full text of the License. +# ----------------------------------------------------------------------------------------------------------- +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" + +if [[ -d /usr/local/Ascend/ascend-toolkit/latest ]]; then + export ASCEND_HOME_PATH=/usr/local/Ascend/ascend-toolkit/latest +elif [[ -d /usr/local/Ascend/cann-9.2.0 ]]; then + export ASCEND_HOME_PATH=/usr/local/Ascend/cann-9.2.0 +else + echo "ASCEND_HOME_PATH not found" >&2 + exit 1 +fi + +# shellcheck disable=SC1091 +source "${ASCEND_HOME_PATH}/set_env.sh" +echo "ASCEND_HOME_PATH=${ASCEND_HOME_PATH}" >&2 + +DEVICE_ID="${TASK_DEVICE:-0}" +OUTPUT_MODE="--json" +while [[ $# -gt 0 ]]; do + case "$1" in + --device) + if [[ $# -lt 2 ]]; then + echo "--device requires an id" >&2 + exit 1 + fi + DEVICE_ID="$2" + shift 2 + ;; + --json|--rtt-json) + OUTPUT_MODE="$1" + shift + ;; + *) + if [[ "$1" =~ ^[0-9]+$ ]]; then + DEVICE_ID="$1" + shift + else + echo "usage: $0 [--device N|N] [--json|--rtt-json]" >&2 + exit 1 + fi + ;; + esac +done + +REPO_DISPATCHER="${REPO}/build/lib/a5/dispatcher/libsimpler_aicpu_dispatcher.so" +DISPATCHER="${SIMPLER_DISPATCHER_SO:-${REPO_DISPATCHER}}" +if [[ ! -f "${DISPATCHER}" ]]; then + echo "Building a5 runtime artifacts (includes dispatcher)..." >&2 + PYTHONPATH="${REPO}:${PYTHONPATH:-}" python3 -m simpler_setup.build_runtimes \ + --lib-dir "${REPO}/build/lib" \ + --cache-dir "${REPO}/build/cache" \ + --platforms a5 >&2 + DISPATCHER="${REPO_DISPATCHER}" +fi +if [[ ! -f "${DISPATCHER}" ]]; then + echo "Missing dispatcher SO after build: ${REPO_DISPATCHER}" >&2 + echo "Set SIMPLER_DISPATCHER_SO to an existing libsimpler_aicpu_dispatcher.so, or build a5 runtimes." >&2 + exit 1 +fi + +DEVICE_BUILD="${REPO}/build/cache/aicpu-device-query/device" +REPO_QUERY_SO="${DEVICE_BUILD}/libaicpu_query.so" +QUERY_SO="${SIMPLER_AICPU_QUERY_SO:-${REPO_QUERY_SO}}" +if [[ ! -f "${QUERY_SO}" ]]; then + echo "Building AICPU query/probe device SO..." >&2 + cmake -S "${REPO}/tools/cann-examples/aicpu-device-query/device" \ + -B "${DEVICE_BUILD}" \ + -DCMAKE_C_COMPILER="${ASCEND_HOME_PATH}/tools/hcc/bin/aarch64-target-linux-gnu-gcc" \ + -DCMAKE_CXX_COMPILER="${ASCEND_HOME_PATH}/tools/hcc/bin/aarch64-target-linux-gnu-g++" >&2 + cmake --build "${DEVICE_BUILD}" -j"$(nproc)" >&2 + QUERY_SO="${REPO_QUERY_SO}" +fi +if [[ ! -f "${QUERY_SO}" ]]; then + echo "Missing query SO after build: ${QUERY_SO}" >&2 + exit 1 +fi + +HOST_BUILD="${REPO}/build/cache/aicpu-device-query/host" +HOST_BIN="${HOST_BUILD}/query_device_hal" +if [[ ! -f "${HOST_BIN}" ]]; then + cmake -S "${REPO}/tools/cann-examples/aicpu-device-query/host" \ + -B "${HOST_BUILD}" >&2 + cmake --build "${HOST_BUILD}" -j"$(nproc)" >&2 +fi +if [[ ! -f "${HOST_BIN}" ]]; then + echo "Missing host launcher: ${HOST_BIN}" >&2 + exit 1 +fi + +export SIMPLER_DISPATCHER_SO="${DISPATCHER}" +export SIMPLER_AICPU_QUERY_SO="${QUERY_SO}" + +echo "=== query_device_hal ${OUTPUT_MODE} on device ${DEVICE_ID} ===" >&2 +exec "${HOST_BIN}" "${DEVICE_ID}" "${OUTPUT_MODE}" diff --git a/tools/cann-examples/aicpu-device-query/shared/rtt_probe_types.h b/tools/cann-examples/aicpu-device-query/shared/rtt_probe_types.h new file mode 100644 index 0000000000..0420b70d91 --- /dev/null +++ b/tools/cann-examples/aicpu-device-query/shared/rtt_probe_types.h @@ -0,0 +1,109 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ +#pragma once + +#include +#include + +// Full AICPU affinity preflight protocol (replaces the earlier 4-scheduler-only +// COND RTT probe). Host enumerates the user pool, launches one thread per pool +// CPU, measures pairwise atomic-flag handshake latency, then measures COND +// access sums for non-orchestrator threads. + +constexpr uint32_t kAffinityMaxPool = 16; +constexpr uint32_t kAffinitySchedCount = 4; +constexpr uint32_t kAffinityHandshakeIters = 1000; +constexpr uint32_t kAffinitySamplesPerCore = 100; +constexpr uint32_t kAffinityWarmupSamplesPerCore = 8; +constexpr uint32_t kAffinityBarrierTimeoutSeconds = 30; + +// Legacy aliases kept so existing include sites compile during the transition. +constexpr uint32_t kRttProbeSchedulerCount = kAffinitySchedCount; +constexpr uint32_t kRttProbeSamplesPerCore = kAffinitySamplesPerCore; +constexpr uint32_t kRttProbeWarmupSamplesPerCore = kAffinityWarmupSamplesPerCore; +constexpr uint32_t kRttProbeBarrierTimeoutSeconds = kAffinityBarrierTimeoutSeconds; + +enum class AffinityProbeError : uint32_t { + kNone = 0, + kDuplicatePool = 1, + kBarrierTimeout = 2, + kInvalidPool = 3, +}; + +using RttProbeError = AffinityProbeError; + +struct AffinityPoolSlot { + int32_t pool_idx; + int32_t cpu_id; + uint64_t avg_handshake_ticks; + uint64_t die0_sum_ticks; + uint64_t die1_sum_ticks; + uint32_t handshake_valid; + uint32_t die_valid; + uint32_t is_orch; + uint32_t reserved; +}; + +struct AffinityPreflightOutput { + uint32_t ready_count; + uint32_t claimed_mask; + uint32_t error_code; + uint32_t pool_count; + uint32_t orch_pool_idx; + uint32_t handshake_done; + uint32_t die_done; + uint32_t reserved; + AffinityPoolSlot slots[kAffinityMaxPool]; + // Symmetric pair average ticks; index = i * kAffinityMaxPool + j (i != j). + uint64_t handshake_pair_ticks[kAffinityMaxPool * kAffinityMaxPool]; + // Atomic flag handshake scratch (req/ack). Host zeroes before launch. + uint32_t handshake_req[kAffinityMaxPool][kAffinityMaxPool]; + uint32_t handshake_ack[kAffinityMaxPool][kAffinityMaxPool]; +}; + +// Serial enum (aicpu_num=1): one thread writes its sched_getcpu into slot 0. +struct AffinityEnumOutput { + uint32_t ready; + int32_t cpu_id; + uint32_t reserved[2]; +}; + +struct AffinityEnumDeviceArgs { + uint64_t reserved_pre[12]; + uint64_t output_addr; +}; + +// Reuses the dispatcher's 160-byte DeviceArgs buffer. Fields start at offset 96. +struct AffinityPreflightDeviceArgs { + uint64_t reserved_pre[12]; + uint64_t output_addr; + uint64_t aicore_regs_addr; + uint64_t user_pool_addr; + uint32_t pool_count; + uint32_t aicore_count; + uint32_t handshake_iters; + uint32_t samples_per_core; +}; + +using RttProbeDeviceArgs = AffinityPreflightDeviceArgs; +using RttProbeOutput = AffinityPreflightOutput; +using RttProbeSlot = AffinityPoolSlot; + +static_assert(sizeof(AffinityPreflightDeviceArgs) <= 160, "affinity probe args exceed DeviceArgs storage"); +static_assert(sizeof(AffinityEnumDeviceArgs) <= 160, "affinity enum args exceed DeviceArgs storage"); +static_assert(std::is_trivially_copyable_v && std::is_standard_layout_v); +static_assert( + std::is_trivially_copyable_v && std::is_standard_layout_v +); +static_assert( + std::is_trivially_copyable_v && + std::is_standard_layout_v +); diff --git a/tools/rtt_die_preflight.sh b/tools/rtt_die_preflight.sh new file mode 100755 index 0000000000..ce9e1b1192 --- /dev/null +++ b/tools/rtt_die_preflight.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Copyright (c) PyPTO Contributors. +# This program is free software, you can redistribute it and/or modify it under the terms and conditions of +# CANN Open Software License Agreement Version 2.0 (the "License"). +# Please refer to the License for details. You may not use this file except in compliance with the License. +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. +# See LICENSE in the root of the software repository for the full text of the License. +# ----------------------------------------------------------------------------------------------------------- +# Probe and atomically write/merge build/config/aicpu_affinity_plan.json via the +# Python affinity preflight CLI. Prefer task-submit for onboard runs. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" +export PYTHONPATH="$PROJECT_ROOT${PYTHONPATH:+:$PYTHONPATH}" + +DEVICE="${DEVICE_ID:-${TASK_DEVICE:-0}}" +exec python3 -m simpler_setup.tools.rtt_die_preflight --device "$DEVICE" --probe "$@"