Repository: conan-io/conan-center-index

Issue:
[request] stdgpu
### Webpage

https://stotko.github.io/stdgpu/

### Source code

https://github.com/stotko/stdgpu

### Description of the library/tool

stdgpu is an open-source library providing several generic GPU data structures for fast and reliable data management. Multiple platforms such as CUDA, OpenMP, and HIP are supported allowing you to rapidly write highly complex agnostic and native algorithms that look like sequential CPU code but are executed in parallel on the GPU.

The CUDA and HIP portions of the code are almost, but not entirely, header-only. I suppose ConanCenter could limit the builds to OpenMP-only due to lack of CI support and packaging conventions for the CUDA/HIP portions.


Discussion:

Resolving patch:
diff --git a/recipes/stdgpu/all/conanfile.py b/recipes/stdgpu/all/conanfile.py
new file mode 100644
--- /dev/null
+++ b/recipes/stdgpu/all/conanfile.py
@@ -0,0 +1,213 @@
+import os
+
+from conan import ConanFile
+from conan.errors import ConanInvalidConfiguration
+from conan.tools.build import check_min_cppstd
+from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
+from conan.tools.env import VirtualBuildEnv
+from conan.tools.files import copy, get, rm, rmdir, replace_in_file
+from conan.tools.microsoft import is_msvc
+from conan.tools.scm import Version
+
+required_conan_version = ">=1.53.0"
+
+
+class StdgpuConan(ConanFile):
+    name = "stdgpu"
+    description = "Efficient STL-like Data Structures on the GPU"
+    license = "Apache-2.0"
+    url = "https://github.com/conan-io/conan-center-index"
+    homepage = "https://stotko.github.io/stdgpu/"
+    topics = ("cuda", "data-structures", "gpgpu", "gpu", "hip", "openmp", "rocm", "stl", "thrust")
+
+    package_type = "library"
+    settings = "os", "arch", "compiler", "build_type"
+    options = {
+        "shared": [True, False],
+        "fPIC": [True, False],
+        "backend": ["cuda", "openmp", "hip"],
+        "setup_compiler_flags": [True, False],
+        "enable_contract_checks": [None, True, False],
+        "use_32_bit_index": [True, False],
+        "openmp": ["llvm", "system"],
+    }
+    default_options = {
+        "shared": False,
+        "fPIC": True,
+        "backend": "openmp",
+        "setup_compiler_flags": False,
+        "enable_contract_checks": None,
+        "use_32_bit_index": True,
+        "openmp": "llvm",
+    }
+
+    @property
+    def _min_cppstd(self):
+        if self.version == "1.3.0":
+            return 14
+        else:
+            return 17
+
+    @property
+    def _compilers_min_versions(self):
+        if self.version == "1.3.0":
+            # Based on https://github.com/stotko/stdgpu/tree/1.3.0#building
+            return {
+                "gcc": "7",
+                "clang": "6",
+                "apple-clang": "6",
+                "msvc": "192",
+                "Visual Studio": "16",
+            }
+        else:
+            # > 1.3.0
+            # Based on https://github.com/stotko/stdgpu/tree/32e0517#building
+            return {
+                "gcc": "9",
+                "clang": "10",
+                "apple-clang": "12",
+                "msvc": "192",
+                "Visual Studio": "16",
+            }
+
+    def export_sources(self):
+        copy(self, "cmake/*", dst=self.export_sources_folder, src=self.recipe_folder)
+
+    def config_options(self):
+        if self.settings.os == "Windows":
+            del self.options.fPIC
+
+    def configure(self):
+        if self.options.shared:
+            self.options.rm_safe("fPIC")
+        if self.options.backend == "cuda":
+            self.options["thrust"].device_system = "cuda"
+        elif self.options.backend == "openmp":
+            self.options["thrust"].device_system = "omp"
+
+    def package_id(self):
+        if self.info.options.backend != "openmp":
+            del self.info.options.openmp
+
+    def layout(self):
+        cmake_layout(self, src_folder="src")
+
+    def requirements(self):
+        if self.options.backend != "hip":
+            self.requires("thrust/1.17.2", transitive_headers=True)
+        else:
+            # The baseline Thrust version provided by Nvidia and Conan is not compatible with HIP.
+            self.output.warning("HIP support requires Thrust with ROCm. "
+                                "Using Thrust from system instead of Conan.")
+        if self.options.backend == "openmp":
+            if self.options.openmp == "llvm":
+                self.requires("llvm-openmp/12.0.1", transitive_headers=True, transitive_libs=True)
+            else:
+                self.output.info("Using OpenMP backend with system OpenMP")
+
+    def build_requirements(self):
+        if Version(self.version) > "1.3.0":
+            self.tool_requires("cmake/[>=3.18 <4]")
+
+    def validate(self):
+        if self.settings.compiler.cppstd:
+            check_min_cppstd(self, self._min_cppstd)
+        minimum_version = self._compilers_min_versions.get(str(self.settings.compiler), False)
+        if minimum_version and Version(self.settings.compiler.version) < minimum_version:
+            raise ConanInvalidConfiguration(
+                f"{self.ref} requires C++{self._min_cppstd}, which your compiler does not support."
+            )
+        if self.options.backend == "cuda" and self.dependencies["thrust"].options.device_system != "cuda":
+            raise ConanInvalidConfiguration(f"{self.ref} option backend=cuda should use '-o thrust/*:device_system=cuda' as well.")
+        if self.options.backend == "openmp" and self.dependencies["thrust"].options.device_system != "omp":
+            raise ConanInvalidConfiguration(f"{self.ref} option backend=openmp should use '-o thrust/*:device_system=omp' as well.")
+
+    def source(self):
+        get(self, **self.conan_data["sources"][self.version], strip_root=True)
+
+    def generate(self):
+        tc = CMakeToolchain(self)
+        # All the main params from https://github.com/stotko/#integration
+        backend = str(self.options.backend).upper()
+        tc.variables["STDGPU_BACKEND"] = f"STDGPU_BACKEND_{backend}"
+        tc.variables["STDGPU_BUILD_SHARED_LIBS"] = self.options.shared
+        tc.variables["STDGPU_SETUP_COMPILER_FLAGS"] = self.options.setup_compiler_flags
+        tc.variables["STDGPU_TREAT_WARNINGS_AS_ERRORS"] = False
+        tc.variables["STDGPU_BUILD_EXAMPLES"] = False
+        tc.variables["STDGPU_BUILD_BENCHMARKS"] = False
+        tc.variables["STDGPU_BUILD_TESTS"] = False
+        tc.variables["STDGPU_BUILD_TEST_COVERAGE"] = False
+        tc.variables["STDGPU_ANALYZE_WITH_CLANG_TIDY"] = False
+        tc.variables["STDGPU_ANALYZE_WITH_CPPCHECK"] = False
+        if self.options.enable_contract_checks is not None:
+            tc.variables["STDGPU_ENABLE_CONTRACT_CHECKS"] = self.options.enable_contract_checks
+        tc.variables["STDGPU_USE_32_BIT_INDEX"] = self.options.use_32_bit_index
+        tc.preprocessor_definitions["THRUST_IGNORE_CUB_VERSION_CHECK"] = "1"
+        tc.generate()
+        deps = CMakeDeps(self)
+        # FIXME: should be set by the thrust recipe instead
+        deps.set_property("thrust", "cmake_find_mode", "both")
+        deps.set_property("openmp", "cmake_find_mode", "both")
+        deps.generate()
+        env = VirtualBuildEnv(self)
+        env.generate()
+
+    def _patch_sources(self):
+        # Fix repeated application of the THRUST_DEVICE_SYSTEM define
+        for backend in ["cuda", "openmp"]:
+            replace_in_file(self, os.path.join(self.source_folder, "src", "stdgpu", backend, "CMakeLists.txt"),
+                            "THRUST_DEVICE_SYSTEM=",
+                            ") # THRUST_DEVICE_SYSTEM=")
+        replace_in_file(self, os.path.join(self.source_folder, "src", "stdgpu", "openmp", "CMakeLists.txt"),
+                        "find_package(OpenMP 2.0 REQUIRED)",
+                        "find_package(OpenMP REQUIRED)")
+
+    def build(self):
+        self._patch_sources()
+        cmake = CMake(self)
+        cmake.configure()
+        cmake.build()
+
+    def package(self):
+        copy(self, "LICENSE", dst=os.path.join(self.package_folder, "licenses"), src=self.source_folder)
+        cmake = CMake(self)
+        cmake.install()
+        rmdir(self, os.path.join(self.package_folder, "lib", "cmake"))
+        rm(self, "*.pdb", self.package_folder, recursive=True)
+        copy(self, "*.cmake",
+             dst=os.path.join(self.package_folder, "lib", "cmake"),
+             src=os.path.join(self.export_sources_folder, "cmake"))
+
+    def _configure_system_openmp(self):
+        openmp_flags = []
+        if is_msvc(self):
+            openmp_flags = ["-openmp"]
+        elif self.settings.compiler in ["clang", "apple-clang"]:
+            openmp_flags = ["-Xpreprocessor", "-fopenmp"]
+        elif self.settings.compiler == "gcc":
+            openmp_flags = ["-fopenmp"]
+        elif self.settings.compiler == "intel":
+            openmp_flags = ["/Qopenmp"] if self.settings.os == "Windows" else ["-Qopenmp"]
+        self.cpp_info.cflags += openmp_flags
+        self.cpp_info.cxxflags += openmp_flags
+        self.cpp_info.sharedlinkflags += openmp_flags
+        self.cpp_info.exelinkflags += openmp_flags
+        if self.settings.os == "Windows":
+            if is_msvc(self):
+                self.cpp_info.system_libs.append("delayimp")
+            elif self.settings.compiler == "gcc":
+                self.cpp_info.system_libs.append("gomp")
+
+    def package_info(self):
+        self.cpp_info.libs = ["stdgpu"]
+
+        if self.options.backend == "openmp":
+            if self.options.openmp == "system":
+                self._configure_system_openmp()
+        elif self.options.backend == "cuda":
+            module_path = os.path.join("lib", "cmake", "stdgpu-dependencies-cuda.cmake")
+            self.cpp_info.set_property("cmake_build_modules", [module_path])
+        elif self.options.backend == "hip":
+            module_path = os.path.join("lib", "cmake", "stdgpu-dependencies-hip.cmake")
+            self.cpp_info.set_property("cmake_build_modules", [module_path])
+        self.cpp_info.builddirs.append(os.path.join("lib", "cmake"))


Test patch:
diff --git a/recipes/stdgpu/all/test_package/CMakeLists.txt b/recipes/stdgpu/all/test_package/CMakeLists.txt
new file mode 100644
--- /dev/null
+++ b/recipes/stdgpu/all/test_package/CMakeLists.txt
@@ -0,0 +1,35 @@
+cmake_minimum_required(VERSION 3.15)
+project(test_package CXX)
+
+find_package(stdgpu CONFIG REQUIRED)
+
+if (stdgpu_VERSION_STRING EQUAL "1.3.0")
+    set(CMAKE_CXX_STANDARD 14)
+else()
+    set(CMAKE_CXX_STANDARD 17)
+endif()
+
+if (STDGPU_BACKEND STREQUAL "cuda")
+    cmake_minimum_required(VERSION 3.17 FATAL_ERROR)
+    enable_language(CUDA)
+    add_executable(${PROJECT_NAME}_cuda test_package_cuda.cu)
+    target_link_libraries(${PROJECT_NAME}_cuda stdgpu::stdgpu)
+    set_target_properties(${PROJECT_NAME}_cuda PROPERTIES CUDA_ARCHITECTURES OFF)
+endif()
+if (STDGPU_BACKEND STREQUAL "hip")
+    cmake_minimum_required(VERSION 3.21 FATAL_ERROR)
+    enable_language(HIP)
+    add_executable(${PROJECT_NAME}_hip test_package_hip.hip)
+    target_link_libraries(${PROJECT_NAME}_hip stdgpu::stdgpu)
+    set_target_properties(${PROJECT_NAME}_hip PROPERTIES HIP_ARCHITECTURES OFF)
+endif()
+if (STDGPU_BACKEND STREQUAL "openmp")
+    add_executable(${PROJECT_NAME}_openmp test_package_openmp.cpp)
+    target_link_libraries(${PROJECT_NAME}_openmp stdgpu::stdgpu)
+endif()
+
+if (NOT stdgpu_VERSION_STRING STREQUAL "1.3.0")
+    # v1.3.0 does not have stdgpu/device.h
+    add_executable(${PROJECT_NAME}_basic test_package_basic.cpp)
+    target_link_libraries(${PROJECT_NAME}_basic stdgpu::stdgpu)
+endif()
diff --git a/recipes/stdgpu/all/test_package/conanfile.py b/recipes/stdgpu/all/test_package/conanfile.py
new file mode 100644
--- /dev/null
+++ b/recipes/stdgpu/all/test_package/conanfile.py
@@ -0,0 +1,39 @@
+import os
+
+from conan import ConanFile
+from conan.tools.build import can_run
+from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
+
+
+class TestPackageConan(ConanFile):
+    settings = "os", "arch", "compiler", "build_type"
+    generators = "CMakeDeps", "VirtualRunEnv"
+    test_type = "explicit"
+
+    def layout(self):
+        cmake_layout(self)
+
+    def requirements(self):
+        self.requires(self.tested_reference_str)
+
+    def generate(self):
+        tc = CMakeToolchain(self)
+        backend = str(self.dependencies["stdgpu"].options.backend)
+        tc.variables["STDGPU_BACKEND"] = backend
+        tc.generate()
+
+    def build(self):
+        cmake = CMake(self)
+        cmake.configure()
+        cmake.build()
+
+    def test(self):
+        if can_run(self):
+            for variant in ["basic", "openmp", "cuda", "hip"]:
+                if variant == "openmp" and self.settings.compiler == "apple-clang" and self.settings.build_type == "Debug":
+                    # Skip OpenMP test for apple-clang in debug mode, which segfaults for some reason
+                    # https://c3i.jfrog.io/c3i/misc/summary.html?json=https://c3i.jfrog.io/c3i/misc/logs/pr/17542/12-macos-clang/stdgpu/cci.20230903/summary.json
+                    continue
+                bin_path = os.path.join(self.cpp.build.bindir, f"test_package_{variant}")
+                if os.path.exists(bin_path):
+                    self.run(bin_path, env="conanrun")
diff --git a/recipes/stdgpu/all/test_package/test_package_basic.cpp b/recipes/stdgpu/all/test_package/test_package_basic.cpp
new file mode 100644
--- /dev/null
+++ b/recipes/stdgpu/all/test_package/test_package_basic.cpp
@@ -0,0 +1,10 @@
+#include <stdgpu/config.h>
+#include <stdgpu/device.h>
+
+#include <iostream>
+
+int main() {
+    // Minimal test to confirm that the non-platform-specific portion of the library links correctly
+    std::cout << "stdgpu version: " << STDGPU_VERSION_STRING << std::endl;
+    stdgpu::print_device_information();
+}
diff --git a/recipes/stdgpu/all/test_package/test_package_cuda.cu b/recipes/stdgpu/all/test_package/test_package_cuda.cu
new file mode 100644
--- /dev/null
+++ b/recipes/stdgpu/all/test_package/test_package_cuda.cu
@@ -0,0 +1,55 @@
+// Based on https://github.com/stotko/stdgpu/blob/32e0517/examples/cuda/vector.cu
+#include <thrust/copy.h>
+#include <thrust/reduce.h>
+#include <thrust/sequence.h>
+
+#include <stdgpu/iterator.h> // device_begin, device_end
+#include <stdgpu/memory.h>   // createDeviceArray, destroyDeviceArray
+#include <stdgpu/platform.h> // STDGPU_HOST_DEVICE
+#include <stdgpu/vector.cuh> // stdgpu::vector
+
+#include <cstdlib>
+#include <iostream>
+
+__global__ void insert_neighbors_with_duplicates(const int *d_input, const stdgpu::index_t n,
+                                                 stdgpu::vector<int> vec) {
+    stdgpu::index_t i = static_cast<stdgpu::index_t>(blockIdx.x * blockDim.x + threadIdx.x);
+
+    if (i >= n)
+        return;
+
+    int num = d_input[i];
+    int num_neighborhood[3] = {num - 1, num, num + 1};
+
+    for (int num_neighbor : num_neighborhood) {
+        vec.push_back(num_neighbor);
+    }
+}
+
+int sum_stdgpu(stdgpu::index_t n) {
+    int *d_input = createDeviceArray<int>(n);
+    auto vec = stdgpu::vector<int>::createDeviceObject(3 * n);
+
+    thrust::sequence(stdgpu::device_begin(d_input), stdgpu::device_end(d_input), 1);
+
+    stdgpu::index_t threads = 32;
+    stdgpu::index_t blocks = (n + threads - 1) / threads;
+    insert_neighbors_with_duplicates<<<static_cast<unsigned int>(blocks),
+                                       static_cast<unsigned int>(threads)>>>(d_input, n, vec);
+    cudaDeviceSynchronize();
+
+    auto range_vec = vec.device_range();
+    int sum = thrust::reduce(range_vec.begin(), range_vec.end(), 0, thrust::plus<int>());
+
+    destroyDeviceArray<int>(d_input);
+    stdgpu::vector<int>::destroyDeviceObject(vec);
+
+    return sum;
+}
+
+int main() {
+    const int n = 20;
+    const int sum_closed_form = 3 * (n * (n + 1) / 2);
+    std::cout << "Sum: " << sum_stdgpu(n) << ", expected: " << sum_closed_form << std::endl;
+    return EXIT_SUCCESS;
+}
diff --git a/recipes/stdgpu/all/test_package/test_package_hip.hip b/recipes/stdgpu/all/test_package/test_package_hip.hip
new file mode 100644
--- /dev/null
+++ b/recipes/stdgpu/all/test_package/test_package_hip.hip
@@ -0,0 +1,64 @@
+// https://github.com/stotko/stdgpu/blob/50530de/examples/hip/ranges.hip
+/*
+ *  Copyright 2020 Patrick Stotko
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+#include <iostream>
+#include <thrust/reduce.h>
+#include <thrust/sequence.h>
+#include <thrust/transform.h>
+
+#include <stdgpu/iterator.h>        // device_begin, device_end
+#include <stdgpu/memory.h>          // createDeviceArray, destroyDeviceArray
+#include <stdgpu/platform.h>        // STDGPU_HOST_DEVICE
+#include <stdgpu/ranges.h>          // device_range
+#include <stdgpu/unordered_set.cuh> // stdgpu::unordered_set
+
+struct square_int
+{
+    STDGPU_HOST_DEVICE int
+    operator()(const int x) const
+    {
+        return x * x;
+    }
+};
+
+int
+main()
+{
+    const stdgpu::index_t n = 100;
+
+    int* d_input = createDeviceArray<int>(n);
+    int* d_result = createDeviceArray<int>(n);
+    stdgpu::unordered_set<int> set = stdgpu::unordered_set<int>::createDeviceObject(n);
+
+    thrust::sequence(stdgpu::device_begin(d_input), stdgpu::device_end(d_input), 1);
+
+    auto range_int = stdgpu::device_range<int>(d_input);
+    thrust::transform(range_int.begin(), range_int.end(), stdgpu::device_begin(d_result), square_int());
+
+    set.insert(stdgpu::device_cbegin(d_result), stdgpu::device_cend(d_result));
+
+    auto range_set = set.device_range();
+    int sum = thrust::reduce(range_set.begin(), range_set.end(), 0, thrust::plus<int>());
+
+    const int sum_closed_form = n * (n + 1) * (2 * n + 1) / 6;
+
+    std::cout << "The computed sum from i = 1 to " << n << " of i^2 is " << sum << " (" << sum_closed_form
+              << " expected)" << std::endl;
+
+    destroyDeviceArray<int>(d_input);
+    destroyDeviceArray<int>(d_result);
+    stdgpu::unordered_set<int>::destroyDeviceObject(set);
+}
diff --git a/recipes/stdgpu/all/test_package/test_package_openmp.cpp b/recipes/stdgpu/all/test_package/test_package_openmp.cpp
new file mode 100644
--- /dev/null
+++ b/recipes/stdgpu/all/test_package/test_package_openmp.cpp
@@ -0,0 +1,47 @@
+// Based on https://github.com/stotko/stdgpu/blob/32e0517/examples/openmp/vector.cpp
+#include <thrust/copy.h>
+#include <thrust/reduce.h>
+#include <thrust/sequence.h>
+
+#include <stdgpu/iterator.h> // device_begin, device_end
+#include <stdgpu/memory.h>   // createDeviceArray, destroyDeviceArray
+#include <stdgpu/platform.h> // STDGPU_HOST_DEVICE
+#include <stdgpu/vector.cuh> // stdgpu::vector
+
+#include <cstdlib>
+#include <iostream>
+
+void insert_neighbors_with_duplicates(const int *d_input, const stdgpu::index_t n,
+                                      stdgpu::vector<int> &vec) {
+#pragma omp parallel for
+    for (stdgpu::index_t i = 0; i < n; ++i) {
+        int num = d_input[i];
+        int num_neighborhood[3] = {num - 1, num, num + 1};
+        for (int num_neighbor : num_neighborhood)
+            vec.push_back(num_neighbor);
+    }
+}
+
+int sum_stdgpu(stdgpu::index_t n) {
+    int *d_input = createDeviceArray<int>(n);
+    auto vec = stdgpu::vector<int>::createDeviceObject(3 * n);
+
+    thrust::sequence(stdgpu::device_begin(d_input), stdgpu::device_end(d_input), 1);
+
+    insert_neighbors_with_duplicates(d_input, n, vec);
+
+    auto range_vec = vec.device_range();
+    int sum = thrust::reduce(range_vec.begin(), range_vec.end(), 0, thrust::plus<int>());
+
+    destroyDeviceArray<int>(d_input);
+    stdgpu::vector<int>::destroyDeviceObject(vec);
+
+    return sum;
+}
+
+int main() {
+    const int n = 20;
+    const int sum_closed_form = 3 * (n * (n + 1) / 2);
+    std::cout << "Sum: " << sum_stdgpu(n) << ", expected: " << sum_closed_form << std::endl;
+    return EXIT_SUCCESS;
+}