From 6ddd13dd6120a43946b47595d12c2c86aa9d0c75 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Mon, 18 May 2026 19:06:05 +0000 Subject: [PATCH 1/9] Add CPU-only C++ build CI --- .github/workflows/pr.yaml | 21 +++++++++++++++++++++ conda/recipes/libnvforest/recipe.yaml | 1 + 2 files changed, 22 insertions(+) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 138b0bd..601f83c 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -15,6 +15,7 @@ jobs: - changed-files - checks - clang-tidy + - conda-cpp-build-cpu - conda-cpp-build - conda-cpp-tests - conda-cpp-checks @@ -169,6 +170,26 @@ jobs: arch: "amd64" container_image: "rapidsai/ci-conda:26.06-latest" script: "ci/run_clang_tidy.sh" + conda-cpp-build-cpu: + needs: [checks, changed-files] + permissions: + actions: read + contents: read + id-token: write + packages: read + pull-requests: read + secrets: inherit # zizmor: ignore[secrets-inherit] + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.06 + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp + with: + build_type: pull-request + node_type: cpu16 + arch: "amd64" + container_image: "rapidsai/ci-conda:26.06-latest" + script: | + NVFOREST_EXTRA_CMAKE_ARGS="-DNVFOREST_ENABLE_GPU=OFF" + export NVFOREST_EXTRA_CMAKE_ARGS + ci/build_cpp.sh conda-cpp-build: needs: checks permissions: diff --git a/conda/recipes/libnvforest/recipe.yaml b/conda/recipes/libnvforest/recipe.yaml index 1994538..3495392 100644 --- a/conda/recipes/libnvforest/recipe.yaml +++ b/conda/recipes/libnvforest/recipe.yaml @@ -40,6 +40,7 @@ cache: CMAKE_CXX_COMPILER_LAUNCHER: ${{ env.get("CMAKE_CXX_COMPILER_LAUNCHER") }} CMAKE_GENERATOR: ${{ env.get("CMAKE_GENERATOR") }} NVCC_APPEND_FLAGS: ${{ env.get("NVCC_APPEND_FLAGS", default="") }} + NVFOREST_EXTRA_CMAKE_ARGS: ${{ env.get("NVFOREST_EXTRA_CMAKE_ARGS", default="") }} PARALLEL_LEVEL: ${{ env.get("PARALLEL_LEVEL", default="8") }} RAPIDS_ARTIFACTS_DIR: ${{ env.get("RAPIDS_ARTIFACTS_DIR", default="") }} SCCACHE_BUCKET: ${{ env.get("SCCACHE_BUCKET", default="") }} From be89d8ee5bf875f4e7c14529bcc9e383ba81b9ca Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Mon, 18 May 2026 20:02:54 +0000 Subject: [PATCH 2/9] Fix CPU-only CI script invocation --- .github/workflows/pr.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 601f83c..591db18 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -186,10 +186,7 @@ jobs: node_type: cpu16 arch: "amd64" container_image: "rapidsai/ci-conda:26.06-latest" - script: | - NVFOREST_EXTRA_CMAKE_ARGS="-DNVFOREST_ENABLE_GPU=OFF" - export NVFOREST_EXTRA_CMAKE_ARGS - ci/build_cpp.sh + script: "env NVFOREST_EXTRA_CMAKE_ARGS=-DNVFOREST_ENABLE_GPU=OFF ci/build_cpp.sh" conda-cpp-build: needs: checks permissions: From 9b33c2b32dee92ae59e3697283d3b4383e9d1fdb Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Mon, 18 May 2026 20:28:13 +0000 Subject: [PATCH 3/9] Include CUDA runtime API in forest model --- cpp/include/nvforest/forest_model.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpp/include/nvforest/forest_model.hpp b/cpp/include/nvforest/forest_model.hpp index 783b517..f3f447a 100644 --- a/cpp/include/nvforest/forest_model.hpp +++ b/cpp/include/nvforest/forest_model.hpp @@ -11,6 +11,8 @@ #include #include +#include + #include #include #include From 471ca56bfeb3c740e53c69f9abc4267abbb652e8 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Mon, 18 May 2026 20:29:43 +0000 Subject: [PATCH 4/9] Replace importer ASSERT checks --- cpp/include/nvforest/exceptions.hpp | 5 +++-- cpp/include/nvforest/treelite_importer.hpp | 24 ++++++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/cpp/include/nvforest/exceptions.hpp b/cpp/include/nvforest/exceptions.hpp index d9e3cb7..9795dd1 100644 --- a/cpp/include/nvforest/exceptions.hpp +++ b/cpp/include/nvforest/exceptions.hpp @@ -22,11 +22,12 @@ struct unusable_model_exception : std::exception { /** Exception indicating model import failed */ struct model_import_error : std::exception { model_import_error() : model_import_error("Error while importing model") {} + model_import_error(std::string msg) : msg_{msg} {} model_import_error(char const* msg) : msg_{msg} {} - virtual char const* what() const noexcept { return msg_; } + virtual char const* what() const noexcept { return msg_.c_str(); } private: - char const* msg_; + std::string msg_; }; /** diff --git a/cpp/include/nvforest/treelite_importer.hpp b/cpp/include/nvforest/treelite_importer.hpp index 4e1b380..6fbc919 100644 --- a/cpp/include/nvforest/treelite_importer.hpp +++ b/cpp/include/nvforest/treelite_importer.hpp @@ -21,6 +21,7 @@ #include #include +#include #include namespace nvforest { @@ -351,24 +352,31 @@ struct treelite_importer { *processed_tl_model.get(), align_bytes, use_double_precision, dev_type, device, stream); } - ASSERT(tl_model.num_target == 1, "nvForest does not support multi-target model"); + if (tl_model.num_target != 1) { + throw model_import_error("nvForest does not support multi-target model"); + } // Check tree annotation (assignment) if (tl_model.task_type == treelite::TaskType::kMultiClf) { // Must be either vector leaf or grove-per-class if (tl_model.leaf_vector_shape[1] > 1) { // vector-leaf - ASSERT(tl_model.leaf_vector_shape[1] == int(tl_model.num_class[0]), - "Vector leaf must be equal to num_class = %d", - tl_model.num_class[0]); + if (tl_model.leaf_vector_shape[1] != int(tl_model.num_class[0])) { + throw model_import_error("Vector leaf must be equal to num_class = " + + std::to_string(tl_model.num_class[0])); + } auto tree_count = num_trees(tl_model); for (decltype(tree_count) tree_id = 0; tree_id < tree_count; ++tree_id) { - ASSERT(tl_model.class_id[tree_id] == -1, "Tree %d has invalid class assignment", tree_id); + if (tl_model.class_id[tree_id] != -1) { + throw model_import_error("Tree " + std::to_string(tree_id) + + " has invalid class assignment"); + } } } else { // grove-per-class auto tree_count = num_trees(tl_model); for (decltype(tree_count) tree_id = 0; tree_id < tree_count; ++tree_id) { - ASSERT(tl_model.class_id[tree_id] == int(tree_id % tl_model.num_class[0]), - "Tree %d has invalid class assignment", - tree_id); + if (tl_model.class_id[tree_id] != int(tree_id % tl_model.num_class[0])) { + throw model_import_error("Tree " + std::to_string(tree_id) + + " has invalid class assignment"); + } } } } From 5a76c2dec2331fae204274dd762f80a8cc09f301 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Mon, 18 May 2026 20:31:07 +0000 Subject: [PATCH 5/9] Use raft proto handle in importer tests --- cpp/tests/treelite_importer.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cpp/tests/treelite_importer.cpp b/cpp/tests/treelite_importer.cpp index 8f946e4..4c2b80b 100644 --- a/cpp/tests/treelite_importer.cpp +++ b/cpp/tests/treelite_importer.cpp @@ -4,6 +4,7 @@ */ #include +#include #include #include #include @@ -337,7 +338,7 @@ TEST(TreeliteImporter, DegenerateTree) auto fil_model = import_from_treelite_model(*tl_model, tree_layout::breadth_first); ASSERT_FALSE(fil_model.has_vector_leaves()); - auto handle = raft::handle_t{}; + auto handle = raft_proto::handle_t{}; auto X = std::vector{0.0}; auto preds = std::vector(1, 0.0); auto expected_preds = std::vector{1.0}; @@ -358,7 +359,7 @@ TEST(TreeliteImporter, DegenerateTreeWithVectorLeaf) auto fil_model = import_from_treelite_model(*tl_model, tree_layout::breadth_first); ASSERT_TRUE(fil_model.has_vector_leaves()); - auto handle = raft::handle_t{}; + auto handle = raft_proto::handle_t{}; auto X = std::vector{0.0}; auto preds = std::vector(2, 0.0); auto expected_preds = std::vector{0.5, 0.5}; From a839e4c7129975f143b0d9fa67e73f1ccaf5c07f Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Mon, 18 May 2026 20:51:05 +0000 Subject: [PATCH 6/9] Create real RAFT handle in GPU importer tests --- cpp/tests/treelite_importer.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cpp/tests/treelite_importer.cpp b/cpp/tests/treelite_importer.cpp index 4c2b80b..f29baad 100644 --- a/cpp/tests/treelite_importer.cpp +++ b/cpp/tests/treelite_importer.cpp @@ -338,7 +338,12 @@ TEST(TreeliteImporter, DegenerateTree) auto fil_model = import_from_treelite_model(*tl_model, tree_layout::breadth_first); ASSERT_FALSE(fil_model.has_vector_leaves()); - auto handle = raft_proto::handle_t{}; +#ifdef NVFOREST_ENABLE_GPU + auto raft_handle = raft::handle_t{}; + auto handle = raft_proto::handle_t{raft_handle}; +#else + auto handle = raft_proto::handle_t{}; +#endif auto X = std::vector{0.0}; auto preds = std::vector(1, 0.0); auto expected_preds = std::vector{1.0}; @@ -359,7 +364,12 @@ TEST(TreeliteImporter, DegenerateTreeWithVectorLeaf) auto fil_model = import_from_treelite_model(*tl_model, tree_layout::breadth_first); ASSERT_TRUE(fil_model.has_vector_leaves()); - auto handle = raft_proto::handle_t{}; +#ifdef NVFOREST_ENABLE_GPU + auto raft_handle = raft::handle_t{}; + auto handle = raft_proto::handle_t{raft_handle}; +#else + auto handle = raft_proto::handle_t{}; +#endif auto X = std::vector{0.0}; auto preds = std::vector(2, 0.0); auto expected_preds = std::vector{0.5, 0.5}; From 2091250ad51ae529c7e6b6c2384a04f5aec41555 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Mon, 18 May 2026 20:54:01 +0000 Subject: [PATCH 7/9] Validate Treelite class count before import --- cpp/include/nvforest/treelite_importer.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cpp/include/nvforest/treelite_importer.hpp b/cpp/include/nvforest/treelite_importer.hpp index 6fbc919..ccbcc13 100644 --- a/cpp/include/nvforest/treelite_importer.hpp +++ b/cpp/include/nvforest/treelite_importer.hpp @@ -357,6 +357,9 @@ struct treelite_importer { } // Check tree annotation (assignment) if (tl_model.task_type == treelite::TaskType::kMultiClf) { + if (tl_model.num_class.Empty() || tl_model.num_class[0] <= 0) { + throw model_import_error("MultiClf model must define positive num_class"); + } // Must be either vector leaf or grove-per-class if (tl_model.leaf_vector_shape[1] > 1) { // vector-leaf if (tl_model.leaf_vector_shape[1] != int(tl_model.num_class[0])) { From 7fd48e0760564c48ac82215bb88275bab4919837 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Mon, 18 May 2026 20:55:43 +0000 Subject: [PATCH 8/9] Guard CUDA runtime include in forest model --- cpp/include/nvforest/forest_model.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cpp/include/nvforest/forest_model.hpp b/cpp/include/nvforest/forest_model.hpp index f3f447a..9c07f13 100644 --- a/cpp/include/nvforest/forest_model.hpp +++ b/cpp/include/nvforest/forest_model.hpp @@ -11,7 +11,9 @@ #include #include +#ifdef NVFOREST_ENABLE_GPU #include +#endif #include #include @@ -291,7 +293,11 @@ struct forest_model { int current_device_id; if (out_mem_type == raft_proto::device_type::gpu || in_mem_type == raft_proto::device_type::gpu) { +#ifdef NVFOREST_ENABLE_GPU raft_proto::cuda_check(cudaGetDevice(¤t_device_id)); +#else + throw raft_proto::gpu_unsupported("Tried to use GPU memory in CPU-only build"); +#endif } else { current_device_id = -1; } From 042cf6522c69c5ed4aacb0b1d1eef390da941a02 Mon Sep 17 00:00:00 2001 From: Simon Adorf Date: Tue, 19 May 2026 01:27:25 +0000 Subject: [PATCH 9/9] Remove redundant model import error constructor --- cpp/include/nvforest/exceptions.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/include/nvforest/exceptions.hpp b/cpp/include/nvforest/exceptions.hpp index 9795dd1..3074934 100644 --- a/cpp/include/nvforest/exceptions.hpp +++ b/cpp/include/nvforest/exceptions.hpp @@ -23,7 +23,6 @@ struct unusable_model_exception : std::exception { struct model_import_error : std::exception { model_import_error() : model_import_error("Error while importing model") {} model_import_error(std::string msg) : msg_{msg} {} - model_import_error(char const* msg) : msg_{msg} {} virtual char const* what() const noexcept { return msg_.c_str(); } private: