Skip to content

Commit 3738506

Browse files
committed
feat: refuse a prebuilt artifact whose device code cannot run on the target
This is the failure the whole dimension exists to move. A library built for one compute capability and consumed by a build targeting another linked cleanly and failed at the first kernel launch, with a runtime message that names neither the package nor the architecture either side expected. The refusal now names both, and the remedy is the one that can work. The generic advice to pin [toolchain] is actively misleading on this axis, because no toolchain pin changes which GPU architecture a build targets; the device case offers --accel and --no-accel instead. The published listing carries the dimension too, since it travels beside the tag rather than inside it and a listing showing only tags would print two indistinguishable lines for a package publishing a CPU and a device build of one triple. Observed: error: mcpplibs.gpuonly@0.1.0: no prebuilt artifact matches this toolchain. your toolchain : x86_64-linux-gnu-gcc16-libstdcxx16-c++23 accel=cuda12.8+{sm_86} published tags : x86_64-linux-gnu accel=cuda12.8+{sm_90f} closest is x86_64-linux-gnu, and it differs on: accel needs cuda12.8+{sm_90f}, this build has cuda12.8+{sm_86} The accompanying end-to-end test also covers the ordering rule: a descriptor listing its CPU-only artifact first lets a build targeting an architecture no device variant carries fall back to the portable one, which is the same ordering that keeps an mcpp predating this field selecting a usable artifact.
1 parent f2de22b commit 3738506

3 files changed

Lines changed: 152 additions & 6 deletions

File tree

modules/manifest/src/toml.cppm

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1905,8 +1905,15 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
19051905
std::string path;
19061906
auto const& table = value.as_table();
19071907
for (auto const& [key, _] : table) {
1908+
// ⚠️ A CLOSED whitelist: an unrecognised key is an error here,
1909+
// unlike the published xpkg descriptor, whose reader collects
1910+
// and skips one. The two readers differ on purpose — a typo in
1911+
// a hand-written manifest should be loud — but it means adding
1912+
// a key to THIS table is a compatibility event: an mcpp that
1913+
// predates the key refuses the whole manifest rather than
1914+
// ignoring the field. See the accelerator design's RK-2.
19081915
if (key != "role" && key != "path" && key != "provenance"
1909-
&& key != "abi" && key != "digest"
1916+
&& key != "abi" && key != "accel" && key != "digest"
19101917
&& key != "host_fingerprint") {
19111918
return std::unexpected(error(origin, std::format(
19121919
"runtime.artifacts[{}] has unsupported key '{}'",
@@ -1917,6 +1924,7 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
19171924
|| !table_string(table, "path", path)
19181925
|| !table_string(table, "provenance", artifact.provenance)
19191926
|| !table_string(table, "abi", artifact.abi)
1927+
|| !table_string(table, "accel", artifact.accel)
19201928
|| !table_string(table, "digest", artifact.digest)
19211929
|| !table_string(table, "host_fingerprint",
19221930
artifact.hostFingerprint)) {

src/pack/prebuilt.cppm

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,12 @@ check_prebuilt(const mcpp::manifest::Manifest& m, const PrebuiltCheck& in)
104104
accepted = true;
105105
continue;
106106
}
107-
publishedTags.push_back(a.abi);
107+
// The device dimension is not part of the tag string, so a listing
108+
// that printed only the tag would show two indistinguishable lines for
109+
// a package publishing a CPU and a device build of the same triple.
110+
publishedTags.push_back(a.accel.empty()
111+
? a.abi
112+
: std::format("{} accel={}", a.abi, a.accel));
108113
auto published = parse_abi_tag(a.abi);
109114
if (!published) { accepted = true; continue; } // unreadable → lenient
110115
// The device dimension travels beside the tag rather than inside it.
@@ -123,16 +128,35 @@ check_prebuilt(const mcpp::manifest::Manifest& m, const PrebuiltCheck& in)
123128
std::string tags;
124129
for (auto const& t : publishedTags) tags += std::format("\n {}", t);
125130
std::string why;
126-
for (auto const& b : bestRefusal)
131+
bool accelOnly = !bestRefusal.empty();
132+
for (auto const& b : bestRefusal) {
127133
why += std::format("\n {:<9} needs {}, this build has {}", b.dimension, b.need, b.got);
134+
if (b.dimension != "accel") accelOnly = false;
135+
}
136+
// The remedy differs by dimension, and the generic one is actively
137+
// misleading for the device axis: pinning [toolchain] cannot change
138+
// which GPU architecture a build targets.
139+
const std::string fix = accelOnly
140+
? std::string(
141+
" fix: build for an architecture the package carries (--accel), or take\n"
142+
" a variant that carries no device code (--no-accel), or ask the\n"
143+
" publisher for one covering yours.")
144+
: std::string(
145+
" fix: ask the publisher for a build matching your toolchain, or pin\n"
146+
" [toolchain] to the one the package was built with.");
147+
// The device dimension travels beside the tag, so the summary line has
148+
// to name it too or it contradicts the per-dimension list below.
149+
const std::string current = in.current.accel.empty()
150+
? in.current.str()
151+
: std::format("{} accel={}", in.current.str(),
152+
accel_str(in.current.accel));
128153
return std::unexpected(std::format(
129154
"{}: no prebuilt artifact matches this toolchain.\n"
130155
" your toolchain : {}\n"
131156
" published tags :{}\n"
132157
" closest is {}, and it differs on:{}\n"
133-
" fix: ask the publisher for a build matching your toolchain, or pin\n"
134-
" [toolchain] to the one the package was built with.",
135-
in.packageLabel, in.current.str(), tags, bestRefusalTag, why));
158+
"{}",
159+
in.packageLabel, current, tags, bestRefusalTag, why, fix));
136160
}
137161

138162
// ── 3. the interface is the one the binaries were built from ──────
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env bash
2+
# requires: elf
3+
# The accelerator dimension decides which prebuilt artifact a build may take.
4+
#
5+
# Two packages, because the two halves of the rule need different shapes:
6+
#
7+
# gpuonly publishes ONE artifact, for sm_90f. A build targeting sm_86 has
8+
# nothing it can use, so it must be refused — and the refusal must
9+
# name the dimension, not a digest or a triple.
10+
# gpukit publishes a CPU-only artifact FIRST and a device one after it.
11+
# The same sm_86 build must succeed by taking the first, which is
12+
# the ordering that also keeps an mcpp predating this field working.
13+
set -e
14+
15+
TMP=$(mktemp -d)
16+
trap "rm -rf $TMP" EXIT
17+
cd "$TMP"
18+
19+
TRIPLE="x86_64-linux-gnu"
20+
21+
make_pkg() { # $1 name, $2... artifact tables
22+
local name="$1"; shift
23+
mkdir -p "$name/lib" "$name/include"
24+
: > "$name/lib/lib${name}.a"
25+
{
26+
echo '[package]'
27+
echo "name = \"$name\""
28+
echo 'version = "0.1.0"'
29+
echo '[language]'
30+
echo 'standard = "c++23"'
31+
printf '%s\n' "$@"
32+
} > "$name/mcpp.toml"
33+
}
34+
35+
# One device-only artifact.
36+
make_pkg gpuonly \
37+
'[[runtime.artifacts]]' \
38+
'role = "static-library"' \
39+
'path = "lib/libgpuonly.a"' \
40+
'provenance = "mcpp-pack/1"' \
41+
"abi = \"$TRIPLE\"" \
42+
'accel = "cuda12.8+{sm_90f}"'
43+
44+
# CPU-only first, device second.
45+
make_pkg gpukit \
46+
'[[runtime.artifacts]]' \
47+
'role = "static-library"' \
48+
'path = "lib/libgpukit.a"' \
49+
'provenance = "mcpp-pack/1"' \
50+
"abi = \"$TRIPLE\"" \
51+
'' \
52+
'[[runtime.artifacts]]' \
53+
'role = "static-library"' \
54+
'path = "lib/libgpukit.a"' \
55+
'provenance = "mcpp-pack/1"' \
56+
"abi = \"$TRIPLE\"" \
57+
'accel = "cuda12.8+{sm_90f}"'
58+
59+
consumer() { # $1 dep name
60+
rm -rf app; "$MCPP" new app > /dev/null; cd app
61+
cat > mcpp.toml <<EOF
62+
[package]
63+
name = "app"
64+
version = "0.1.0"
65+
[language]
66+
standard = "c++23"
67+
[dependencies]
68+
$1 = { path = "../$1" }
69+
EOF
70+
cat > src/main.cpp <<'EOF'
71+
int main() { return 0; }
72+
EOF
73+
rm -f src/*.cppm
74+
cd ..
75+
}
76+
77+
# ── 1. a device-only package refuses a build targeting another architecture ──
78+
consumer gpuonly
79+
cd app
80+
if "$MCPP" build --accel 'cuda12.8+{sm_86}' > out.log 2>&1; then
81+
cat out.log; echo "FAIL: sm_86 accepted an sm_90f-only artifact"; exit 1
82+
fi
83+
grep -q 'accel' out.log || { cat out.log; echo "FAIL: refusal does not name the accel dimension"; exit 1; }
84+
grep -q 'sm_90f' out.log || { cat out.log; echo "FAIL: refusal does not say what the artifact has"; exit 1; }
85+
grep -q 'sm_86' out.log || { cat out.log; echo "FAIL: refusal does not say what this build asked for"; exit 1; }
86+
# The remedy has to be the one that can actually work. Pinning [toolchain]
87+
# cannot change which GPU architecture a build targets, so the generic advice
88+
# is worse than none here.
89+
grep -q -- '--no-accel' out.log \
90+
|| { cat out.log; echo "FAIL: refusal offers no remedy for the device axis"; exit 1; }
91+
grep -q 'accel=cuda12.8+{sm_90f}' out.log \
92+
|| { cat out.log; echo "FAIL: the published listing hides the device dimension"; exit 1; }
93+
cd ..
94+
95+
# ── 2. the same package accepts the architecture it was built for ────────────
96+
cd app
97+
"$MCPP" build --accel 'cuda12.8+{sm_90}' > ok.log 2>&1 \
98+
|| { cat ok.log; echo "FAIL: sm_90 refused by an sm_90f artifact"; exit 1; }
99+
cd ..
100+
101+
# ── 3. a build asking for no accelerator is satisfied vacuously ──────────────
102+
cd app
103+
"$MCPP" build --no-accel > none.log 2>&1 \
104+
|| { cat none.log; echo "FAIL: --no-accel refused"; exit 1; }
105+
cd ..
106+
107+
# ── 4. CPU-first ordering lets an unmatched device build fall back ───────────
108+
consumer gpukit
109+
cd app
110+
"$MCPP" build --accel 'cuda12.8+{sm_86}' > fallback.log 2>&1 \
111+
|| { cat fallback.log; echo "FAIL: CPU-only artifact listed first did not accept sm_86"; exit 1; }
112+
cd ..
113+
114+
echo "PASS: accel variant selection"

0 commit comments

Comments
 (0)