Skip to content

Commit 6115c35

Browse files
committed
feat(example): two routes for the CUDA example, and the pairings it refuses
The example took one route — nvcc driving the project's own compiler — and that route has two constraints neither the engine nor the project controls. Both now produce a sentence before anything is compiled, and a second route exists that has neither. **clang is the primary route.** `[toolchain] default = "llvm@22.1.8"` and the device unit is compiled by the same compiler as the rest of the project (`-x cuda --cuda-path=<payload>`). No second host compiler, no host-compiler bound, no CUDA host header in the way. `MCPP_EXAMPLE_CUDA_ROUTE` selects the other route, and the rule declares `rerun_if_env_changed` for it. **nvcc is the alternate, and it refuses two pairings by name.** The host compiler must satisfy the bound nvcc states in its own `crt/host_config.h`: the rule uses the project's toolchain when it fits, otherwise a `xim:gcc` payload the project declared, otherwise a refusal naming the declaration to add. Measured — GCC 16 under nvcc 12.9 fails inside GCC's own `<type_traits>` even with `-allow-unsupported-compiler`, which admits a compiler one step past the bound and not a standard library two majors newer. The second pairing is a toolkit older than the C library. Toolkit 12.9's `crt/math_functions.h` redeclares the C23 functions `cospi`, `sinpi` and `rsqrt` for the host without `noexcept`; glibc 2.41 and later declare them with it, and since C++17 that is part of the function type. The compile stops with six `exception specification is incompatible` errors naming a glibc header and a CUDA header, and no decision. The rule reads the C library's `bits/mathcalls.h` through `mcpp::toolchain_sysroot()` and states the pair it cannot have, naming the 13.x toolkit as the way out. **A CPU implementation behind the same seam.** `src/cpu/saxpy.cpp` is selected by `cfg(not(accelerator = "cuda"))` while the `.cu` carries the accel its glob is for, so `mcpp build --no-accel` compiles one and `mcpp build` the other, with no hand-written condition on either side. Measured on an RTX 4080, driver 550.144.03 reporting CUDA 12.4: `mcpp run` and `mcpp run --no-accel` both print `12 24 36 48`, from different artifact directories, and the second contains no `cudaMalloc`. The nvcc route is not exercisable there — 12.9 meets the driver and not the C library, 13.3 meets the C library and not the driver — and both refusals are the ones above.
1 parent 8a3a18d commit 6115c35

6 files changed

Lines changed: 684 additions & 240 deletions

File tree

examples/09-cuda-kernel/README.md

Lines changed: 132 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ What this example demonstrates, and what it does not.
66

77
```
88
app/
9-
src/kernels/saxpy.cu the island: compiled by nvcc, never scanned, no BMI
9+
src/kernels/saxpy.cu the island: a device translation unit, never scanned,
10+
no BMI, and compiled only when the build asks for CUDA
11+
src/cpu/saxpy.cpp the same interface implemented for the host, compiled
12+
instead when it does not
1013
include/saxpy/saxpy.h the island's interface: extern "C", no std types
1114
src/app.cppm the seam: a module that turns the C interface back
1215
into a C++ one
1316
src/main.cpp an ordinary consumer, which imports the seam and
1417
never sees the header
15-
build.mcpp names the sources and the architectures
16-
rules-cuda/ a build-rule package that knows how to run nvcc
18+
build.mcpp hands the device sources to the rule package
19+
rules-cuda/ a build-rule package that knows how to compile them
1720
```
1821

1922
Three properties are load-bearing.
@@ -24,16 +27,45 @@ imports, never producing a BMI. Its header is classified as a header, so
2427
editing one still invalidates the fast path.
2528

2629
**The island's interface is `extern "C"` and free of standard-library types.**
27-
nvcc drives a host compiler that mcpp did not choose, so the two sides do not
28-
share a C++ ABI and must not exchange anything that depends on one. The island
29-
also uses no standard library itself, which keeps it from linking a second copy
30-
of the C++ runtime into a program whose own copy came from mcpp's toolchain.
30+
The device unit may be compiled by a compiler mcpp did not choose, so the two
31+
sides do not share a C++ ABI and must not exchange anything that depends on
32+
one. The island also uses no standard library itself, which keeps it from
33+
linking a second copy of the C++ runtime into a program whose own copy came
34+
from mcpp's toolchain.
3135

3236
**The seam exists for backend substitution, not for the module boundary.** It
33-
is the one place where the island underneath could become HIP or a CPU
34-
fallback without any consumer of `app.saxpy` changing, and the one place a
35-
`cfg(accelerator = ...)` section has to apply. Remove it and every importer
36-
becomes backend-specific.
37+
is the one place where the island underneath becomes a CPU implementation, or
38+
could become HIP, without any consumer of `app.saxpy` changing, and the one
39+
place a `cfg(accelerator = ...)` section has to apply. Remove it and every
40+
importer becomes backend-specific.
41+
42+
## Two builds from one source tree
43+
44+
The device axis is written once, in the manifest, and the source set follows
45+
it:
46+
47+
```toml
48+
[build]
49+
accel = "cuda12.9+{sm_89} ptx>=89"
50+
sources = [
51+
"src/*.cppm",
52+
"src/*.cpp",
53+
{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" },
54+
]
55+
56+
[target.'cfg(not(accelerator = "cuda"))'.build]
57+
sources = ["src/cpu/*.cpp"]
58+
```
59+
60+
A glob may carry the accelerator it is for. `mcpp build` compiles the `.cu`
61+
and the CPU file is absent; `mcpp build --no-accel` compiles the CPU file and
62+
the `.cu` is absent — not excluded by a hand-written condition, but by the
63+
constraint the glob states. The two land in different artifact directories
64+
because the device axis is part of the build's identity, so switching between
65+
them does not rebuild from scratch.
66+
67+
An `--accel` that does not cover a constrained glob is refused before anything
68+
is compiled, with `accel-mismatch` on the machine-readable channel.
3769

3870
## Where the toolkit comes from
3971

@@ -45,59 +77,120 @@ The project names it:
4577
"xim:cuda-cudart" = "12.9.79"
4678
```
4779

48-
These are payloads, so the version is the project's choice and not the machine's.
49-
The rule package resolves them with `mcpp::xpkg_dir` and builds the whole
50-
invocation from what it finds — the compiler, the include directories and the
51-
library search paths. **No path in this example is absolute**, and a build here
52-
touches nothing of the host's CUDA:
80+
These are payloads, so the version is the project's choice and not the
81+
machine's. The rule package resolves them with `mcpp::xpkg_dir` and builds the
82+
whole invocation from what it finds — the compiler, the include directories
83+
and the library search paths. No path in this example is absolute, and a build
84+
here touches nothing of the host's CUDA:
5385

5486
```
5587
$ mcpp build -v | grep -c '/usr/local/cuda\|/usr/bin/nvcc'
5688
0
5789
```
5890

59-
Host locations remain in the rule as a last fallback, so a machine that has only
60-
a distribution toolkit still builds. They are a fallback, not the design.
61-
62-
⚠️ **The payload's headers have to be named.** nvcc adds
91+
**The payload's headers have to be named.** nvcc adds
6392
`<its own directory>/../include` by itself, and on the 12.x line that holds
64-
`crt/` but not `cuda_runtime.h` which lives in the `cuda-cudart` component. An
93+
`crt/` but not `cuda_runtime.h`, which lives in the `cuda-cudart` component. An
6594
earlier revision of this rule left it out, and nvcc resolved `cuda_runtime.h`
66-
from `/usr/include` and then read the **host's** `crt/host_config.h` beside it.
67-
The build failed with the host toolkit's complaint while using the payload's
95+
from `/usr/include` and then read the host's `crt/host_config.h` beside it. The
96+
build failed with the host toolkit's complaint while using the payload's
6897
compiler.
6998

70-
## The rule package, and why nvcc's host compiler is its problem
99+
## Two routes, and why the primary one is clang
100+
101+
The rule package compiles the device unit either way:
102+
103+
- **clang** (`-x cuda --cuda-path=<payload>`) is the default and what
104+
`[toolchain] default = "llvm@22.1.8"` selects. The compiler that builds the
105+
rest of the project builds the device unit too. There is no second host
106+
compiler, no host-compiler bound, and no CUDA host header in the way.
107+
- **nvcc** (`-ccbin <host g++>`) is taken when the project's toolchain is GCC.
108+
It drives a second compiler, and that is where its constraints come from.
109+
110+
`MCPP_EXAMPLE_CUDA_ROUTE=clang|nvcc` overrides the choice, and the rule
111+
declares `rerun_if_env_changed` for it.
112+
113+
Two pairings nvcc cannot have, both stated before the compile rather than
114+
discovered inside it:
115+
116+
- **A host compiler past the bound.** nvcc states a maximum GCC major in its
117+
own `crt/host_config.h`. The rule reads it, uses the project's toolchain when
118+
it fits, otherwise a `xim:gcc` payload the project declared for this purpose,
119+
and otherwise refuses naming the declaration to add. Measured: GCC 16 under
120+
nvcc 12.9 fails inside GCC's own `<type_traits>` even with
121+
`-allow-unsupported-compiler` — that escape hatch admits a compiler one step
122+
past the bound, not a standard library two majors newer.
123+
- **An old toolkit and a new C library.** Toolkit 12.9's
124+
`crt/math_functions.h` redeclares the C23 functions `cospi`, `sinpi` and
125+
`rsqrt` for the host without `noexcept`; glibc 2.41 and later declare them
126+
with it, and since C++17 that is part of the function type. The compile stops
127+
with six `exception specification is incompatible` errors naming a glibc
128+
header and a CUDA header, and no decision. The rule reads the C library's
129+
`bits/mathcalls.h` through `mcpp::toolchain_sysroot()` and refuses the pair,
130+
naming the 13.x toolkit as the way out. The clang route does not include that
131+
header at all.
132+
133+
A second compiler also has to be told where it is. `mcpp::toolchain_sysroot()`
134+
and `mcpp::toolchain_binutils_dir()` are the `--sysroot` and `-B` mcpp passes
135+
to its own compiler; without forwarding them, NVIDIA's `crt/host_config.h`
136+
stops at `features.h: No such file or directory`.
137+
138+
Everything about a compiler's spelling lives in the rule package. The engine
139+
owns the graph, the artifact's identity and the architecture set; it does not
140+
own `-gencode` or `--cuda-gpu-arch`.
141+
142+
## What the rule reports before the first compile
143+
144+
The rule states machine facts through the build program's own channel, and mcpp
145+
compares them:
146+
147+
```
148+
mcpp:fact=cuda.driver=12.4
149+
mcpp:floor=cuda.driver >= 12.0
150+
```
71151

72-
nvcc refuses host compilers newer than a bound it states in its own
73-
`crt/host_config.h`, and mcpp's toolchain payload is routinely newer than that
74-
bound. The rule reads the bound — from the payload, which states a newer one
75-
than a distribution toolkit does — selects a host compiler that satisfies it,
76-
and says which one it chose:
152+
The fact comes from opening the driver's own library through the
153+
`libcuda-host-link` sentinel and asking it for its version; the floor comes
154+
from the toolkit the project named. mcpp refuses a build whose floor is not met
155+
and says so in one sentence, because the failure it prevents is not a build
156+
failure:
77157

78158
```
79-
example.rules.cuda: nvcc …/xpkgs/xim-x-cuda-nvcc/12.9.86/bin/nvcc with -ccbin …
159+
error: `cuda-saxpy` requires cuda.driver >= 13.0, and this machine has 12.4.
80160
```
81161

82-
`mcpp self doctor` reports the same pairing independently, and reads the same
83-
payload.
162+
A separate advisory covers PTX: embedded PTX emitted by a toolkit newer than
163+
the driver cannot be JIT-compiled by that driver, so hardware outside the named
164+
architecture set will not run. The named architectures still do, so this is a
165+
warning rather than a refusal.
84166

85-
Everything about nvcc's spelling lives in the rule package. The engine owns the
86-
graph, the artifact's identity and the architecture set; it does not own
87-
`-gencode`.
167+
The engine holds no vendor name for any of this. `cuda.driver` is a string
168+
flowing from a declaration to a comparison; a second backend needs no engine
169+
change. A unit test refuses vendor probes in `src/`.
88170

89171
## Verified
90172

91-
On an NVIDIA RTX 4080 (compute capability 8.9) with CUDA 12.0 and driver
92-
550.144.03:
173+
On an NVIDIA RTX 4080 (compute capability 8.9), driver 550.144.03 reporting
174+
CUDA 12.4, with an LLVM 22.1.8 toolchain:
93175

94176
```
95177
$ mcpp run
96-
Running `target/.../bin/cuda-saxpy`
178+
Running `target/x86_64-linux-gnu/<accel>/bin/cuda-saxpy`
179+
12 24 36 48
180+
181+
$ mcpp run --no-accel
182+
Running `target/x86_64-linux-gnu/<host>/bin/cuda-saxpy`
97183
12 24 36 48
98184
```
99185

100-
which is `2.0 * [1,2,3,4] + [10,20,30,40]` computed on the device.
186+
which is `2.0 * [1,2,3,4] + [10,20,30,40]`, computed on the device in the first
187+
case and on the host in the second. The two artifact directories differ, and
188+
the CPU one contains no `cudaMalloc`.
189+
190+
The nvcc route is not exercisable on that machine: the 12.9 toolkit meets the
191+
driver and not the C library, and the 13.3 toolkit meets the C library and not
192+
the driver. Both refusals are the ones described above, and both name the way
193+
out.
101194

102195
## Where the driver comes from
103196

examples/09-cuda-kernel/app/build.mcpp

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@ import std;
22
import mcpp;
33
import example.rules.cuda;
44

5+
// Everything the rule needs is in the manifest: the architectures in
6+
// `[build] accel`, the device sources in the constrained glob, the toolkit
7+
// under `[xlings.workspace]`. This program names the island's include
8+
// directory and says "go".
9+
//
10+
// The route follows the project's toolchain (clang for an LLVM toolchain,
11+
// nvcc for a GCC one); MCPP_EXAMPLE_CUDA_ROUTE=nvcc|clang overrides it, which
12+
// is how the alternate route is measured without editing the manifest.
513
int main() {
6-
mcpp::rerun_if_changed_glob("src/kernels/**/*.cu");
7-
mcpp::rerun_if_changed_glob("include/**/*.h");
8-
14+
mcpp::rerun_if_env_changed("MCPP_EXAMPLE_CUDA_ROUTE");
915
example::rules::cuda::options opt;
10-
// RTX 4080 is compute capability 8.9. Named rather than detected: what a
11-
// build compiles for is a decision, and a machine's own hardware is a poor
12-
// default for it — the artifact would run here and nowhere else.
13-
opt.archs = { "sm_89" };
14-
// Embed the portable form as well, so the same object runs on hardware
15-
// newer than this one.
16-
opt.ptx = "89";
1716
opt.includes = { "include" };
18-
const std::vector<std::string> sources{ "src/kernels/saxpy.cu" };
19-
return example::rules::cuda::compile(sources, opt) ? 0 : 1;
17+
if (const char* r = std::getenv("MCPP_EXAMPLE_CUDA_ROUTE"); r && *r)
18+
opt.which = std::string_view(r) == "nvcc" ? example::rules::cuda::route::nvcc
19+
: example::rules::cuda::route::clang;
20+
return example::rules::cuda::compile(opt) ? 0 : 1;
2021
}

examples/09-cuda-kernel/app/mcpp.toml

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,71 @@
22
name = "cuda-saxpy"
33
namespace = "example"
44
version = "0.1.0"
5-
description = "A CUDA kernel behind a seam module"
5+
description = "A CUDA kernel behind a seam module, with a CPU fallback"
66
accelerators = ["cuda"]
77

88
[language]
99
standard = "c++23"
1010
modules = true
1111
import_std = true
1212

13+
# The primary route is clang: the toolchain's own clang++ compiles the device
14+
# unit (`-x cuda`), so there is no second host compiler and no host-compiler
15+
# bound to satisfy. With a GCC toolchain the rule package takes the nvcc route
16+
# instead, driving that GCC and reading the bound nvcc states for it.
17+
[toolchain]
18+
default = "llvm@22.1.8"
19+
1320
[dependencies]
1421
rules-cuda = { path = "../rules-cuda", host-module = true }
1522

1623
# The driver's userspace library, reached through an index package that owns
1724
# the one hop mcpp needs: a directory on the artifact's runtime search path.
1825
# mcpp's private loader does not consult /usr/lib, so without it the statically
19-
# linked CUDA runtime cannot dlopen the driver.
26+
# linked CUDA runtime cannot dlopen the driver. It is the one CUDA component
27+
# that cannot be an ordinary payload -- the licence forbids redistributing it
28+
# and it is in ABI lockstep with the kernel module.
2029
[dependencies.compat]
2130
cuda-runtime = "2026.09.05"
2231

23-
# The driver's userspace library, reached through the sentinel package.
24-
#
25-
# It is the one CUDA component that cannot be an ordinary payload: NVIDIA's
26-
# driver licence forbids redistributing it, and it is in ABI lockstep with the
27-
# kernel module, so a version of it is meaningless outside the machine it came
28-
# from. The sentinel installs a symlink to whatever the host has, which gives
29-
# mcpp a path it can put on the artifact's runtime search path — mcpp's private
30-
# loader does not consult /usr/lib, so without this the statically linked CUDA
31-
# runtime cannot dlopen the driver and reports it as missing.
32-
# The toolkit this project builds with, named rather than discovered.
33-
#
34-
# ⭐ These are PAYLOADS, so the version is the project's choice and not the
35-
# machine's. The 12.9 line is named on purpose: a runtime must not be newer than
36-
# the driver it will meet, and 12.x reaches every driver from r525 onward.
37-
# `mcpp self doctor` reports the pairing.
38-
#
39-
# The 12.x `cuda-nvcc` carries its own NVVM back end; on the 13.x line that is a
40-
# separate `libnvvm` alongside `cuda-crt`, and `cuda-nvcc`'s install hook brings
41-
# them. Either way the project names the compiler and gets a working one.
32+
# The toolkit this project builds with, named rather than discovered. These
33+
# are PAYLOADS, so the version is the project's choice and not the machine's.
34+
# The 12.9 line is named on purpose: a runtime must not be newer than the
35+
# driver it will meet, and 12.x reaches every driver from r525 onward. The
36+
# rule package states the driver relation and mcpp compares it before the
37+
# first compile.
4238
[xlings.workspace]
4339
"xim:cuda-nvcc" = "12.9.86"
4440
"xim:cuda-cudart" = "12.9.79"
4541
"xim:libcuda-host-link" = { linux = "0.0.1" }
4642

4743
[build]
48-
# The CUDA runtime is linked STATICALLY. mcpp refuses a dynamic link against
49-
# the host's libcudart because its private loader does not consult /usr/lib,
50-
# and it is right to: such an artifact is not self-contained. Linking the
51-
# redistributable half in leaves exactly one host dependency, libcuda.so.1,
52-
# which is the driver and genuinely cannot be redistributed — that is what the
53-
# libcuda-host-link sentinel package in xim exists for.
54-
accel = "cuda12.9+{sm_89} ptx>=89"
44+
# What this build compiles device code FOR. Written once, here: the rule
45+
# package derives its own flags (`--cuda-gpu-arch`, `-gencode`) from it.
46+
accel = "cuda12.9+{sm_89} ptx>=89"
47+
# The device sources carry the accel they are for. Under `--no-accel` the glob
48+
# is left out and the CPU fallback below takes its place; under an accel that
49+
# does not cover it the build is refused naming both.
50+
sources = [
51+
"src/*.cppm",
52+
"src/*.cpp",
53+
{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" },
54+
]
5555
include_dirs = ["include"]
56-
# ⭐ NO ABSOLUTE PATHS. The CUDA runtime comes from the `cuda-cudart` payload
57-
# named above, and the rule package puts its library directory on the link line
58-
# from `mcpp::xpkg_dir` -- so this manifest names libraries, never locations.
59-
# The one host component left is `libcuda.so.1`, which the driver owns and the
60-
# `compat.cuda-runtime` dependency reaches.
61-
ldflags = ["-lcudart_static", "-lrt", "-lpthread", "-ldl"]
56+
57+
# The CUDA runtime is linked STATICALLY, and only when a device build asks for
58+
# it. Linking the redistributable half in leaves exactly one host dependency,
59+
# libcuda.so.1, which the driver owns and the sentinel package reaches.
60+
# ⭐ NO ABSOLUTE PATHS: the rule package puts the payload's library directory on
61+
# the link line from `mcpp::xpkg_dir`, so this manifest names libraries only.
62+
[target.'cfg(accelerator = "cuda")'.build]
63+
ldflags = ["-lcudart_static", "-lrt", "-lpthread", "-ldl"]
64+
65+
# The CPU-only variant: the same seam, a host implementation behind it. This
66+
# is what `mcpp build --no-accel` produces, and what a machine with no device
67+
# runs.
68+
[target.'cfg(not(accelerator = "cuda"))'.build]
69+
sources = ["src/cpu/*.cpp"]
6270

6371
[targets.cuda-saxpy]
6472
kind = "bin"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// The CPU implementation behind the same seam. Compiled only when the build
2+
// asks for no accelerator (`mcpp build --no-accel`), through the
3+
// `cfg(not(accelerator = "cuda"))` section of the manifest; the device island
4+
// and this file define the same symbol and are never in one link.
5+
#include "saxpy/saxpy.h"
6+
7+
extern "C" int saxpy_device(float a, const float* x, const float* y,
8+
float* out, unsigned n) {
9+
for (unsigned i = 0; i < n; ++i) out[i] = a * x[i] + y[i];
10+
return 0;
11+
}

examples/09-cuda-kernel/rules-cuda/mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
name = "rules-cuda"
33
namespace = "example"
44
version = "0.1.0"
5-
description = "Compile CUDA device translation units with nvcc (role = object)"
5+
description = "Compile CUDA device translation units: clang -x cuda by default, nvcc as the alternate route (role = object)"
66
license = "Apache-2.0"

0 commit comments

Comments
 (0)