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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions docs/en/dev/08-entry-points.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,32 @@ Its fields split three ways, and each way is a type:
| The system-test harness only | — | `rtol`, `atol`, `golden_data_dir`, `save_kernels`, `codegen_only` |
| Nobody — derived | — | `backend_type`, a read-only property over `platform`, not a field |

**`platform` names the target once.** `RunConfig` derives `backend_type` from it
during construction, and `ir.compile` lets `platform` win whenever one is given,
so a `backend_type` that disagrees has never taken effect — passing one to
`RunConfig` now warns and is discarded. `CompileOptions` therefore does not carry
it at all: the object always passes a platform, so a second spelling of the same
decision could only ever be redundant or wrong. On `RunConfig` it is a read-only
property rather than a field, so `dataclasses.replace(cfg, platform=...)` cannot
re-supply the previous platform's backend and trip its own warning.
`ir.compile` keeps its `backend_type` parameter for the lower-level callers that
pass no platform.
**`platform` is two decisions, so `RunConfig` stores two fields.** The string
packs an architecture (`a2a3` / `a5`) and an execution mode (the `sim` suffix),
and each is read by a different consumer: compilation takes only the
architecture — codegen never sees the string — while assembly picks `.so` vs
`.o` from the suffix and the worker compares the whole token. `RunConfig`
therefore carries `arch: BackendType` and `execution_mode: ExecutionMode`, with
`platform` a derived property that serializes them:

```python
RunConfig(arch=BackendType.Ascend950, execution_mode=ExecutionMode.ONBOARD).platform # "a5"
RunConfig(platform="a5").arch # Ascend950
```

`platform=` stays constructible — 238 call sites use it — and sets both axes.
Nothing else moves: `cfg.platform` is still a plain `str`, so the artifact
sidecars, `--platform`, and simpler's `Worker(platform=...)` are untouched. The
split lands where a target is *chosen*; the artifact, the worker and the
assembly layer only *carry* one, and keep the string.

Two things disappear with it. `__post_init__` no longer validates the string
against four literals or rebuilds it from the backend it implied — a platform
that disagrees with its own architecture is no longer a value that can be made.
And `arch` is `BackendType`, not a third enum, so `backend_type` is that field
under the compiler's name rather than a competing input: `CompileOptions` does
not carry it, and `ir.compile` keeps its parameter only for callers that pass no
platform.

`RunConfig.compile_options()` / `run_options()` / `dfx_options()` are views onto
the aggregate, and `compile_kwargs()` is `compile_options().as_compile_kwargs()`.
Expand Down
28 changes: 21 additions & 7 deletions docs/zh/dev/08-entry-points.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,27 @@ compiled(*tensors, config=config)
| 仅系统测试 harness | —— | `rtol`、`atol`、`golden_data_dir`、`save_kernels`、`codegen_only` |
| 无人读取 —— 派生 | —— | `backend_type`,是 `platform` 上的只读属性,不是字段 |

**目标只由 `platform` 说一次。** `RunConfig` 在构造时由它推导出 `backend_type`,而
`ir.compile` 只要拿到 platform 就让 platform 胜出 —— 所以一个与之矛盾的 `backend_type`
从来就没有生效过;现在把它传给 `RunConfig` 会告警并被丢弃。`CompileOptions` 因此干脆
不带这个字段:它总是会传 platform,同一个决策的第二种写法只可能是冗余或错误。在 `RunConfig`
上它是只读属性而非字段,这样 `dataclasses.replace(cfg, platform=...)` 就不会把上一个
platform 的 backend 重新塞回来、触发它自己的告警。`ir.compile` 保留 `backend_type`
参数,供那些完全不传 platform 的底层调用方使用。
**`platform` 是两个决策,所以 `RunConfig` 存两个字段。** 这个字符串打包了架构
(`a2a3` / `a5`)和执行模式(`sim` 后缀),而两者各由不同的消费者读取:编译只取架构 ——
codegen 根本看不到这个字符串 —— 装配层按后缀选 `.so` 还是 `.o`,worker 则比对整个 token。
因此 `RunConfig` 携带 `arch: BackendType` 与 `execution_mode: ExecutionMode`,
`platform` 降为把二者序列化出来的派生属性:

```python
RunConfig(arch=BackendType.Ascend950, execution_mode=ExecutionMode.ONBOARD).platform # "a5"
RunConfig(platform="a5").arch # Ascend950
```

`platform=` 仍然可以构造(238 处调用点在用),并会同时设置两个轴。其余一切不动:
`cfg.platform` 依旧是普通 `str`,所以产物 sidecar、`--platform`、simpler 的
`Worker(platform=...)` 都无感。拆分只落在**选择**目标的地方;产物、worker 与装配层
只是**携带**它,继续用字符串。

随之消失两样东西:`__post_init__` 不再拿四个字面量校验字符串、也不再用它蕴含的 backend
把字符串重拼一遍 —— 一个与自身架构矛盾的 platform 已经不是能被构造出来的值。而 `arch`
的类型就是 `BackendType`、不是第三个枚举,所以 `backend_type` 只是这个字段在编译器词汇下的
名字,而非与之竞争的输入:`CompileOptions` 不带它,`ir.compile` 保留该参数仅供完全不传
platform 的调用方使用。

`RunConfig.compile_options()` / `run_options()` / `dfx_options()` 是这个聚合体上的视图,
而 `compile_kwargs()` 就是 `compile_options().as_compile_kwargs()`。`CompileOptions`
Expand Down
10 changes: 9 additions & 1 deletion python/pypto/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,14 @@
from .log_config import configure_log
from .log_config import current_level as log_level
from .pto_isa import ensure_pto_isa_root, pto_isa_include_dir
from .runner import CompileOptions, DfxOptions, RunConfig, RunResult, execute_compiled
from .runner import (
CompileOptions,
DfxOptions,
ExecutionMode,
RunConfig,
RunResult,
execute_compiled,
)
from .runtime_base import Worker
from .tensor_spec import ScalarSpec, TensorSpec
from .worker import ChipWorker, RegistrationHandle
Expand Down Expand Up @@ -98,6 +105,7 @@
"RegistrationHandle",
"CompileOptions",
"DfxOptions",
"ExecutionMode",
"RunConfig",
"RunResult",
"ScalarSpec",
Expand Down
144 changes: 123 additions & 21 deletions python/pypto/runtime/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import functools
import importlib.util
import inspect
import json
import shlex
import subprocess
Expand All @@ -44,6 +45,7 @@
from ctypes import _SimpleCData
from dataclasses import dataclass, field, replace
from datetime import datetime
from enum import Enum, auto
from pathlib import Path
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -114,9 +116,55 @@ def _load_golden_from_data_dir(out_dir: Path, output_names: set[str]) -> dict[st
)


class ExecutionMode(Enum):
"""Whether a run reaches real silicon or the simulator.

One of the two axes a ``platform`` string packs. It decides how kernels are
assembled (``.so`` for the simulator, ``.o`` plus a text-section extract for
silicon) and whether the two-pass swimlane capture applies; it never reaches
codegen, which sees only the architecture.
"""

ONBOARD = auto()
SIM = auto()


_ARCHES: tuple[BackendType, ...] = (BackendType.Ascend910B, BackendType.Ascend950)


def _arch_name(arch: BackendType) -> str:
"""Return the wire name of an architecture (``"a2a3"`` / ``"a5"``).

Asks the backend handler rather than mapping it here: the C++ side already
owns this string — it is what codegen stamps as ``pto.target_arch`` — and a
second copy in Python is a second thing to keep in step.
"""
return _backend_core.get_backend_instance(arch).get_handler().get_pto_target_arch()


def _platform_string(arch: BackendType, execution_mode: ExecutionMode) -> str:
"""Join the two axes into the wire spelling the runtime and CLI take."""
return f"{_arch_name(arch)}{'sim' if execution_mode is ExecutionMode.SIM else ''}"


def _parse_platform(platform: str) -> tuple[BackendType, ExecutionMode]:
"""Split a wire platform string back into its two axes.

The single place that sniffs the string. Everything else asks the axes.
"""
for arch in _ARCHES:
name = _arch_name(arch)
if platform == name:
return arch, ExecutionMode.ONBOARD
if platform == f"{name}sim":
return arch, ExecutionMode.SIM
expected = ", ".join(f"{_arch_name(a)!r}, {_arch_name(a) + 'sim'!r}" for a in _ARCHES)
raise ValueError(f"Invalid platform {platform!r}. Expected {expected}.")


def _backend_type_for_platform(platform: str) -> BackendType:
"""Return the codegen backend a runtime platform string selects."""
return BackendType.Ascend950 if platform.startswith("a5") else BackendType.Ascend910B
return _parse_platform(platform)[0]


_BACKEND_TYPE_DEPRECATION = (
Expand Down Expand Up @@ -171,7 +219,7 @@ def _normalize_swimlane_level(value: int | bool, source: str) -> int:
return value


@dataclass
@dataclass(kw_only=True)
class RunConfig:
"""Configuration for compiling and dispatching a program.

Expand All @@ -187,8 +235,12 @@ class RunConfig:
does not execute or write compilation artifacts.

Attributes:
platform: Target execution platform — ``"a2a3sim"`` / ``"a2a3"``
(Ascend 910B) or ``"a5sim"`` / ``"a5"`` (Ascend 950).
arch: Target architecture, as the codegen backend that names it —
``BackendType.Ascend910B`` (a2a3) or ``BackendType.Ascend950`` (a5).
execution_mode: :class:`ExecutionMode.SIM` or ``ONBOARD``.
platform: **Not a field** — the wire spelling of the two axes above
(``"a2a3sim"`` / ``"a2a3"`` / ``"a5sim"`` / ``"a5"``), derived on
read. Accepted as a constructor keyword, where it sets both axes.
device_id: Hardware device index (ignored for simulator).
backend_type: **Not a field** — a read-only property derived from
``platform``. Accepted as a deprecated constructor keyword, which
Expand Down Expand Up @@ -350,7 +402,8 @@ class RunConfig:

__test__ = False # Not a pytest test class

platform: str = "a2a3sim"
arch: BackendType = field(default_factory=lambda: BackendType.Ascend910B)
execution_mode: ExecutionMode = ExecutionMode.SIM
device_id: int = 0
rtol: float = 1e-5
atol: float = 1e-5
Expand Down Expand Up @@ -386,15 +439,24 @@ class RunConfig:
dump_ptoas_passes: bool = False

def __post_init__(self) -> None:
if self.platform not in ("a2a3sim", "a2a3", "a5sim", "a5"):
raise ValueError(
f"Invalid platform {self.platform!r}. Expected 'a2a3sim', 'a2a3', 'a5sim', or 'a5'."
# The two axes replace what used to be a membership test on the packed
# platform string. They make a *disagreeing* platform unrepresentable,
# but not a nonsensical one: ``execution_mode="sim"`` is not
# ``ExecutionMode.SIM``, and silently reading it as ONBOARD would turn a
# simulator request into a hardware run. Reject it here, where the value
# is still attached to the name the caller typed.
if not isinstance(self.arch, BackendType):
raise TypeError(
f"RunConfig.arch must be a BackendType, got {type(self.arch).__name__} "
f"({self.arch!r}). Pass BackendType.Ascend910B / Ascend950, or use "
f"platform='a2a3sim' to set both axes from the wire spelling."
)
if not isinstance(self.execution_mode, ExecutionMode):
raise TypeError(
f"RunConfig.execution_mode must be an ExecutionMode, got "
f"{type(self.execution_mode).__name__} ({self.execution_mode!r}). Pass "
f"ExecutionMode.SIM / ONBOARD, or use platform='a2a3sim' to set both axes."
)
backend = _backend_core.get_backend_instance(self.backend_type)
expected_arch = backend.get_handler().get_pto_target_arch()
if not self.platform.startswith(expected_arch):
sim_suffix = "sim" if self.platform.endswith("sim") else ""
self.platform = f"{expected_arch}{sim_suffix}"

# Chip swimlane is levelled; normalize ``bool``/int to an explicit
# level before ``any_dfx_enabled()`` and the CLI round-trip read it.
Expand Down Expand Up @@ -556,15 +618,25 @@ def dfx_options(self) -> "DfxOptions":
enable_scope_stats=self.enable_scope_stats,
)

@property
def platform(self) -> str:
"""The wire spelling of :attr:`arch` + :attr:`execution_mode`.

Derived, never stored. It is what the simpler ``Worker``, the artifact
sidecars and ``--platform`` all take, so it stays a plain ``str`` — but
it is a serialization of the two axes rather than a field anything can
set out of step with them.
"""
return _platform_string(self.arch, self.execution_mode)

@property
def backend_type(self) -> BackendType:
"""The codegen backend :attr:`platform` selects. Derived, never stored.
"""The codegen backend, which is :attr:`arch` under the compiler's name.

``platform`` is the single source of truth for the target, so there is
nothing to keep in sync: ``a5`` / ``a5sim`` are Ascend950 and the rest
are Ascend910B. Read it to learn which backend a platform chose.
Kept as a read accessor because the artifact metadata and downstream
callers read it; :attr:`arch` is the field to set.
"""
return _backend_type_for_platform(self.platform)
return self.arch

@property
def enable_l2_swimlane(self) -> int:
Expand Down Expand Up @@ -614,11 +686,22 @@ def enable_l2_swimlane(self, value: int | bool) -> None:

@functools.wraps(_RUN_CONFIG_INIT)
def _run_config_init(self: RunConfig, *args: Any, **kwargs: Any) -> None:
"""``RunConfig.__init__`` that also accepts the deprecated keywords."""
"""``RunConfig.__init__`` that also accepts ``platform=`` and the deprecated keywords."""
if "platform" in kwargs:
# ``platform=`` is the wire spelling of the two axes, and setting it
# sets both. It deliberately wins over an ``arch=`` / ``execution_mode=``
# in the same call rather than reporting a conflict -- the class is
# ``kw_only`` so an axis can only arrive as a keyword, and this rewrite
# therefore reaches every spelling of it: ``replace(cfg,
# platform=...)`` re-supplies both axes from the existing instance, and
# nothing can tell that echo from a caller who typed a contradicting
# value -- the same ambiguity documented for the deprecated keywords
# below. Rejecting the pair would break every ``replace`` by platform.
kwargs["arch"], kwargs["execution_mode"] = _parse_platform(kwargs.pop("platform"))
Comment thread
lyfne123 marked this conversation as resolved.

if "backend_type" in kwargs:
supplied = kwargs.pop("backend_type")
platform = kwargs.get("platform", "a2a3sim")
if supplied is not None and supplied != _backend_type_for_platform(platform):
if supplied is not None and supplied != kwargs.get("arch", BackendType.Ascend910B):
warnings.warn(_BACKEND_TYPE_DEPRECATION, DeprecationWarning, stacklevel=2)

alias = kwargs.pop("enable_l2_swimlane", None)
Expand All @@ -635,6 +718,25 @@ def _run_config_init(self: RunConfig, *args: Any, **kwargs: Any) -> None:

RunConfig.__init__ = _run_config_init # type: ignore[method-assign]

# ``functools.wraps`` copies the dataclass-generated signature, which lists the
# two axes but not the ``platform=`` spelling the wrapper accepts -- so
# ``inspect.signature(RunConfig)``, and every doc tool and IDE reading it, would
# report a keyword the overwhelming majority of call sites use as unsupported.
# Advertise it. The deprecated keywords stay out on purpose: they are accepted
# for compatibility, not offered.
_RUN_CONFIG_SIGNATURE = inspect.signature(_RUN_CONFIG_INIT)
RunConfig.__signature__ = _RUN_CONFIG_SIGNATURE.replace( # type: ignore[attr-defined]
parameters=[
*(p for name, p in _RUN_CONFIG_SIGNATURE.parameters.items() if name != "self"),
inspect.Parameter(
"platform",
inspect.Parameter.KEYWORD_ONLY,
default=None,
annotation="str | None",
),
]
)


@dataclass
class RunResult:
Expand Down
Loading
Loading