Skip to content

Commit 758971c

Browse files
committed
docs(examples): a multi-backend artifact, an Ascend one, and the table a rule package belongs in
Two examples, and one correction that applies to every example already here. multi-backend -- several backends in ONE artifact, chosen at run time. The four examples beside it are each one seam: a device file and a CPU file define the same symbol and are never in one link, so exactly one exists and the choice is made at build time. That is right for a program. A library cannot make that choice; it is compiled once and consumed by people whose machines differ, so its backends are additive. It is also the first real use of cfg(accelerator = "none"). The dispatcher must be built for cuda, for vulkan, for both, and for a backend that does not exist yet, and not(any(cuda, vulkan)) needs editing every time the ecosystem grows -- the edit that is forgotten being silent. Its default accel is empty, so a plain mcpp build needs no payloads at all, which is what lets CI build it and makes the "none" path genuinely covered rather than described. cann -- Ascend, behind the same seam. It DOES NOT BUILD, and the README says so and names the two missing pieces: a rules-ascendc rule package, and an xim package for the toolkit. It records what was measured instead of guessed: both BiSheng and the simulator live in that one toolkit; the toolkit image pulls anonymously; sim mode needs no hardware AND keeps the island, while cpu mode does not keep it and therefore cannot stand in for a device criterion. The manifest is written out rather than described so the shape is concrete. CANN's own operator libraries already split op_kernel/ from op_host/, so the island is not a shape mcpp imposes on Ascend. THE CORRECTION. All four existing examples declare mcpp:plugins in [dependencies], and docs/05 section 2.6.1 names exactly that case as what [build-dependencies] is for: a package whose library must never reach the target while its rule is still wanted. The two axes are separate -- host-module = true says which build-time product is wanted, the section says whether the package reaches the target, and a rule package answers no on the second. Verified by moving it and rebuilding: build.mcpp compiles, the rule runs, shaders compile, and the artifact runs on a real device. Writing it in [dependencies] still works, which is precisely why the distinction has to be stated rather than left to a failure to teach.
1 parent 5db679f commit 758971c

28 files changed

Lines changed: 623 additions & 11 deletions

File tree

.github/tools/build_examples.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ BUILD=(
2424
examples/03-pack-static
2525
examples/04-workspace
2626
examples/08-build-rules/app
27+
# The CPU-only path of the multi-backend example: no payloads, and it is
28+
# where `cfg(accelerator = "none")` is exercised. The device paths are
29+
# opt-in via --accel and are covered by the rule packages' own CI.
30+
examples/09-heterogeneous/multi-backend
2731
)
2832

2933
# `key|reason`.
@@ -38,6 +42,7 @@ SKIP=(
3842
"examples/09-heterogeneous/hip/app|same, for the HIP payloads"
3943
"examples/09-heterogeneous/sycl/app|needs the dpcpp payload (over a gigabyte) and a device its runtime accepts"
4044
"examples/09-heterogeneous/vulkan/app|built AND RUN by the next step of this job, on the lavapipe payload, which needs no GPU"
45+
"examples/09-heterogeneous/cann/app|does not build yet, and says so in its README: it needs a rules-ascendc rule package and an xim package for the CANN toolkit, neither of which exists. The manifest is written out so the shape is concrete rather than described"
4146
)
4247

4348
# Every ROOT manifest in the tree: a directory with an `mcpp.toml` that has no

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@
55
66
## [Unreleased]
77

8+
### 两个新示例,以及一处所有既有示例都写错了的地方
9+
10+
**`examples/09-heterogeneous/multi-backend`** —— 多个后端进**同一个产物**,运行期选择。
11+
既有四个示例每个都是**一道接缝**:设备文件与 CPU 文件定义同一个符号、永不同时进入一次
12+
链接,所以恰好存在一个,选择在构建期做完。那是程序的正确形态。库做不了这个选择:它
13+
只编译一次,而消费者的机器各不相同,所以它的后端是**叠加**的。这个示例是那种形态。
14+
15+
它同时是 `cfg(accelerator = "none")` 的第一个真实用例:分发器要在 cuda、在 vulkan、在
16+
两者同时、以及在一个还不存在的后端下都被构建,而 `not(any(cuda, vulkan))` 每次生态新增
17+
后端都要改一遍 —— 忘掉的那次是静默的。默认 `accel` 为空,所以 `mcpp build` 不需要任何
18+
载荷,CI 因此能真正构建它,`none` 那条路径也就真的被覆盖。
19+
20+
**`examples/09-heterogeneous/cann`** —— 昇腾。**目前构建不了**,README 点明缺的两块
21+
(`mcpp.rules.ascendc` 规则包,以及承载毕昇与仿真器的 `xim:cann-toolkit`),并记下已经
22+
查实的三件事:两者在同一个工具包里;工具包镜像可匿名拉取;`sim` 模式无需硬件且保留岛,
23+
`cpu` 模式**不保留**因而不能充当设备判据。manifest 写出来而不是描述出来,是为了让
24+
形状具体。CANN 自己的算子库本来就是 `op_kernel/``op_host/` 分开的,岛不是 mcpp 强加
25+
给昇腾的形状。
26+
27+
### 规则包应当声明在 `[build-dependencies]`
28+
29+
既有四个示例都把 `mcpp:plugins` 写在 `[dependencies]` 里,而 docs/05 §2.6.1 自己立的规则
30+
恰恰点名这种情形:**库绝不该到达目标,而它的规则仍然被需要**。两条轴是分开的 ——
31+
`host-module = true` 说要哪种构建期产物,section 说这个包是否到达目标,规则包在第二条轴
32+
上答"否"。四个示例与两份文档已改正。
33+
34+
写在 `[dependencies]` 里同样能工作,这正是这条区分必须被**陈述**而不能指望由一次失败来
35+
教会的原因。
36+
37+
838
### 共享库能说出自己发布哪些符号:`exports`
939

1040
两个平台的默认都是"全导出":ELF 给符号默认可见性,PE 由引擎自动生成列出全部符号的

docs/01-examples.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ examples.
3131
| 06 | [`examples/06-openkal-cross`](../examples/06-openkal-cross/) | One program asking each machine what it is, built for four targets from any host | `--target`, openkal, cross-compilation without editing the source |
3232
| 07 | [`examples/07-project-subos`](../examples/07-project-subos/) | A build program that finds its tools in the environment the project declared | `[xlings] subos`, `[xlings.workspace]`, a build program whose `PATH` is the environment the project named |
3333
| 08 | [`examples/08-build-rules`](../examples/08-build-rules/) | Two rule packages and a project that uses both | `host-module = true`, `[build-dependencies]`, `mcpp::action` with `role = "check"` |
34-
| 09 | [`examples/09-heterogeneous`](../examples/09-heterogeneous/) | One computation on a device, in four programming models, with a CPU fallback in each | `accel`, constrained source globs, the seam module, rule packages from `mcpp:plugins`, `cfg(accelerator = …)` |
34+
| 09 | [`examples/09-heterogeneous`](../examples/09-heterogeneous/) | One computation on a device, in several programming models, with a CPU fallback in each; plus one artifact carrying several backends at once | `accel`, constrained source globs, the seam module, rule packages from `mcpp:plugins`, `cfg(accelerator = …)` |
3535
| 09a | [`…/cuda`](../examples/09-heterogeneous/cuda/) | A CUDA kernel behind a seam module | `mcpp.rules.cuda`, `mcpp::action` with `role = "object"`, the driver stated as a fact and a floor |
3636
| 09b | [`…/vulkan`](../examples/09-heterogeneous/vulkan/) | The same computation as a Vulkan compute shader, on a GPU or on the CPU | `mcpp.rules.spirv`, `mcpp::action` with `role = "source"`, generated headers, a software driver as a payload |
3737
| 09c | [`…/sycl`](../examples/09-heterogeneous/sycl/) | The same computation as a SYCL kernel, compiled by a second compiler | `mcpp.rules.sycl`, the `.sycl` device extension, a chained `mcpp::action` for the device link, `compat:sycl-runtime` |
3838
| 09d | [`…/hip`](../examples/09-heterogeneous/hip/) | The same computation in HIP, reaching an NVIDIA device | `mcpp.rules.hip`, HIP as a header layer over the CUDA runtime, a two-chunk `accel` |
39+
| 09e | [`…/multi-backend`](../examples/09-heterogeneous/multi-backend/) | Several backends in ONE artifact, chosen at run time — the library shape, not the program shape | `accel` as a set, `cfg(accelerator = "none")` and its negation, a dispatch chain, a module seam over a C island boundary |
40+
| 09f | [`…/cann`](../examples/09-heterogeneous/cann/) | An Ascend C kernel behind the same seam. **Does not build yet** — its README names the two missing pieces | the `.asc` device extension, `op_kernel`/`op_host` as an island CANN already has, `accelerator = "none"` for the fallback |
3941

4042
## Suggested Reading Order
4143

docs/05-mcpp-toml.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2379,9 +2379,18 @@ rules-spirv = { sources = ["rules/spirv.cppm"] } # export module mcpp.rules.spir
23792379

23802380
```toml
23812381
# a consumer
2382-
[dependencies.mcpp]
2383-
plugins = { version = "0.1.1", features = ["rules-spirv"], host-module = true }
2384-
```
2382+
[build-dependencies.mcpp]
2383+
plugins = { version = "0.2.1", features = ["rules-spirv"], host-module = true }
2384+
```
2385+
2386+
**`[build-dependencies]`, not `[dependencies]`** — a rule package is the case
2387+
§2.6.1 describes exactly: its library must never reach the target while its
2388+
rule is still wanted. The two axes are separate, so `host-module = true` says
2389+
*which build-time product* is wanted and the section says *whether the package
2390+
reaches the target*; a rule package answers "no" on the second axis, and the
2391+
section is where that is said. Written in `[dependencies]` it still works, and
2392+
that is precisely why the distinction has to be stated rather than enforced by
2393+
a failure.
23852394

23862395
The module set is the feature set: a unit whose feature is not active is not
23872396
compiled, and importing it fails as an unknown module. `mcpp:plugins` is the

docs/zh/01-examples.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ mcpp build && mcpp run
2828
| 06 | [`examples/06-openkal-cross`](../../examples/06-openkal-cross/) | 同一个程序问每台机器它是什么,从任意宿主构建到四个目标 | `--target`、openkal、不改源码的交叉编译 |
2929
| 07 | [`examples/07-project-subos`](../../examples/07-project-subos/) | 构建程序在工程声明的环境里找工具,而不是问机器上恰好有什么 | `[xlings] subos``[xlings.workspace]`、构建程序的 `PATH` 来自工程声明的那个环境 |
3030
| 08 | [`examples/08-build-rules`](../../examples/08-build-rules/) | 两个规则包,以及同时用到它们的工程 | `host-module = true``[build-dependencies]``role = "check"``mcpp::action` |
31-
| 09 | [`examples/09-heterogeneous`](../../examples/09-heterogeneous/) | 同一个计算在设备上跑,写成四种编程模型,每种都带 CPU 回退 | `accel`、带约束的 source glob、接缝模块、来自 `mcpp:plugins` 的规则包、`cfg(accelerator = …)` |
31+
| 09 | [`examples/09-heterogeneous`](../../examples/09-heterogeneous/) | 同一个计算在设备上跑,写成多种编程模型,每种都带 CPU 回退;外加一个同时携带多个后端的产物 | `accel`、带约束的 source glob、接缝模块、来自 `mcpp:plugins` 的规则包、`cfg(accelerator = …)` |
3232
| 09a | [`…/cuda`](../../examples/09-heterogeneous/cuda/) | 接缝模块背后的 CUDA kernel | `mcpp.rules.cuda``role = "object"``mcpp::action`、把驱动陈述为 fact 与 floor |
3333
| 09b | [`…/vulkan`](../../examples/09-heterogeneous/vulkan/) | 同一个计算写成 Vulkan compute shader,在 GPU 上或在 CPU 上 | `mcpp.rules.spirv``role = "source"``mcpp::action`、生成的头文件、作为载荷的软件驱动 |
3434
| 09c | [`…/sycl`](../../examples/09-heterogeneous/sycl/) | 同一个计算写成 SYCL kernel,由第二个编译器编译 | `mcpp.rules.sycl``.sycl` 设备扩展名、为 device link 串起来的 `mcpp::action``compat:sycl-runtime` |
3535
| 09d | [`…/hip`](../../examples/09-heterogeneous/hip/) | 同一个计算写成 HIP,够到一台 NVIDIA 设备 | `mcpp.rules.hip`、HIP 作为 CUDA 运行时之上的一层头文件、两段式的 `accel` |
36+
| 09e | [`…/multi-backend`](../../examples/09-heterogeneous/multi-backend/) | 多个后端进**同一个产物**,运行期选择 —— 这是库的形态,不是程序的形态 | `accel` 作为集合、`cfg(accelerator = "none")` 及其否定、分发链、C 岛边界之上的模块接缝 |
37+
| 09f | [`…/cann`](../../examples/09-heterogeneous/cann/) | 同一道接缝背后的 Ascend C kernel。**目前还构建不了** —— README 里点明了缺的两块 | `.asc` 设备扩展名、CANN 本来就有的 `op_kernel`/`op_host` 岛、回退用 `accelerator = "none"` |
3638

3739
## 推荐阅读顺序
3840

docs/zh/05-mcpp-toml.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2027,10 +2027,17 @@ rules-spirv = { sources = ["rules/spirv.cppm"] } # export module mcpp.rules.spir
20272027

20282028
```toml
20292029
# 消费者
2030-
[dependencies.mcpp]
2031-
plugins = { version = "0.1.1", features = ["rules-spirv"], host-module = true }
2030+
[build-dependencies.mcpp]
2031+
plugins = { version = "0.2.1", features = ["rules-spirv"], host-module = true }
20322032
```
20332033

2034+
**`[build-dependencies]` 而不是 `[dependencies]`** —— 规则包正是 §2.6.1 描述的那种
2035+
情形:它的库绝不该到达目标,而它的规则仍然被需要。两条轴是分开的:
2036+
`host-module = true` 说的是**要哪一种构建期产物**,而 section 说的是**这个包是否到达
2037+
目标**;规则包在第二条轴上的答案是"否",而 section 就是说这件事的地方。写在
2038+
`[dependencies]` 里同样能工作 —— 这恰恰是为什么这条区分必须被**陈述**,而不能指望由
2039+
一次失败来教会。
2040+
20342041
模块集合就是 feature 集合:feature 未激活的单元不编译,import 它会以未知模块失败。
20352042
`mcpp:plugins` 是 mcpp 项目维护的集合(仓库 `mcpp-community/mcpp-plugins`);其成员
20362043
命名为 `mcpp.rules.<x>`(规则包)与 `mcpp.tools.<x>`(构建期工具)。
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Ascend, through the same island
2+
3+
`op_kernel/` beside `op_host/` is how CANN's own operator libraries are already
4+
laid out — every operator in `ops-math` splits that way on disk. The island is
5+
therefore not a shape mcpp imposes on Ascend; it is the shape Ascend already
6+
has, and this example writes it in mcpp's vocabulary.
7+
8+
`.asc` is that seam made checkable, exactly as `.sycl` is in the SYCL example:
9+
the file's content is C++ and nothing in it would tell a reader otherwise. What
10+
makes it a device translation unit is that it goes to BiSheng — a compiler with
11+
a device back end, and one that does not accept C++20 modules.
12+
13+
## This example does not build yet
14+
15+
Two pieces do not exist:
16+
17+
| Missing | What it is |
18+
|---|---|
19+
| `mcpp.rules.ascendc` | the rule package that drives BiSheng, the sibling of `rules-cuda` / `rules-spirv` |
20+
| `xim:cann-toolkit` | an index package carrying the toolkit |
21+
22+
Nothing else is missing, and that is why the manifest is written out rather than
23+
described. It is listed in `.github/tools/build_examples.sh` as skipped, with
24+
that reason.
25+
26+
## What was established about the toolkit
27+
28+
Measured 2026-09-07 and recorded in
29+
`.agents/docs/2026-09-07-general-build-infrastructure-gaps-design.md` section 10:
30+
31+
* **One payload, not two.** BiSheng and the simulator live in the same toolkit:
32+
`compiler/ccec_compiler/bin/bisheng` and `*/simulator/<SoC>/lib`.
33+
* **It can be obtained anonymously.** The official distribution is a container
34+
image, `swr.cn-south-1.myhuaweicloud.com/ascendhub/cann`, and its registry
35+
issues a pull token without credentials. Fetching from the vendor's own
36+
registry is the tier this ecosystem's invariant already permits — linked
37+
where it is, never copied into a release of ours.
38+
* **A device is not required to verify a device build.** Ascend C has three run
39+
modes, and `sim` needs no hardware. It is not a substitute for `cpu` mode:
40+
`cpu` links `tikicpulib` and compiles the same kernel source with the HOST
41+
compiler, so that graph contains no island at all and passing in it would
42+
prove the kernel's arithmetic rather than the build. Every `RUN_MODE` test in
43+
asc-devkit is `STREQUAL "cpu"` — there is no `sim` branch — so `sim` takes the
44+
same path as `npu` and BiSheng is invoked.
45+
46+
## Why `accelerator = "none"` for the fallback
47+
48+
`not(accelerator = "ascend")` would work today and rot tomorrow: `accelerator`
49+
is an open vocabulary, so a fallback written by enumerating what it is not
50+
changes meaning every time the ecosystem gains a backend. Ascend is itself an
51+
instance of that growth, which is the neatest possible argument for the
52+
spelling.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// The Ascend island's interface.
2+
//
3+
// `extern "C"` and free of standard-library types, for the reason every island
4+
// in this directory gives: the device half is compiled by BiSheng, a compiler
5+
// mcpp did not choose, so the two sides share no C++ ABI.
6+
#ifndef MCPP_EXAMPLE_ASCEND_SAXPY_H
7+
#define MCPP_EXAMPLE_ASCEND_SAXPY_H
8+
9+
#ifdef __cplusplus
10+
extern "C" {
11+
#endif
12+
13+
// out[i] = a * x[i] + y[i], computed on the NPU. Returns 0 on success.
14+
int saxpy_device(float a, const float* x, const float* y, float* out, unsigned n);
15+
16+
// Which device the last successful call ran on, or "" if none has. Both
17+
// implementations of this seam produce the same numbers, so the numbers alone
18+
// do not separate a device run from the fallback.
19+
const char* saxpy_device_name(void);
20+
21+
#ifdef __cplusplus
22+
}
23+
#endif
24+
#endif
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
[package]
2+
name = "ascend-saxpy"
3+
namespace = "example"
4+
version = "0.1.0"
5+
description = "An Ascend C kernel behind a seam module, with a CPU fallback"
6+
accelerators = ["ascend"]
7+
8+
[language]
9+
standard = "c++23"
10+
modules = true
11+
import_std = true
12+
13+
# NOT YET BUILDABLE, AND THE TWO MISSING PIECES ARE NAMED IN README.md.
14+
#
15+
# This manifest is the shape the Ascend lane takes, written out so that the
16+
# design it follows from is concrete rather than described. What it needs and
17+
# does not have: a rule package `mcpp.rules.ascendc`, and an xim package for the
18+
# CANN toolkit that carries BiSheng and the simulator.
19+
[build-dependencies.mcpp]
20+
plugins = { version = "0.2.1", features = ["rules-ascendc"], host-module = true }
21+
22+
# The toolkit carries BOTH the device compiler and the per-SoC simulator, so
23+
# this is one payload rather than two:
24+
#
25+
# <toolkit>/compiler/ccec_compiler/bin/bisheng the device compiler
26+
# <toolkit>/*/simulator/<SoC>/lib the hardware-free device
27+
[xlings.workspace]
28+
"xim:cann-toolkit" = "8.5.0"
29+
30+
[build]
31+
# `dav-2201` is the device architecture, the role `sm_89` plays for CUDA. The
32+
# rule package derives BiSheng's own flag from it.
33+
accel = "ascend, dav-2201"
34+
sources = [
35+
"src/*.cppm",
36+
"src/*.cpp",
37+
# The kernel carries the accel it is for. It is never offered to the C++
38+
# compiler; the constrained glob routes it to the build program instead.
39+
{ glob = "src/kernels/*.asc", accel = "ascend, dav-2201" },
40+
]
41+
include_dirs = ["include"]
42+
43+
# The CPU variant of the same seam. `accelerator = "none"` rather than
44+
# `not(accelerator = "ascend")`: `accelerator` is an open vocabulary, so a
45+
# fallback written by enumeration changes meaning as the ecosystem grows.
46+
[target.'cfg(accelerator = "none")'.build]
47+
sources = ["src/cpu/*.cpp"]
48+
49+
[targets.ascend-saxpy]
50+
kind = "bin"
51+
main = "src/main.cpp"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// The seam, as a module.
2+
//
3+
// Its reason for existing is not that BiSheng rejects modules. It is that this
4+
// is the one place a backend can be exchanged: the island underneath can become
5+
// CUDA or a CPU fallback without a single importer changing.
6+
module;
7+
#include "saxpy/saxpy.h"
8+
export module app.saxpy;
9+
import std;
10+
11+
export namespace app {
12+
13+
std::optional<std::vector<float>>
14+
saxpy(float a, std::span<const float> x, std::span<const float> y) {
15+
if (x.size() != y.size()) return std::nullopt;
16+
std::vector<float> out(x.size());
17+
if (saxpy_device(a, x.data(), y.data(), out.data(),
18+
static_cast<unsigned>(x.size())) != 0)
19+
return std::nullopt;
20+
return out;
21+
}
22+
23+
std::string_view device_name() { return saxpy_device_name(); }
24+
25+
} // namespace app

0 commit comments

Comments
 (0)