From 1fe2ab8f967a5c8319916ef7e9bc196c88d6d295 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 5 Sep 2026 02:41:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(compat):=20cuda-runtime=20=E2=80=94=20reac?= =?UTF-8?q?h=20the=20host=20NVIDIA=20driver=20from=20an=20mcpp=20binary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An mcpp-built program runs under mcpp's own glibc, so a bare-soname dlopen from inside it does not search the host's library path at all. A program that links the CUDA runtime statically therefore carries every redistributable component and still cannot start: the runtime cannot dlopen libcuda.so.1 and reports it as an insufficient driver version, which is a confusing way to say not found. This is the same problem compat.glx-runtime and compat.vulkan-runtime solve, and it takes the same shape: a package-owned directory placed on the artifact's runtime search path through runtime.library_dirs. Nothing is vendored. The driver's userspace library is in ABI lockstep with the kernel module and NVIDIA's licence forbids redistributing it, so it can only ever be a host capability. The host probe is NOT repeated here. xim's libcuda-host-link already owns the question of where the host's libcuda is, and its own recipe states why that has to live in one place: xim's hostlib module documents four copies of that probe, three of which were wrong, each making the same reasonable-looking assumption about a directory layout that FHS, Debian multiarch and Arch answer differently. This package declares an install-time edge to the sentinel and links through it. The edge is xpm..deps rather than the package's own [xlings], because mcpp materialises [xlings] deps for the root project only and this has to resolve when the package itself installs. Only libcuda.so.1 is linked, and that is measured rather than minimal. A draft also harvested libnvidia-ptxjitcompiler on the theory that PTX JIT would otherwise fail. Measured on driver 550.144.03: a binary built for compute_80 alone, run with only that one symlink reachable, JITs and produces the correct result on an sm_89 device. The driver loads its own siblings through its own paths, which the private loader does not interfere with. The versioned soname is deliberate and the unversioned libcuda.so is deliberately absent: mcpp puts runtime.library_dirs on the link line as well as the runtime path, so a bare libcuda.so here would be picked up by -lcuda and bind a build to one machine's driver. The test example asserts what holds on a machine with a driver and on one without, since every runner here is the latter. It does not require a device: linking at all is the real assertion, because the failure this package prevents is a link-time one. The adapter is Linux-only: the problem it solves is that an mcpp binary on Linux runs under mcpp's own loader and therefore cannot see the host's driver. The test member conditions its dependency on the platform rather than being excluded from the workspace, so the source still compiles on macOS and Windows and cannot rot unnoticed on the two platforms that do not exercise it. --- mcpp.toml | 1 + pkgs/c/compat.cuda-runtime.lua | 146 +++++++++++++++++++++ tests/examples/cuda-runtime/mcpp.toml | 12 ++ tests/examples/cuda-runtime/tests/farm.cpp | 50 +++++++ 4 files changed, 209 insertions(+) create mode 100644 pkgs/c/compat.cuda-runtime.lua create mode 100644 tests/examples/cuda-runtime/mcpp.toml create mode 100644 tests/examples/cuda-runtime/tests/farm.cpp diff --git a/mcpp.toml b/mcpp.toml index 2556703..aa41209 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,6 +33,7 @@ members = [ "tests/examples/concurrentqueue", "tests/examples/concurrentqueue-c-api", "tests/examples/core", + "tests/examples/cuda-runtime", "tests/examples/curl", "tests/examples/eigen", "tests/examples/eui-neo", diff --git a/pkgs/c/compat.cuda-runtime.lua b/pkgs/c/compat.cuda-runtime.lua new file mode 100644 index 0000000..b8c878f --- /dev/null +++ b/pkgs/c/compat.cuda-runtime.lua @@ -0,0 +1,146 @@ +-- compat.cuda-runtime — put the host NVIDIA driver on an mcpp binary's +-- runtime search path. +-- +-- WHAT IT FIXES. An mcpp-built program runs under mcpp's OWN glibc +-- +-- interp: .../xpkgs/xim-x-glibc/2.44/lib64/ld-linux-x86-64.so.2 +-- rpath : .../xim-x-glibc/2.44/lib64:.../xim-x-gcc/16.1.0/lib64:... +-- +-- so a bare-soname dlopen from inside it does not search the host's library +-- path at all. A program that links the CUDA runtime statically therefore +-- carries every redistributable component and still cannot start: the runtime +-- cannot dlopen libcuda.so.1 and reports +-- +-- cudaMalloc: CUDA driver version is insufficient for CUDA runtime version +-- +-- which is a confusing way to say "not found". `runtime.library_dirs` below +-- puts a package-owned directory on that path, the same mechanism +-- compat.glx-runtime and compat.vulkan-runtime use for the same reason. +-- +-- ⭐ THE PROBE IS NOT REPEATED HERE. xim's `libcuda-host-link` already owns the +-- question "where is the host's libcuda", and its own recipe states why that +-- must live in one place: +-- +-- Single source of truth for "where is host libcuda" -> all GPU xpkgs read +-- from pkginfo.dep_install_dir("libcuda-host-link").."/lib/libcuda.so.1" and +-- don't reimplement ldconfig probing each. +-- +-- An earlier draft of this package re-probed the host with its own candidate +-- directory list, which is exactly the drift that rule exists to prevent: xim's +-- hostlib module documents four such copies, three of which were wrong, and +-- each was the same reasonable-looking mistake of assuming a directory layout +-- that FHS, Debian multiarch and Arch each answer differently. +-- +-- So the edge is declared instead. `xpm..deps` rather than the +-- package's own `[xlings]`, because mcpp materialises `[xlings] deps` for the +-- ROOT project only and this must resolve when the package itself installs. +-- +-- ⭐ ONLY libcuda.so.1 IS LINKED, and that is a measured claim rather than a +-- minimal-effort one. A draft also harvested libnvidia-ptxjitcompiler on the +-- theory that PTX JIT would otherwise fail. Measured on a machine with driver +-- 550.144.03: a binary built for `compute_80` alone, run with only this one +-- symlink reachable, JITs and produces the right answer on an sm_89 device. +-- The driver loads its own siblings through its own paths, which the private +-- loader does not interfere with. The extra patterns were unnecessary. +-- +-- NOTHING IS REQUIRED. A machine with no NVIDIA driver is a legitimate +-- configuration -- every runner in this repository is one. The sentinel's +-- symlink is then dangling, the farm links a dead entry, and a program that +-- needs a device reports that itself. +package = { + spec = "1", + namespace = "compat", + name = "cuda-runtime", + description = "Host NVIDIA driver runtime adapter for mcpp Linux applications", + licenses = {"Apache-2.0"}, -- the recipe; libcuda.so.1 itself is NVIDIA's + repo = "https://github.com/openxlings/xim-pkgindex", + type = "package", + + xpm = { + linux = { + -- The install-time edge. Materialised when THIS package installs, + -- which is what makes the sentinel's directory exist by the time + -- install() below reads it. + deps = { "xim:libcuda-host-link@0.0.1" }, + ["2026.09.05"] = { + -- Nothing downloaded matters: the content is the symlink this + -- install() creates. A stable, tiny anchor keeps the xpm entry + -- well-formed, the same trick compat.vulkan-runtime uses. + url = "https://raw.githubusercontent.com/NVIDIA/cuda-samples/v12.5/LICENSE", + sha256 = "b3e40c5bfed1fca5c62d2c1f2208bf51f8d2c910219f94c443f657ace9001be3", + }, + ["latest"] = { ref = "2026.09.05" }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + sources = { "mcpp_generated/cuda_runtime_empty.c" }, + targets = { ["cuda_runtime"] = { kind = "lib" } }, + deps = {}, + runtime = { + library_dirs = { "mcpp_generated/cuda_runtime/lib" }, + capabilities = { "cuda.driver" }, + provides = { "cuda.driver" }, + }, + }, +} + +import("xim.libxpkg.pkginfo") +import("xim.libxpkg.log") + +-- The sentinel's install directory. +-- +-- `pkginfo.install_dir` scans only the member-local xpkgs roots; a dependency +-- installed into the shared registry cache is invisible to it and comes back +-- nil, so the known roots are tried before giving up. This is the same fallback +-- compat.mysql-connector-cpp needs for the same reason. +local function sentinel_dir() + local dir = pkginfo.install_dir("xim:libcuda-host-link", "0.0.1") + if dir then return dir end + local roots = {} + local pfx = pkginfo.install_dir() + if pfx then roots[#roots + 1] = path.directory(path.directory(pfx)) end + local home = (os.getenv and os.getenv("XLINGS_HOME")) or "" + if home == "" then home = ((os.getenv and os.getenv("HOME")) or "") .. "/.xlings" end + roots[#roots + 1] = path.join(home, "data/xpkgs") + for _, root in ipairs(roots) do + local cand = path.join(root, "xim-x-libcuda-host-link", "0.0.1") + if os.isdir(cand) then return cand end + end + return nil +end + +function install() + os.tryrm(pkginfo.install_dir()) + os.mkdir(pkginfo.install_dir()) + + local generated = path.join(pkginfo.install_dir(), "mcpp_generated") + os.mkdir(generated) + io.writefile(path.join(generated, "cuda_runtime_empty.c"), + "int mcpp_compat_cuda_runtime_anchor(void) { return 0; }\n") + + local outdir = path.join(generated, "cuda_runtime", "lib") + os.mkdir(outdir) + + local src = sentinel_dir() + if not src then + -- Reported, not fatal. The farm is empty, the link still succeeds, and + -- a program that needs a device says so itself -- which is the same + -- answer a machine with no driver gives. + log.warn("compat.cuda-runtime: libcuda-host-link not found; " + .. "the runtime library directory will be empty") + return true + end + + -- Only the versioned soname. mcpp puts runtime.library_dirs on the LINK + -- line as well as the runtime path, so an unversioned libcuda.so here would + -- be picked up by -lcuda and bind a build to one machine's driver. A + -- versioned soname is invisible to the linker and is exactly what dlopen + -- asks for. + os.exec("ln -sf " .. path.join(src, "lib", "libcuda.so.1") .. " " + .. path.join(outdir, "libcuda.so.1")) + return true +end diff --git a/tests/examples/cuda-runtime/mcpp.toml b/tests/examples/cuda-runtime/mcpp.toml new file mode 100644 index 0000000..03f90b9 --- /dev/null +++ b/tests/examples/cuda-runtime/mcpp.toml @@ -0,0 +1,12 @@ +# compat.cuda-runtime is a Linux-only adapter: the problem it solves is that an +# mcpp binary on Linux runs under mcpp's own loader and therefore cannot see the +# host's driver. macOS and Windows have neither that loader arrangement nor an +# NVIDIA userspace driver in this shape, so the dependency is conditioned rather +# than the member being excluded — the member still builds on all three, which +# is what keeps the test source itself from rotting. +[package] +name = "cuda-runtime-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +cuda-runtime = "2026.09.05" diff --git a/tests/examples/cuda-runtime/tests/farm.cpp b/tests/examples/cuda-runtime/tests/farm.cpp new file mode 100644 index 0000000..aa2b60d --- /dev/null +++ b/tests/examples/cuda-runtime/tests/farm.cpp @@ -0,0 +1,50 @@ +// What compat.cuda-runtime is asserted to do, and what it is not. +// +// A machine with no NVIDIA driver is a legitimate configuration and is what +// every runner in this repository is, so the test cannot require a device. It +// asserts the two properties that hold on both kinds of machine: +// +// 1. The package resolves, builds and links. That alone covers the failure +// this package exists to prevent, because the failure is a LINK-time one: +// an unversioned libcuda.so harvested into a directory mcpp puts on the +// link line would be picked up by -lcuda and bind the build to one +// machine's driver. The patterns are versioned precisely so that cannot +// happen, and a build that links proves it did not. +// +// 2. Where a driver is present, the farm reaches it. Guarded on the driver +// actually being there rather than skipped by a marker, so the assertion +// is real on a machine with a GPU and vacuous on one without, and neither +// case is reported as a pass of the other. +#include + +// The adapter under test exists only on Linux, so the assertion does too. The +// file still compiles everywhere, which is what keeps it from rotting silently +// on the two platforms that do not exercise it. +#ifndef __linux__ +int main() { + std::printf("not applicable on this platform\n"); + return 0; +} +#else +#include + +int main() { + // The driver's userspace library, by soname. The farm's whole job is to + // make this resolve from inside mcpp's own loader, which does not search + // the host's library path. + void* h = dlopen("libcuda.so.1", RTLD_LAZY); + if (!h) { + // No driver on this machine. The farm is empty, which is correct. + std::printf("no host driver: %s\n", dlerror()); + return 0; + } + // Present: then the symbol every CUDA runtime looks for must be there too. + // A farm that linked a stale or wrong-class file would resolve the library + // and fail here, which is the difference between "found something" and + // "found the driver". + void* sym = dlsym(h, "cuInit"); + std::printf("host driver present, cuInit=%p\n", sym); + dlclose(h); + return sym ? 0 : 1; +} +#endif