· 5 min read ·

Making the CUDA Compatibility Matrix Machine-Readable with Conan and CMake

Source: isocpp

C++ dependency management has been the top pain point in the ISO C++ annual survey for years running, and JetBrains’ State of Developer Ecosystem data consistently shows C++ developers among the least satisfied with their package tooling. Most of that dissatisfaction is earned. The problem isn’t just that the tooling is immature; it’s that C++ projects routinely carry compatibility constraints that no general-purpose package manager was designed to express. CUDA is the clearest example of this.

When you build a CUDA-accelerated library, you’re navigating three independent axes simultaneously: the CUDA toolkit version used to compile, the GPU driver version installed on the target machine, and the compute capability of the physical hardware. Each axis interacts with the others in non-obvious ways. CUDA 12.4 requires a minimum driver version of 550.54.14 on Linux. A machine running driver 525 cannot execute CUDA 12.0 code. macOS support was dropped after CUDA 10.2, and Apple Silicon has no CUDA support at all, which is why projects targeting Apple hardware have pivoted to Apple’s MLX framework instead. None of this is expressible in a standard package manifest.

The conventional response is to document it. You write a compatibility table in the README, add a note to the contributing guide, and hope that everyone who builds or consumes your binaries reads it carefully. This works until it doesn’t, and it doesn’t work at the boundary where automated CI systems assemble dependencies without human review.

The more durable approach is to make these rules machine-readable in the build system itself. A talk from using std::cpp 2026 on cross-platform C++ AI development with Conan, CMake, and CUDA laid out a concrete pipeline for doing exactly this.

CMake’s CUDA Language Support

CMake 3.8 introduced CUDA as a first-class language via enable_language(CUDA), and the ecosystem has been catching up to that decision ever since. The old FindCUDA.cmake module was deprecated in CMake 3.27. The current pattern uses FindCUDAToolkit, available since CMake 3.17, which provides properly namespaced imported targets.

cmake_minimum_required(VERSION 3.17)
project(my_cuda_lib LANGUAGES CXX CUDA)

find_package(CUDAToolkit REQUIRED)

add_library(my_cuda_lib src/kernels.cu src/host.cpp)

target_link_libraries(my_cuda_lib
    PRIVATE
        CUDA::cudart
        CUDA::cublas
        CUDA::cublasLt
)

# CUDA::cuda_driver links against the stub library,
# which allows the binary to build on GPU-less CI machines
# without a real driver present at link time.
target_link_libraries(my_cuda_lib PRIVATE CUDA::cuda_driver)

The CMAKE_CUDA_ARCHITECTURES variable controls which GPU generations to compile for. Since CMake 3.23 and 3.24, it accepts the special values all, all-major, and native in addition to explicit lists of compute capability numbers.

# Explicit list targeting Turing, Ampere, and Ada Lovelace
set(CMAKE_CUDA_ARCHITECTURES "75;80;86;89;90")

# Or compile for the GPU installed on the current machine
set(CMAKE_CUDA_ARCHITECTURES native)

# Or compile for all major architectures in the installed toolkit
set(CMAKE_CUDA_ARCHITECTURES all-major)

The native option is useful for development builds. The all-major option is appropriate when you’re producing binaries for distribution and don’t know the target hardware in advance. Explicit lists are the right choice when you need to bound binary size or when you’re targeting a known fleet of hardware.

Encoding CUDA Compatibility in Conan

CMake handles the compilation side. The harder problem is binary compatibility across consumers of your package. A binary compiled against CUDA 12.0 may not be compatible with a consumer whose environment has CUDA 11.8. Standard Conan settings don’t capture this because cuda_version isn’t part of the default settings model.

Conan 2.x addresses this through two mechanisms. First, you can add CUDA as a custom setting in settings_user.yml:

cuda_version:
    "11.8":
    "12.0":
    "12.1":
    "12.2":
    "12.3":
    "12.4":

Second, and more importantly, you can encode the actual ABI compatibility rules in a compatibility plugin at ~/.conan2/extensions/plugins/compatibility.py.

def compatibility(conanfile):
    if not hasattr(conanfile, 'settings') or not conanfile.settings.get_safe('cuda_version'):
        return []

    cuda_version = str(conanfile.settings.cuda_version)
    major = int(cuda_version.split('.')[0])
    minor = int(cuda_version.split('.')[1])

    # CUDA minor versions within the same major are generally
    # forward-compatible for the runtime API. A binary built
    # against 12.0 can run on a system with the 12.4 toolkit.
    compatible_versions = []
    for m in range(minor + 1, 10):
        compatible_versions.append(
            [{"cuda_version": f"{major}.{m}"}]
        )

    return compatible_versions

With this plugin in place, conan install can find a binary built against CUDA 12.0 when the consumer’s environment declares CUDA 12.4, because the plugin has told Conan that 12.0 binaries satisfy 12.4 requirements. Without the plugin, Conan treats these as distinct binary packages and either rebuilds from source or fails.

The conanfile.py for a CUDA-enabled library then looks like this:

from conan import ConanFile
from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout

class MyCudaLib(ConanFile):
    name = "my_cuda_lib"
    version = "1.0"
    settings = "os", "compiler", "build_type", "arch", "cuda_version"
    generators = "CMakeToolchain", "CMakeDeps"

    def layout(self):
        cmake_layout(self)

    def generate(self):
        tc = CMakeToolchain(self)
        cuda_ver = self.settings.get_safe("cuda_version")
        if cuda_ver:
            # Pass the CUDA version to CMake so it can
            # validate against the installed toolkit.
            tc.variables["EXPECTED_CUDA_VERSION"] = cuda_ver
        tc.generate()

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def package(self):
        cmake = CMake(self)
        cmake.install()

    def package_info(self):
        self.cpp_info.libs = ["my_cuda_lib"]

The full build pipeline then reduces to three commands:

conan install . --build=missing
cmake --preset conan-release
cmake --build build/Release

This is the pipeline that projects like Faiss and llama.cpp are moving toward. Faiss has Conan Center Index recipes with CUDA options. llama.cpp uses CMake’s native CUDA language support directly.

What This Actually Solves

The compatibility plugin doesn’t eliminate the CUDA matrix; it relocates it. The matrix still exists, and the rules encoded in the plugin still require someone who understands CUDA’s ABI guarantees to write them correctly. What changes is where those rules live and how they’re enforced.

When the rules live in a README, they’re checked by humans who may or may not read the README before building. When they live in a Conan compatibility plugin, they’re checked by the package manager on every conan install invocation. The rules become part of the build graph rather than documentation that shadows it.

For GPU-less CI machines, the CUDA::cuda_driver stub target in FindCUDAToolkit handles the link step without requiring a real driver. The combination of stub targets for CI and compatibility plugins for binary resolution covers the two scenarios where CUDA’s constraints most often surface as silent failures: builds that succeed on a developer’s GPU workstation but fail in CI, and package installations that silently pull incompatible binaries because the version mismatch wasn’t encoded anywhere the tooling could see.

The broader point is that C++ build systems have accumulated enough machinery to express constraints that used to require documentation. Using that machinery consistently is a separate discipline from writing the C++ itself, but it’s the discipline that determines whether a project’s build is reproducible across environments that the original author never tested.

Was this interesting?