@@ -6,14 +6,17 @@ What this example demonstrates, and what it does not.
66
77```
88app/
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
1922Three properties are load-bearing.
@@ -24,16 +27,45 @@ imports, never producing a BMI. Its header is classified as a header, so
2427editing 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'
56880
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
6594earlier 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
6897compiler.
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`
9718312 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
0 commit comments