Skip to content

feat(accel): version floors, a probe channel, constrained device globs, and a CUDA example that takes two routes - #563

Merged
Sunrisepeak merged 25 commits into
mainfrom
feat/multi-device
Sep 5, 2026
Merged

feat(accel): version floors, a probe channel, constrained device globs, and a CUDA example that takes two routes#563
Sunrisepeak merged 25 commits into
mainfrom
feat/multi-device

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Sep 5, 2026

Copy link
Copy Markdown
Member

Three things, all from the multi-device design. Companion to
openxlings/xim-pkgindex#759 (merged), which put the toolkit into the ecosystem.

1. The device-toolkit report was answering about the wrong toolkit

mcpp self doctor located crt/host_config.h through host locations only. A
toolkit installed through xlings is the one a build will use, and it is usually
the newer one:

toolkit bound it states
12.9 payload gcc <= 14
13.3 payload gcc <= 15
this machine's distribution CUDA 12.0 gcc <= 12
before   warning: cuda will refuse this host compiler: gcc 13 exceeds
                  the bound of 12 stated in /usr/include/crt/host_config.h
after    ok       cuda accepts this host compiler (gcc 13 <= 15)

Both stores are searched — mcpp's own and the one xlings install writes
to. 191 packages in one and 211 in the other here, with the CUDA components only
in the second.

2. Version floors

A device runtime must not be newer than the driver it meets. Measured on a
driver serving CUDA 12.4:

built with compile link run
13.3 clean clean cudaMalloc: CUDA driver version is insufficient
12.9 clean clean result: 12 24 36 48

Both numbers are knowable beforehand, and mcpp must not run a vendor's tool to
get them — test_runtime_contract refuses that in src/, and it caught the
first revision of this PR, which launched nvidia-smi. So they arrive as
declarations:

[[runtime.requirements]]           # what a package needs
kind  = "version-floor"
value = "cuda.driver >= 12.0"

[runtime]                          # what a package established at install time
provides = ["cuda.driver=12.4"]
error: `toolkitnew` requires cuda.driver >= 13.0, and this machine has 12.4.
         stated by: driverfact

No vendor vocabulary reaches the engine. version_floor.cppm reads a name,
a relation and a version. No new manifest keys were needed — kind was already
a free string.

⚠️ A floor nobody answered is silent. A machine that never declared what it
has is not one that fails the floor; it is one nobody asked. That is the third
case in the e2e, not a comment — without it the first case would also pass
against an engine that refused every floor.

3. The example stops reaching for the host

The example shipped in 2026.9.5.1 wrote -L/usr/local/cuda/lib64 and probed
/usr/bin/nvcc. It was the project's own demonstration, and what it
demonstrated was reaching for the host. It now names the toolkit:

[xlings.workspace]
"xim:cuda-nvcc"   = "12.9.86"
"xim:cuda-cudart" = "12.9.79"
$ mcpp build -v | grep -c '/usr/local/cuda\|/usr/bin/nvcc\|-I/usr/include'
0
$ mcpp run
12 24 36 48

⚠️ The payload's headers have to be named. nvcc adds
<its own directory>/../include, and on the 12.x line that holds crt/ but not
cuda_runtime.h — which is in cuda-cudart. The first revision left it out:
nvcc resolved cuda_runtime.h from /usr/include and read the host's
crt/host_config.h beside it, so the build failed with the host toolkit's
complaint while using the payload's compiler. "Uses the payload" and "uses only
the payload" are different, and the first looks like the second.

Verified

602_device_toolkit_payload_first.sh a fabricated payload states gcc <= 41; the report reads it. Control: without the store, 41 is not reported
603_version_floor.sh refused / met / silent-when-unanswered
601 (from #562) still green
unit 8 version-floor + 5 driver-relation, run individually

101 test binaries pass, including the contract test that rejected the earlier
revision of §2.

… and state the driver relation without probing for it

## The report was answering about the wrong toolkit

`mcpp self doctor` located `crt/host_config.h` through `CUDA_PATH`, `CUDA_HOME`,
`/usr/local/cuda` and `/usr/include` — host locations only. A toolkit installed
through xlings is the one a build will use, and it is usually the newer one:
measured here, a 12.9 payload states `gcc <= 14` and a 13.3 payload `gcc <= 15`,
where this machine's distribution CUDA 12.0 states `gcc <= 12`. The report said
`gcc 13 exceeds the bound of 12` about a toolkit the build was not using.

Payload stores are now searched first. Both of them: mcpp keeps its own under
`<mcpp home>/registry/data/xpkgs`, and `xlings install` writes to
`<xlings home>/data/xpkgs` — 191 packages in one and 211 in the other on this
machine, with the CUDA components only in the second. The host locations remain,
last, because a machine with a distribution toolkit and no payload is real.

Reading changes accordingly:

    before   warning: cuda will refuse this host compiler: gcc 13 exceeds
                      the bound of 12 stated in /usr/include/crt/host_config.h
    after    ok       cuda accepts this host compiler (gcc 13 <= 15)

## The driver relation, and why the acquisition is not here

A device runtime must not be newer than the driver it runs against. Measured on
a driver serving CUDA 12.4: the 13.3 payload compiles and links cleanly and then
fails at the first allocation with "CUDA driver version is insufficient for CUDA
runtime version", while the 12.9 payload prints the right answer.

`mcpp::toolchain::driver_accepts_toolkit` states when one version may meet
another, including that minor-version compatibility makes 12.9 fine against a
driver serving 12.4 — the case a naive "toolkit <= driver" check would have
refused. Five unit tests, including that either side unknown makes no claim.

The acquisition is NOT here. Asking a machine which driver it has means running
a vendor's tool, and `tests/unit/test_runtime_contract.cpp` refuses exactly that
in `src/`. It caught the first revision of this change, which launched one. The
rule predates this work and it is right: a core that learns to run one vendor's
probe learns to run four. Those numbers will reach the report as declarations —
a toolkit payload stating the driver it needs, and the package that owns the
host driver stating what the host has — which is the rule-package channel.

## Verified

`tests/e2e/602_device_toolkit_payload_first.sh` fabricates a payload store whose
header states `gcc <= 41`, a bound nothing real would state, and asserts the
report reads it. The control is the half that matters: without the payload store
the same command must not report 41, or the assertion would pass against a
doctor that hardcoded it.

100 test binaries pass, including the contract test that rejected the earlier
revision.
… from the host

The example shipped in 2026.9.5.1 wrote `-L/usr/local/cuda/lib64` in its
manifest and probed `/usr/local/cuda/bin/nvcc` and `/usr/include/crt/host_config.h`
in its rule package. It was the project's own demonstration of how to do this,
and what it demonstrated was reaching for the host.

The project now names the toolkit:

    [xlings.workspace]
    "xim:cuda-nvcc"   = "12.9.86"
    "xim:cuda-cudart" = "12.9.79"

and the rule resolves it with `mcpp::xpkg_dir`, building the whole invocation
from what it finds — compiler, include directories, and library search paths
through `mcpp::link_search`. The manifest names libraries and no locations.
Host paths remain in the rule as a last fallback so a machine with only a
distribution toolkit still builds.

⚠️ THE PAYLOAD'S HEADERS HAVE TO BE NAMED. nvcc adds
`<its own directory>/../include` by itself, and on the 12.x line that holds
`crt/` but not `cuda_runtime.h` — which is in the `cuda-cudart` component. The
first revision of this change left it out: nvcc resolved `cuda_runtime.h` from
/usr/include and then read the HOST's `crt/host_config.h` beside it, and the
build failed with the host toolkit's complaint (`clang version must be less than
15`) while using the payload's compiler. Naming the payload include directories
is what makes "uses the payload" true rather than nearly true.

Verified: `mcpp run` prints `12 24 36 48`, and

    $ mcpp build -v | grep -c '/usr/local/cuda\|/usr/bin/nvcc|-I/usr/include'
    0

nvcc and both include directories resolve under
`registry/data/xpkgs/xim-x-cuda-*`. 100 test binaries pass.
…nything is compiled

Some facts about a machine bound what may be built for it, and the failure when
they are ignored arrives late. Measured on a driver serving CUDA 12.4: a binary
built with the 13.3 toolkit compiles cleanly, links cleanly, and fails at the
first allocation with "CUDA driver version is insufficient for CUDA runtime
version" — a message naming neither the toolkit nor the driver.

Both numbers are knowable beforehand. What mcpp must not do is go and ask a
vendor's tool for them, and `tests/unit/test_runtime_contract.cpp` refuses
exactly that in `src/`. So the numbers arrive as declarations.

A package states what it needs:

    [[runtime.requirements]]
    kind  = "version-floor"
    value = "cuda.driver >= 12.0"

A package that established a fact about this machine — at install time, which
is where probing belongs — states it:

    [runtime]
    provides = ["cuda.driver=12.4"]

They are compared when capabilities are bound, and a short machine is refused
before anything is compiled, reporting `version-floor-unmet`.

## No vendor vocabulary reaches the engine

`src/build/version_floor.cppm` reads a name, a relation and a version and knows
nothing about what any of them mean. `cuda.driver` is data passing through, no
new manifest keys were needed — `kind` was already a free string — and a
backend mcpp has never heard of compares the same way. That is asserted with a
name no backend uses.

## A floor nobody answered is silent

A machine that never declared what it has is not a machine that fails the floor;
it is one nobody asked. Turning "we do not know" into "no" is the failure mode
this exists to avoid, so it is the third case in the e2e rather than a comment.

Without that control the first case would also pass against an engine that
refused every version-floor requirement.

## Verified

`tests/e2e/603_version_floor.sh`, three halves:

    PASS: refused, naming the requirement, both versions and who stated the fact
    PASS: a met floor builds
    PASS: a floor with no stated fact is silent, for a name no backend uses

Eight unit tests over the parsing and the comparison, including that `>=` is
never read as `=` — both spellings live in string lists, and reading a floor as
a fact would turn a requirement into its own satisfaction. 101 test binaries
pass.
@Sunrisepeak Sunrisepeak changed the title feat(doctor): read the device toolkit from a payload before the host, and state the driver relation without probing for it feat(accel): payload-first toolkit discovery, version floors, and a CUDA example that stops reaching for the host Sep 5, 2026
speak-agent and others added 8 commits September 5, 2026 11:50
… report exists

Reaching "five consecutive short failures" costs at least 1.25 s (four
restart delays of 250 ms and five 50 ms polls). A two-second pause left
150 ms per spawn, and a loaded macOS runner exceeded it twice in a row on
main while the same code passed twice on this branch. The pause is now
five seconds, so the property is decided by the code.

602 declared no platform, and on Windows the doctor does not emit the
device-toolkit section at all: the payloads are Linux builds and the bound
a Windows toolkit states is an _MSC_VER range the report does not read.
It also ran the doctor in a fresh home, which provisioned a full toolchain
into the temporary directory: 229 s and 1.4 GB per run. It now requires
unix-shell and runs under MCPP_OFFLINE.
--offline promises never to touch the network. In a home that had never
been used, load_or_init cloned the package index through `xlings self
init` and installed ninja and patchelf through `xlings install` before any
command ran: measured, 26 s and 126 MB under MCPP_OFFLINE=1.

The three network-bound steps are now skipped under offline mode, the skip
is announced once per process, and the completion markers are still
written from what is on disk, so a bootstrapped home loses nothing. The
commands that need the tools report their absence through check_base_init.
e2e 604 asserts the fresh-home behaviour and, as its control, that a
bootstrapped home announces nothing.
…m sees it

A rule package is the thing that knows how to ask a machine what it has,
and the engine is the thing that must not. Two directives close that gap:
`mcpp:fact=<name>=<version>` states what the program established about the
machine, `mcpp:floor=<name> >= <version>` what the package needs of it. Both
fold into the runtime declarations a manifest could have carried itself, so
the existing version-floor check reads one list; it now runs a second time
after the root's build program, which is where a rule package speaks from.
Protocol 7.

MCPP_ACCEL carries the resolved device axis (`--accel` / `--no-accel` over
`[build] accel`) to build programs, so an architecture set is written once,
in the manifest. The same value now feeds the `cfg(accelerator = "...")`
layer key, a field that was declared, documented and never written. The
axis also enters the fingerprint and disqualifies the fast path: measured
before, `mcpp build --no-accel` after a device build finished in 0.00 s and
handed back the device build.
…ld narrows to it

`[build] sources` accepts `{ glob = "...", accel = "..." }` entries. The glob
joins the plain list, so every reader keeps working; the constraint is
resolved in prepare after feature application. A constrained glob that
matches nothing is refused, naming it. Under a build that asks for no
accelerator the glob is excluded through the same `!` mechanism feature gates
use, which is how one project yields its CPU-only variant. Under a build
that targets something the constraint is not within, the build is refused
naming the glob and both sides (`accel-mismatch`). Device-kind files the
effective set matches reach the build program as MCPP_DEVICE_SOURCES; the
engine has no compile rule for them and the rule package turns each into an
action. Unit tests cover the table form and its refusals; e2e 606 measures
the four outcomes with a backend nothing knows.
… and test

Three gaps a rule package that drives a compiler mcpp did not resolve fell
into, each measured on the CUDA example.

**The flags mcpp passes to its own compiler.** `MCPP_TOOLCHAIN_SYSROOT` and
`MCPP_TOOLCHAIN_BINUTILS_DIR` state the `--sysroot` and the `-B` directory,
empty when mcpp passes none. They are not `MCPP_TARGET_SYSROOT`, which is a
tier fact and empty on a hosted target: under a sub-OS the C library is not at
`/usr/include` and the assembler is not at `/usr/bin`, so a compiler that mcpp
did not place fails at the first `#include`:

    crt/host_config.h:218: fatal error: features.h: No such file or directory

`hipcc`, `-fsycl-host-compiler` and any generator that compiles what it emits
have the same gap, so the answer belongs to the engine. Both read from the
single producer that already decides them for the engine's own command lines;
`gcc::binutils_prefix_dir` now states the `-B` guard once, where three copies
of it stood and one said so in a comment.

**A dynamic build-program helper on Linux gets `DT_RPATH`.** The driver's
default is the new tag, and a runpath is consulted only for the helper's own
needed libraries. A build program that opens a host library at run time then
fails one hop later: measured, `dlopen("<sentinel>/lib/libcuda.so.1")`
answered `libdl.so.2: cannot open shared object file` while the very
directories that hold it sat in the helper's RUNPATH. The artifacts mcpp links
already carry DT_RPATH for this reason. The link policy is part of the helper's
cache identity, so an older helper is rebuilt rather than replayed.

**`--accel` / `--no-accel` on `run` and `test`.** The axis existed on `build`
only, so a project could build its CPU-only variant and not run it. Both verbs
take the same two flags, with `--no-accel` travelling as the same explicit
sentinel, and both bypass their fast path when either is given — a cached
artefact was built for whatever axis the last build used.

e2e 607 covers the chained-action shape a device link needs: an artifact-role
action's output consumed by an object-role action, with the intermediate
absent from the link line.
…at owns the tool

`mcpp self doctor` grew a CUDA section: it located a toolkit payload, read
NVIDIA's `crt/host_config.h` for the host-compiler bound, and parsed
`nvcc --dryrun` to name a back-end stage the tool could not reach. Every one of
those readings was correct, and none of them belonged to the engine.

The repository already states the rule as an invariant. `test_runtime_contract`
refuses a vendor name beside a probe launch in `src/`, and the reason is that
an engine which learns to run one vendor's tool learns to run four: the second
backend arrives as a second section, the third as a third, and the engine
acquires a table of tools it must keep current with releases it does not
control.

The same answers are now produced where the tool is known — the rule package
the project imports — and reach mcpp as declarations through the build
program's channel, which the engine compares without knowing what any of the
names mean. Nothing is lost: the CUDA example reports the driver relation, the
host-compiler bound and the unreachable stage before its first compile, and the
version floor refuses a build the machine cannot run.

`test_core_vendor_probes` states the property the removal establishes, over
comment-stripped sources so that recording a vendor's name in a comment stays
possible. It carries its own denominator: an enumeration that found fewer than
a hundred files is a broken scan rather than a clean result.

e2e 602 is removed with the section it tested. Its subject was the doctor
report, not a build, and the same preference — payload before host — is what
the example's rule package now exercises end to end.
…efuses

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.
@Sunrisepeak Sunrisepeak changed the title feat(accel): payload-first toolkit discovery, version floors, and a CUDA example that stops reaching for the host feat(accel): version floors, a probe channel, constrained device globs, and a CUDA example that takes two routes Sep 5, 2026
…re name

Two packages in one graph may share a bare name and differ only by namespace —
that is what namespaces are for — and the map that carries device sources to a
build program was keyed by the bare name at both its write site and its two
read sites. A collision would hand one package's device sources to the other's
build program, and nothing would report it: the receiving program would emit
actions for files it does not own, and the owner would emit none.

Keyed by the package root instead, which is unique by construction. Found by
re-reading the diff before merge; no test covered it, and a test that did would
need two same-named packages in one graph, which is worth adding when a second
consumer of this map appears.
The version annotations in the manual said 2026.9.6 while the release lands
today, so they name the version that actually carries them. `mcpp.toml` and
`MCPP_VERSION` move together, as `01_help_and_version.sh` requires.

The CHANGELOG's Unreleased section described the doctor's `nvcc --dryrun`
check as a shipped feature. That check left the engine in this same batch — the
reading was right and the place was wrong — so the entry is rewritten to say
where it went rather than to announce something the release does not contain.
…lling

`accel_str` prints `(none)` for an empty set so an ABI tag reads as a
sentence. `resolvedAccel` handed that spelling on as a value, and two readers
were wrong at once:

- A build program saw `MCPP_ACCEL=(none)` while the manual promised an empty
  string, so a rule package asking whether there is an accelerator got a yes
  and a backend named `(none)`. This reached every project that never
  mentioned an accelerator, not only those passing `--no-accel`.
- The fingerprint's own guard, `if (!accel.empty())`, was true for every
  project, appending `#accel=(none)` to builds that had asked for nothing —
  the opposite of what the comment beside it says.

Measured with a build program that wrote the value to a file, which is the only
way to see it: mcpp shows a build program's stdout only when it exits non-zero.

e2e 605's fourth section was titled "--no-accel empties both the variable and
the layer" and measured only the layer, which is how this passed a suite it was
meant to be covered by. It now asserts both, and the fixture prints the value
delimited — an empty warning is indistinguishable from an absent one.
The adapters and the rename are in mcpp-index#347; the plan table says so and
records the three findings that came out of measuring them — the display
spelling that leaked into a value, the two upstream couplings the CUDA recipes
now carry, and the path-index namespace behaviour that made local verification
look like a resolution failure.

The verification script travels with the plan rather than living only in a
scratch directory: it is the thing that found the `(none)` defect, and it is
what the release is checked with inside a sandbox.
…space

The rule is content this ecosystem wrote rather than anyone's upstream, so
`mcpplibs` is its namespace by the same rule the rest of the index follows, and
the module follows the namespace: `mcpplibs.rules.cuda`.

It also makes the package publishable. The index entry for it points at this
same directory inside an mcpp release tarball — the shape `grpcgen` already
uses — and a descriptor's identity has to match the manifest it points at, so a
package that stays in an `example` namespace can only ever be copied into the
index rather than referenced there.

Measured: `mcpp run` and `mcpp run --no-accel` both print 12 24 36 48 after a
clean rebuild.
… left

Five places the plan was wrong and two defects the implementation created and
fixed, each with the reading that settled it. Also what a consumer writes after
this batch, and the list of what remains with the reason each item is not done
rather than a silence that reads as done.
…nly a linked image

A package whose device code is its point declares `kind = "lib"`. Until this,
the actions its build program emitted were dropped — with a warning, and with
the archive coming out containing none of them:

    build.mcpp action 'cuda:wkv' has role = "object" but this build produces
    no executable, shared library or test binary to link its outputs into

Measured on llama.cpp's CUDA backend, which is 305 `.cu` files behind exactly
such a target: every action reported that line and the build succeeded, having
produced a library with no device code in it.

The archive rule already consumes `lu.objects`; a static library was simply
absent from the predicate that decides which link units an object action
attaches to. The objects an action produced belong there for the same reason a
compiled `.cpp`'s do — a target's content is what it was told to contain.

e2e 608 asserts the member list of the archive rather than the exit status: an
`ar` handed nothing still writes a well-formed archive and reports success.
This is the engine half of the multi-device design's C-6.
The framework batch's first item found the engine gap C-6 named and nothing
had measured, and it got as far as compiling before stopping on a payload
version rather than on anything about the design. Both readings are recorded
with what would close them.
A device unit that includes <cuda_runtime.h> stops on

    crt/host_defines.h:67: error: "libc++ is not supported on x86 system"

whenever the toolchain is LLVM, which is the toolchain the clang route exists
for. The guard is `#if defined(__CUDACC__) && … && defined(_LIBCPP_VERSION)`,
and clang defines `__CUDACC__` when it compiles CUDA itself, so the refusal —
which is about nvcc's host pass — lands on a compiler it was not written about.

The escape hatch is upstream's own and is passed only on the clang route: nvcc
really does break against libc++, and nothing here weakens that refusal.

Found on ggml's CUDA backend. The example's own kernel never showed it because
a bare kernel includes no toolkit header at all — which is worth knowing about
the example: it exercises the plumbing, not the headers.
… on the way

The chain the design is about — axis, narrowed glob, device source list, rule
package, actions, static archive, link — runs end to end and produced 48 device
objects. What stops it is a four-way payload matrix that has no satisfiable
point on this machine, and none of its four sides is mcpp's.
… not a library

A `kind = "lib"` target used to serve as "nothing that takes an object
action's outputs", because a static library did not take them. It does now, so
that fixture stopped exercising the case and the suite reported it — which is
the whole value of running it.

The one shape that still leaves an object action with nowhere to go is a target
behind an inactive `required_features`: it is absent from the build entirely.
A control runs the same project with the feature on and asserts the diagnostic
is gone, so the assertion cannot pass on a build that stopped emitting it for
everyone.
T5.1 did what a gate is for: it found the engine gap C-6 named and nothing had
exercised, and it carried the whole chain to 48 device objects before stopping
on a four-way payload matrix with no satisfiable point on this machine. The
matrix is written out with the reading for each side, and the three general
findings it produced are recorded with where they went.
…uld say which

The README claimed "no CUDA host header in the way", which stopped being true
the moment a device unit includes <cuda_runtime.h>. It says what the flag is,
why the guard fires on a compiler it was not written about, and why this
example's own kernel never showed it.
… for free

Rehearsed inside a sandbox against the previous release — the one that still
had the doctor's toolkit reader — and section E reported ok. It was watching
for a section `self doctor` only prints when a CUDA toolkit is installed, and
a fresh sandbox has none, so the assertion could not fail there.

It now reads the engine binary for the names the removed code carried.
`crt/host_config.h` appears twice in 2026.9.5.1 and not at all in 2026.9.5.2,
so the check discriminates: the control run against the older binary fails with
that exact count. The source-level property stays where it belongs, in
`test_core_vendor_probes`.

Also fixed while writing it: `grep -c` prints 0 and exits 1 when it matches
nothing, so `|| echo 0` appended a second line, `[` said "integer expression
expected", and the section reported ok having skipped every comparison.
@Sunrisepeak
Sunrisepeak merged commit d28d841 into main Sep 5, 2026
37 checks passed
@Sunrisepeak
Sunrisepeak deleted the feat/multi-device branch September 5, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants