Skip to content

Commit b79d559

Browse files
committed
The Ascend example stops being a sketch: the kernel compiles, and the driver is what is missing
It was written as a shape with two named gaps -- a rule package and an xim package for the toolkit -- and both now exist. What replaced them is a list of measurements. On an x86_64 machine with no Ascend hardware and no Ascend driver: `xim:cann-toolkit` provisions 2.9 GB without root; `build.mcpp` compiles and imports `mcpp.rules.ascendc`; the kernel compiles under `bisheng -x asc --cce-aicore-arch=dav-c220`; the resulting object joins the ORDINARY link, because the rule compiles in mixed mode and gets an x86-64 object carrying the device binary rather than a Da Vinci one the host linker cannot place; the host half links against ACL and the six-library closure the rule names. The artifact then does not start, because `libascend_hal.so` is missing -- the DRIVER, which is the role `libcuda.so.1` plays for CUDA: in ABI lockstep with the kernel module, not redistributable, and correctly absent here. `mcpp run --no-accel` builds and runs: `12 24 36 48`, `device: cpu`. So the example completes everywhere and RUNS only on an Ascend machine, which is the same statement `examples/09-heterogeneous/cuda` makes about a machine with no NVIDIA driver. Its skip reason says that instead of what it used to say. The payloads are gated on the accelerator, so the CPU leg installs nothing at all -- which is the first real use of that gating outside the multi-backend example. THE SEAM IS A C FUNCTION, and that is measured rather than stylistic: BiSheng's own launcher for a `__global__` function is C++-MANGLED even when the kernel is declared `extern "C"`. Calling it directly would make the program depend on BiSheng and the project's C++ compiler agreeing about mangling -- clang 15 and whatever the project chose. The `.asc` file exports an `extern "C"` wrapper instead, and the `<<<...>>>` launch spelling never leaves the translation unit the device compiler owns. The README's "what was established about the toolkit" section is replaced by what the toolkit turned out to BE, including the 38 SoC simulators that make this lane verifiable without an NPU -- which is the next thing this example should use, and a separate piece of work with its own contract.
1 parent ad8adbb commit b79d559

6 files changed

Lines changed: 208 additions & 44 deletions

File tree

.github/tools/build_examples.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ SKIP=(
5151
"examples/09-heterogeneous/hip/app|same, for the HIP payloads"
5252
"examples/09-heterogeneous/sycl/app|needs the dpcpp payload (over a gigabyte) and a device its runtime accepts"
5353
"examples/09-heterogeneous/vulkan/app|built AND RUN by the next step of this job, on the lavapipe payload, which needs no GPU"
54-
"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"
54+
"examples/09-heterogeneous/cann/app|its device leg needs the Ascend DRIVER, which a runner does not have: the kernel compiles and the object links, and then `libascend_hal.so` is missing, which is correct on a machine with no NPU. Its CPU leg does build -- and is not built here only because the plugins pin would make this job resolve a fifth rule package for one example. Covered by the measurements in its README"
5555
)
5656

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

examples/09-heterogeneous/cann/app/README.md

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,67 @@ the file's content is C++ and nothing in it would tell a reader otherwise. What
1010
makes it a device translation unit is that it goes to BiSheng — a compiler with
1111
a device back end, and one that does not accept C++20 modules.
1212

13-
## This example does not build yet
13+
## What builds, and what does not
1414

15-
Two pieces do not exist:
15+
```bash
16+
mcpp run --no-accel # builds and runs
17+
mcpp build --accel "ascend8.5+{dav-c220}" # compiles the kernel, links, then
18+
# stops on the missing driver
19+
```
1620

17-
| Missing | What it is |
21+
Measured on an x86_64 machine with **no Ascend hardware and no Ascend driver**:
22+
23+
| step | result |
24+
|---|---|
25+
| `xim:cann-toolkit` provisioned | 2.9 GB, no root, no driver |
26+
| `build.mcpp` compiles and runs | `mcpp.rules.ascendc` imported from `mcpp:plugins` |
27+
| the kernel compiles | `bisheng -x asc --cce-aicore-arch=dav-c220` |
28+
| the object joins the ordinary link | mixed mode: an x86-64 object carrying the device binary |
29+
| the host half links | ACL, plus the six-library closure the rule names |
30+
| the artifact starts | **no** -- `libascend_hal.so` is missing |
31+
| `--no-accel` | builds and runs: `12 24 36 48`, `device: cpu` |
32+
33+
`libascend_hal.so` belongs to the **driver**, not the toolkit, and is the role
34+
`libcuda.so.1` plays for CUDA: in ABI lockstep with the kernel module, not
35+
redistributable, and absent on a machine with no NPU. A device build of this
36+
example therefore completes everywhere and *runs* only on an Ascend machine --
37+
which is the same statement `examples/09-heterogeneous/cuda` makes about a
38+
machine with no NVIDIA driver, and the reason both are skipped by CI.
39+
40+
The payloads are gated on the accelerator, so `mcpp run --no-accel` installs
41+
nothing: the CPU leg costs a C++ compile and no download at all.
42+
43+
## What the toolkit turned out to be
44+
45+
The design this follows expected the toolkit to be hard to obtain. It is not.
46+
Every CANN toolkit from 8.0.RC1 to 8.5.0 is a plain `.run` on Huawei's own OBS,
47+
answering 200 to an anonymous HEAD request, and it installs unattended:
48+
49+
```bash
50+
./Ascend-cann-toolkit_8.5.0_linux-x86_64.run --install \
51+
--install-path=<dir> --quiet
52+
```
53+
54+
Both halves the lane needs are inside it:
55+
56+
| | |
1857
|---|---|
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.
58+
| `<arch>-linux/ccec_compiler/bin/{ccec,bisheng}` | the device compiler, clang 15.0.5 |
59+
| `<arch>-linux/simulator/<SoC>/lib/libpem_davinci.so` | **38 SoCs**, no hardware required |
60+
61+
The second is why the lane is verifiable without an NPU at all, and it is the
62+
next thing this example should use: the kernel compiles today, and running it
63+
under `libpem_davinci` is a separate piece of work with its own contract.
64+
65+
## The seam is a C function, and that is measured rather than stylistic
66+
67+
BiSheng's own launcher for a `__global__` function is **C++-mangled** even when
68+
the kernel is declared `extern "C"`. Calling it from the host half would make
69+
this program depend on BiSheng and the project's C++ compiler agreeing about
70+
mangling -- two different compilers, one of them clang 15 and the other
71+
whatever the project chose. The `.asc` file therefore exports an `extern "C"`
72+
wrapper, and the `<<<...>>>` launch spelling never leaves the translation unit
73+
the device compiler owns.
4574

4675
## Why `accelerator = "none"` for the fallback
4776

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import std;
2+
import mcpp;
3+
import mcpp.rules.ascendc;
4+
5+
// The kernel is compiled by BiSheng, which mcpp does not drive. Everything the
6+
// rule needs is already in the manifest -- the architecture in `[build] accel`,
7+
// the kernel in the constrained glob, the toolkit under `[xlings.workspace]` --
8+
// so this program names the project's own include directory and says go.
9+
int main() {
10+
mcpp::rerun_if_changed_glob("src/kernels/**/*.asc");
11+
mcpp::rules::ascendc::options opt;
12+
opt.includes = { "include" };
13+
return mcpp::rules::ascendc::compile(opt) ? 0 : 1;
14+
}

examples/09-heterogeneous/cann/app/mcpp.toml

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,34 @@ standard = "c++23"
1010
modules = true
1111
import_std = true
1212

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.
13+
# The rule that compiles the kernel, selected by its feature; `build.mcpp`
14+
# imports it as `mcpp.rules.ascendc`. `[build-dependencies]`, because a rule
15+
# package's library must never reach the target while its rule is wanted.
1916
[build-dependencies.mcpp]
20-
plugins = { version = "0.2.1", features = ["rules-ascendc"], host-module = true }
17+
plugins = { version = "0.2.3", features = ["rules-ascendc"], host-module = true }
2118

22-
# The toolkit carries BOTH the device compiler and the per-SoC simulator, so
23-
# this is one payload rather than two:
19+
# The toolkit carries BOTH halves this lane needs, so it is one payload rather
20+
# than two:
21+
#
22+
# <toolkit>/cann/<arch>-linux/ccec_compiler/bin/bisheng the device compiler
23+
# <toolkit>/cann/<arch>-linux/simulator/<SoC>/lib 38 SoCs, no hardware
2424
#
25-
# <toolkit>/compiler/ccec_compiler/bin/bisheng the device compiler
26-
# <toolkit>/*/simulator/<SoC>/lib the hardware-free device
27-
[xlings.workspace]
25+
# Gated on the accelerator: it is 2.9 GB installed, and `mcpp build` with no
26+
# accelerator has no use for it. That gating needs mcpp 2026.9.6.5; before it
27+
# the only spellings were "unconditionally" and "not at all".
28+
[target.'cfg(accelerator = "ascend")'.xlings.workspace]
2829
"xim:cann-toolkit" = "8.5.0"
2930

3031
[build]
3132
# `dav-2201` is the device architecture, the role `sm_89` plays for CUDA. The
3233
# rule package derives BiSheng's own flag from it.
33-
accel = "ascend, dav-2201"
34+
accel = "ascend8.5+{dav-c220}"
3435
sources = [
3536
"src/*.cppm",
3637
"src/*.cpp",
3738
# The kernel carries the accel it is for. It is never offered to the C++
3839
# compiler; the constrained glob routes it to the build program instead.
39-
{ glob = "src/kernels/*.asc", accel = "ascend, dav-2201" },
40+
{ glob = "src/kernels/*.asc", accel = "ascend8.5+{dav-c220}" },
4041
]
4142
include_dirs = ["include"]
4243

@@ -46,6 +47,15 @@ include_dirs = ["include"]
4647
[target.'cfg(accelerator = "none")'.build]
4748
sources = ["src/cpu/*.cpp"]
4849

50+
# The HOST half of the island: it launches the kernel through ACL, and declines
51+
# when no NPU is present -- which is every machine that is not an Ascend one,
52+
# including the one this example was written on. Declining is the contract the
53+
# seam is built around, so a device build on a hostless machine still links and
54+
# still runs; it simply reports that it found no device.
55+
[target.'cfg(accelerator = "ascend")'.build]
56+
sources = ["src/ascend/*.cpp"]
57+
ldflags = ["-lascendcl"]
58+
4959
[targets.ascend-saxpy]
5060
kind = "bin"
5161
main = "src/main.cpp"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// The Ascend island's HOST half: it launches the kernel through ACL.
2+
//
3+
// IT DECLINES WHEN NO NPU IS PRESENT, and that is the contract rather than a
4+
// shortcut. `aclInit` and `aclrtSetDevice` fail on a machine with no Ascend
5+
// device -- which is every machine this example was developed on -- so the
6+
// function returns non-zero and the caller falls back. A device build
7+
// therefore still links and still runs on a machine with no device; it simply
8+
// says so, which is the property that lets one artifact serve both.
9+
//
10+
// The kernel itself was compiled by BiSheng into a Da Vinci object and linked
11+
// into this binary by the ordinary link. Nothing here compiles device code.
12+
#include "saxpy/saxpy.h"
13+
14+
#include <acl/acl.h>
15+
16+
#include <cstdint>
17+
#include <cstdio>
18+
#include <cstring>
19+
20+
// The launcher the `.asc` file exports. `extern "C"` for the reason the seam
21+
// header gives, and for a sharper one measured here: BiSheng's own launcher
22+
// symbol is C++-mangled, so a C++ declaration would make this program depend
23+
// on BiSheng and g++ agreeing about mangling. The wrapper is one function in
24+
// the device translation unit and removes that dependency.
25+
extern "C" void saxpy_launch(std::uint32_t blockDim, void* stream,
26+
std::uint8_t* x, std::uint8_t* y, std::uint8_t* out,
27+
float a, std::uint32_t n);
28+
29+
namespace {
30+
char g_ran_on[128] = "";
31+
32+
// One place to leave ACL in the state it was found in, whichever step failed.
33+
struct acl_session {
34+
bool inited = false, device = false;
35+
aclrtStream stream = nullptr;
36+
~acl_session() {
37+
if (stream) aclrtDestroyStream(stream);
38+
if (device) aclrtResetDevice(0);
39+
if (inited) aclFinalize();
40+
}
41+
};
42+
} // namespace
43+
44+
extern "C" const char* saxpy_device_name(void) { return g_ran_on; }
45+
46+
extern "C" int saxpy_device(float a, const float* x, const float* y,
47+
float* out, unsigned n) {
48+
acl_session s;
49+
if (aclInit(nullptr) != ACL_SUCCESS) return 1;
50+
s.inited = true;
51+
52+
std::uint32_t count = 0;
53+
if (aclrtGetDeviceCount(&count) != ACL_SUCCESS || count == 0) return 1;
54+
if (aclrtSetDevice(0) != ACL_SUCCESS) return 1;
55+
s.device = true;
56+
if (aclrtCreateStream(&s.stream) != ACL_SUCCESS) return 1;
57+
58+
const std::size_t bytes = static_cast<std::size_t>(n) * sizeof(float);
59+
void *dx = nullptr, *dy = nullptr, *dout = nullptr;
60+
auto release = [&] {
61+
if (dx) aclrtFree(dx);
62+
if (dy) aclrtFree(dy);
63+
if (dout) aclrtFree(dout);
64+
};
65+
if (aclrtMalloc(&dx, bytes, ACL_MEM_MALLOC_HUGE_FIRST) != ACL_SUCCESS
66+
|| aclrtMalloc(&dy, bytes, ACL_MEM_MALLOC_HUGE_FIRST) != ACL_SUCCESS
67+
|| aclrtMalloc(&dout, bytes, ACL_MEM_MALLOC_HUGE_FIRST) != ACL_SUCCESS) {
68+
release();
69+
return 1;
70+
}
71+
if (aclrtMemcpy(dx, bytes, x, bytes, ACL_MEMCPY_HOST_TO_DEVICE) != ACL_SUCCESS
72+
|| aclrtMemcpy(dy, bytes, y, bytes, ACL_MEMCPY_HOST_TO_DEVICE) != ACL_SUCCESS) {
73+
release();
74+
return 1;
75+
}
76+
77+
// One block: this example is about the build, and a tiling strategy would
78+
// be the subject of a different one.
79+
saxpy_launch(1, s.stream,
80+
static_cast<std::uint8_t*>(dx), static_cast<std::uint8_t*>(dy),
81+
static_cast<std::uint8_t*>(dout), a, n);
82+
if (aclrtSynchronizeStream(s.stream) != ACL_SUCCESS) { release(); return 1; }
83+
84+
if (aclrtMemcpy(out, bytes, dout, bytes, ACL_MEMCPY_DEVICE_TO_HOST) != ACL_SUCCESS) {
85+
release();
86+
return 1;
87+
}
88+
release();
89+
90+
const char* name = aclrtGetSocName();
91+
std::snprintf(g_ran_on, sizeof g_ran_on, "%s", name ? name : "ascend");
92+
return 0;
93+
}

examples/09-heterogeneous/cann/app/src/kernels/saxpy.asc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,21 @@ extern "C" __global__ __aicore__ void saxpy_kernel(
1919
for (uint32_t i = AscendC::GetBlockIdx(); i < n; i += AscendC::GetBlockNum())
2020
go.SetValue(i, a * gx.GetValue(i) + gy.GetValue(i));
2121
}
22+
23+
// THE SEAM, compiled by BiSheng along with the kernel above.
24+
//
25+
// BiSheng's own launcher for a `__global__` function is C++-mangled even when
26+
// the kernel is declared `extern "C"`, so calling it from the host half would
27+
// make the program depend on two different compilers agreeing about name
28+
// mangling. An `extern "C"` wrapper here costs one function and removes that
29+
// dependency entirely: the host half sees a C symbol, which is what the seam
30+
// header already promises.
31+
//
32+
// `<<<blockDim, l2ctrl, stream>>>` is BiSheng syntax and exists only in this
33+
// translation unit. That is the island: the launch spelling never leaves the
34+
// file the device compiler owns.
35+
extern "C" void saxpy_launch(uint32_t blockDim, void* stream,
36+
GM_ADDR x, GM_ADDR y, GM_ADDR out,
37+
float a, uint32_t n) {
38+
saxpy_kernel<<<blockDim, nullptr, stream>>>(x, y, out, a, n);
39+
}

0 commit comments

Comments
 (0)