From 2586f8059673bb4fbd1ae029dacb8bc757c879a3 Mon Sep 17 00:00:00 2001 From: Ben Frederickson Date: Fri, 17 Mar 2023 19:14:37 -0700 Subject: [PATCH 01/25] Remove faiss_mr.hpp (#1351) Remove faiss_mr.hpp - in favour of https://github.com/rapidsai/cuml/pull/5281 Authors: - Ben Frederickson (https://github.com/benfred) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1351 --- cpp/include/raft/spatial/knn/faiss_mr.hpp | 640 ---------------------- cpp/test/CMakeLists.txt | 1 - cpp/test/neighbors/faiss_mr.cu | 95 ---- 3 files changed, 736 deletions(-) delete mode 100644 cpp/include/raft/spatial/knn/faiss_mr.hpp delete mode 100644 cpp/test/neighbors/faiss_mr.cu diff --git a/cpp/include/raft/spatial/knn/faiss_mr.hpp b/cpp/include/raft/spatial/knn/faiss_mr.hpp deleted file mode 100644 index 3cae417996..0000000000 --- a/cpp/include/raft/spatial/knn/faiss_mr.hpp +++ /dev/null @@ -1,640 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/* -This code contains unnecessary code duplication. These could be deleted -once the relevant changes would be made on the FAISS side. Indeed most of -the logic in the below code is similar to FAISS's standard implementation -and should thus be inherited instead of duplicated. This FAISS's issue -once solved should allow the removal of the unnecessary duplicates -in this file : https://github.com/facebookresearch/faiss/issues/2097 -*/ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace raft { -namespace spatial { -namespace knn { - -using namespace faiss::gpu; - -namespace { - -// How many streams per device we allocate by default (for multi-streaming) -constexpr int kNumStreams = 2; - -// Use 256 MiB of pinned memory for async CPU <-> GPU copies by default -constexpr size_t kDefaultPinnedMemoryAllocation = (size_t)256 * 1024 * 1024; - -// Default temporary memory allocation for <= 4 GiB memory GPUs -constexpr size_t k4GiBTempMem = (size_t)512 * 1024 * 1024; - -// Default temporary memory allocation for <= 8 GiB memory GPUs -constexpr size_t k8GiBTempMem = (size_t)1024 * 1024 * 1024; - -// Maximum temporary memory allocation for all GPUs -constexpr size_t kMaxTempMem = (size_t)1536 * 1024 * 1024; - -std::string allocsToString(const std::unordered_map& map) -{ - // Produce a sorted list of all outstanding allocations by type - std::unordered_map> stats; - - for (auto& entry : map) { - auto& a = entry.second; - - auto it = stats.find(a.type); - if (it != stats.end()) { - stats[a.type].first++; - stats[a.type].second += a.size; - } else { - stats[a.type] = std::make_pair(1, a.size); - } - } - - std::stringstream ss; - for (auto& entry : stats) { - ss << "Alloc type " << allocTypeToString(entry.first) << ": " << entry.second.first - << " allocations, " << entry.second.second << " bytes\n"; - } - - return ss.str(); -} - -} // namespace - -/// RMM implementation of the GpuResources object that provides for a -/// temporary memory manager -class RmmGpuResourcesImpl : public GpuResources { - public: - RmmGpuResourcesImpl() - : pinnedMemAlloc_(nullptr), - pinnedMemAllocSize_(0), - // let the adjustment function determine the memory size for us by passing - // in a huge value that will then be adjusted - tempMemSize_(getDefaultTempMemForGPU(-1, std::numeric_limits::max())), - pinnedMemSize_(kDefaultPinnedMemoryAllocation), - allocLogging_(false), - cmr(new rmm::mr::cuda_memory_resource), - mmr(new rmm::mr::managed_memory_resource), - pmr(new rmm::mr::pinned_memory_resource){}; - - ~RmmGpuResourcesImpl() - { - // The temporary memory allocator has allocated memory through us, so clean - // that up before we finish fully de-initializing ourselves - tempMemory_.clear(); - - // Make sure all allocations have been freed - bool allocError = false; - - for (auto& entry : allocs_) { - auto& map = entry.second; - - if (!map.empty()) { - std::cerr << "RmmGpuResources destroyed with allocations outstanding:\n" - << "Device " << entry.first << " outstanding allocations:\n"; - std::cerr << allocsToString(map); - allocError = true; - } - } - - FAISS_ASSERT_MSG(!allocError, "GPU memory allocations not properly cleaned up"); - - for (auto& entry : defaultStreams_) { - DeviceScope scope(entry.first); - - // We created these streams, so are responsible for destroying them - CUDA_VERIFY(cudaStreamDestroy(entry.second)); - } - - for (auto& entry : alternateStreams_) { - DeviceScope scope(entry.first); - - for (auto stream : entry.second) { - CUDA_VERIFY(cudaStreamDestroy(stream)); - } - } - - for (auto& entry : asyncCopyStreams_) { - DeviceScope scope(entry.first); - - CUDA_VERIFY(cudaStreamDestroy(entry.second)); - } - - for (auto& entry : blasHandles_) { - DeviceScope scope(entry.first); - - auto blasStatus = cublasDestroy(entry.second); - FAISS_ASSERT(blasStatus == CUBLAS_STATUS_SUCCESS); - } - - if (pinnedMemAlloc_) { pmr->deallocate(pinnedMemAlloc_, pinnedMemAllocSize_); } - }; - - /// Disable allocation of temporary memory; all temporary memory - /// requests will call cudaMalloc / cudaFree at the point of use - void noTempMemory() { setTempMemory(0); }; - - /// Specify that we wish to use a certain fixed size of memory on - /// all devices as temporary memory. This is the upper bound for the GPU - /// memory that we will reserve. We will never go above 1.5 GiB on any GPU; - /// smaller GPUs (with <= 4 GiB or <= 8 GiB) will use less memory than that. - /// To avoid any temporary memory allocation, pass 0. - void setTempMemory(size_t size) - { - if (tempMemSize_ != size) { - // adjust based on general limits - tempMemSize_ = getDefaultTempMemForGPU(-1, size); - - // We need to re-initialize memory resources for all current devices that - // have been initialized. - // This should be safe to do, even if we are currently running work, because - // the cudaFree call that this implies will force-synchronize all GPUs with - // the CPU - for (auto& p : tempMemory_) { - int device = p.first; - // Free the existing memory first - p.second.reset(); - - // Allocate new - p.second = std::unique_ptr( - new StackDeviceMemory(this, - p.first, - // adjust for this specific device - getDefaultTempMemForGPU(device, tempMemSize_))); - } - } - }; - - /// Set amount of pinned memory to allocate, for async GPU <-> CPU - /// transfers - void setPinnedMemory(size_t size) - { - // Should not call this after devices have been initialized - FAISS_ASSERT(defaultStreams_.size() == 0); - FAISS_ASSERT(!pinnedMemAlloc_); - - pinnedMemSize_ = size; - }; - - /// Called to change the stream for work ordering. We do not own `stream`; - /// i.e., it will not be destroyed when the GpuResources object gets cleaned - /// up. - /// We are guaranteed that all Faiss GPU work is ordered with respect to - /// this stream upon exit from an index or other Faiss GPU call. - void setDefaultStream(int device, cudaStream_t stream) - { - if (isInitialized(device)) { - // A new series of calls may not be ordered with what was the previous - // stream, so if the stream being specified is different, then we need to - // ensure ordering between the two (new stream waits on old). - auto it = userDefaultStreams_.find(device); - cudaStream_t prevStream = nullptr; - - if (it != userDefaultStreams_.end()) { - prevStream = it->second; - } else { - FAISS_ASSERT(defaultStreams_.count(device)); - prevStream = defaultStreams_[device]; - } - - if (prevStream != stream) { streamWait({stream}, {prevStream}); } - } - - userDefaultStreams_[device] = stream; - }; - - /// Revert the default stream to the original stream managed by this resources - /// object, in case someone called `setDefaultStream`. - void revertDefaultStream(int device) - { - if (isInitialized(device)) { - auto it = userDefaultStreams_.find(device); - - if (it != userDefaultStreams_.end()) { - // There was a user stream set that we need to synchronize against - cudaStream_t prevStream = userDefaultStreams_[device]; - - FAISS_ASSERT(defaultStreams_.count(device)); - cudaStream_t newStream = defaultStreams_[device]; - - streamWait({newStream}, {prevStream}); - } - } - - userDefaultStreams_.erase(device); - }; - - /// Returns the stream for the given device on which all Faiss GPU work is - /// ordered. - /// We are guaranteed that all Faiss GPU work is ordered with respect to - /// this stream upon exit from an index or other Faiss GPU call. - cudaStream_t getDefaultStream(int device) - { - initializeForDevice(device); - - auto it = userDefaultStreams_.find(device); - if (it != userDefaultStreams_.end()) { - // There is a user override stream set - return it->second; - } - - // Otherwise, our base default stream - return defaultStreams_[device]; - }; - - /// Called to change the work ordering streams to the null stream - /// for all devices - void setDefaultNullStreamAllDevices() - { - for (int dev = 0; dev < getNumDevices(); ++dev) { - setDefaultStream(dev, nullptr); - } - }; - - /// If enabled, will print every GPU memory allocation and deallocation to - /// standard output - void setLogMemoryAllocations(bool enable) { allocLogging_ = enable; }; - - public: - /// Internal system calls - - /// Initialize resources for this device - void initializeForDevice(int device) - { - if (isInitialized(device)) { return; } - - // If this is the first device that we're initializing, create our - // pinned memory allocation - if (defaultStreams_.empty() && pinnedMemSize_ > 0) { - pinnedMemAlloc_ = pmr->allocate(pinnedMemSize_); - pinnedMemAllocSize_ = pinnedMemSize_; - } - - FAISS_ASSERT(device < getNumDevices()); - DeviceScope scope(device); - - // Make sure that device properties for all devices are cached - auto& prop = getDeviceProperties(device); - - // Also check to make sure we meet our minimum compute capability (3.0) - FAISS_ASSERT_FMT(prop.major >= 3, - "Device id %d with CC %d.%d not supported, " - "need 3.0+ compute capability", - device, - prop.major, - prop.minor); - - // Create streams - cudaStream_t defaultStream = 0; - CUDA_VERIFY(cudaStreamCreateWithFlags(&defaultStream, cudaStreamNonBlocking)); - - defaultStreams_[device] = defaultStream; - - cudaStream_t asyncCopyStream = 0; - CUDA_VERIFY(cudaStreamCreateWithFlags(&asyncCopyStream, cudaStreamNonBlocking)); - - asyncCopyStreams_[device] = asyncCopyStream; - - std::vector deviceStreams; - for (int j = 0; j < kNumStreams; ++j) { - cudaStream_t stream = 0; - CUDA_VERIFY(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking)); - - deviceStreams.push_back(stream); - } - - alternateStreams_[device] = std::move(deviceStreams); - - // Create cuBLAS handle - cublasHandle_t blasHandle = 0; - auto blasStatus = cublasCreate(&blasHandle); - FAISS_ASSERT(blasStatus == CUBLAS_STATUS_SUCCESS); - blasHandles_[device] = blasHandle; - - // For CUDA 10 on V100, enabling tensor core usage would enable automatic - // rounding down of inputs to f16 (though accumulate in f32) which results in - // unacceptable loss of precision in general. - // For CUDA 11 / A100, only enable tensor core support if it doesn't result in - // a loss of precision. -#if CUDA_VERSION >= 11000 - cublasSetMathMode(blasHandle, CUBLAS_MATH_DISALLOW_REDUCED_PRECISION_REDUCTION); -#endif - - FAISS_ASSERT(allocs_.count(device) == 0); - allocs_[device] = std::unordered_map(); - - FAISS_ASSERT(tempMemory_.count(device) == 0); - auto mem = std::unique_ptr( - new StackDeviceMemory(this, - device, - // adjust for this specific device - getDefaultTempMemForGPU(device, tempMemSize_))); - - tempMemory_.emplace(device, std::move(mem)); - }; - - cublasHandle_t getBlasHandle(int device) - { - initializeForDevice(device); - return blasHandles_[device]; - }; - - std::vector getAlternateStreams(int device) - { - initializeForDevice(device); - return alternateStreams_[device]; - }; - - /// Allocate non-temporary GPU memory - void* allocMemory(const AllocRequest& req) - { - initializeForDevice(req.device); - - // We don't allocate a placeholder for zero-sized allocations - if (req.size == 0) { return nullptr; } - - // Make sure that the allocation is a multiple of 16 bytes for alignment - // purposes - auto adjReq = req; - adjReq.size = utils::roundUp(adjReq.size, (size_t)16); - - void* p = nullptr; - - if (allocLogging_) { std::cout << "RmmGpuResources: alloc " << adjReq.toString() << "\n"; } - - if (adjReq.space == MemorySpace::Temporary) { - // If we don't have enough space in our temporary memory manager, we need - // to allocate this request separately - auto& tempMem = tempMemory_[adjReq.device]; - - if (adjReq.size > tempMem->getSizeAvailable()) { - // We need to allocate this ourselves - AllocRequest newReq = adjReq; - newReq.space = MemorySpace::Device; - newReq.type = AllocType::TemporaryMemoryOverflow; - - return allocMemory(newReq); - } - - // Otherwise, we can handle this locally - p = tempMemory_[adjReq.device]->allocMemory(adjReq.stream, adjReq.size); - - } else if (adjReq.space == MemorySpace::Device) { - p = cmr->allocate(adjReq.size, adjReq.stream); - } else if (adjReq.space == MemorySpace::Unified) { - p = mmr->allocate(adjReq.size, adjReq.stream); - } else { - FAISS_ASSERT_FMT(false, "unknown MemorySpace %d", (int)adjReq.space); - } - - allocs_[adjReq.device][p] = adjReq; - - return p; - }; - - /// Returns a previous allocation - void deallocMemory(int device, void* p) - { - FAISS_ASSERT(isInitialized(device)); - - if (!p) { return; } - - auto& a = allocs_[device]; - auto it = a.find(p); - FAISS_ASSERT(it != a.end()); - - auto& req = it->second; - - if (allocLogging_) { std::cout << "RmmGpuResources: dealloc " << req.toString() << "\n"; } - - if (req.space == MemorySpace::Temporary) { - tempMemory_[device]->deallocMemory(device, req.stream, req.size, p); - } else if (req.space == MemorySpace::Device) { - cmr->deallocate(p, req.size, req.stream); - } else if (req.space == MemorySpace::Unified) { - mmr->deallocate(p, req.size, req.stream); - } else { - FAISS_ASSERT_FMT(false, "unknown MemorySpace %d", (int)req.space); - } - - a.erase(it); - }; - - size_t getTempMemoryAvailable(int device) const - { - FAISS_ASSERT(isInitialized(device)); - - auto it = tempMemory_.find(device); - FAISS_ASSERT(it != tempMemory_.end()); - - return it->second->getSizeAvailable(); - }; - - /// Export a description of memory used for Python - std::map>> getMemoryInfo() const - { - using AT = std::map>; - - std::map out; - - for (auto& entry : allocs_) { - AT outDevice; - - for (auto& a : entry.second) { - auto& v = outDevice[allocTypeToString(a.second.type)]; - v.first++; - v.second += a.second.size; - } - - out[entry.first] = std::move(outDevice); - } - - return out; - }; - - std::pair getPinnedMemory() - { - return std::make_pair(pinnedMemAlloc_, pinnedMemAllocSize_); - }; - - cudaStream_t getAsyncCopyStream(int device) - { - initializeForDevice(device); - return asyncCopyStreams_[device]; - }; - - private: - /// Have GPU resources been initialized for this device yet? - bool isInitialized(int device) const - { - // Use default streams as a marker for whether or not a certain - // device has been initialized - return defaultStreams_.count(device) != 0; - }; - - /// Adjust the default temporary memory allocation based on the total GPU - /// memory size - static size_t getDefaultTempMemForGPU(int device, size_t requested) - { - auto totalMem = device != -1 ? getDeviceProperties(device).totalGlobalMem - : std::numeric_limits::max(); - - if (totalMem <= (size_t)4 * 1024 * 1024 * 1024) { - // If the GPU has <= 4 GiB of memory, reserve 512 MiB - - if (requested > k4GiBTempMem) { return k4GiBTempMem; } - } else if (totalMem <= (size_t)8 * 1024 * 1024 * 1024) { - // If the GPU has <= 8 GiB of memory, reserve 1 GiB - - if (requested > k8GiBTempMem) { return k8GiBTempMem; } - } else { - // Never use more than 1.5 GiB - if (requested > kMaxTempMem) { return kMaxTempMem; } - } - - // use whatever lower limit the user requested - return requested; - }; - - private: - /// Set of currently outstanding memory allocations per device - /// device -> (alloc request, allocated ptr) - std::unordered_map> allocs_; - - /// Temporary memory provider, per each device - std::unordered_map> tempMemory_; - - /// Our default stream that work is ordered on, one per each device - std::unordered_map defaultStreams_; - - /// This contains particular streams as set by the user for - /// ordering, if any - std::unordered_map userDefaultStreams_; - - /// Other streams we can use, per each device - std::unordered_map> alternateStreams_; - - /// Async copy stream to use for GPU <-> CPU pinned memory copies - std::unordered_map asyncCopyStreams_; - - /// cuBLAS handle for each device - std::unordered_map blasHandles_; - - /// Pinned memory allocation for use with this GPU - void* pinnedMemAlloc_; - size_t pinnedMemAllocSize_; - - /// Another option is to use a specified amount of memory on all - /// devices - size_t tempMemSize_; - - /// Amount of pinned memory we should allocate - size_t pinnedMemSize_; - - /// Whether or not we log every GPU memory allocation and deallocation - bool allocLogging_; - - // cuda_memory_resource - std::unique_ptr cmr; - - // managed_memory_resource - std::unique_ptr mmr; - - // pinned_memory_resource - std::unique_ptr pmr; -}; - -/// Default implementation of GpuResources that allocates a cuBLAS -/// stream and 2 streams for use, as well as temporary memory. -/// Internally, the Faiss GPU code uses the instance managed by getResources, -/// but this is the user-facing object that is internally reference counted. -class RmmGpuResources : public GpuResourcesProvider { - public: - RmmGpuResources() : res_(new RmmGpuResourcesImpl){}; - - ~RmmGpuResources(){}; - - std::shared_ptr getResources() { return res_; }; - - /// Disable allocation of temporary memory; all temporary memory - /// requests will call cudaMalloc / cudaFree at the point of use - void noTempMemory() { res_->noTempMemory(); }; - - /// Specify that we wish to use a certain fixed size of memory on - /// all devices as temporary memory. This is the upper bound for the GPU - /// memory that we will reserve. We will never go above 1.5 GiB on any GPU; - /// smaller GPUs (with <= 4 GiB or <= 8 GiB) will use less memory than that. - /// To avoid any temporary memory allocation, pass 0. - void setTempMemory(size_t size) { res_->setTempMemory(size); }; - - /// Set amount of pinned memory to allocate, for async GPU <-> CPU - /// transfers - void setPinnedMemory(size_t size) { res_->setPinnedMemory(size); }; - - /// Called to change the stream for work ordering. We do not own `stream`; - /// i.e., it will not be destroyed when the GpuResources object gets cleaned - /// up. - /// We are guaranteed that all Faiss GPU work is ordered with respect to - /// this stream upon exit from an index or other Faiss GPU call. - void setDefaultStream(int device, cudaStream_t stream) - { - res_->setDefaultStream(device, stream); - }; - - /// Revert the default stream to the original stream managed by this resources - /// object, in case someone called `setDefaultStream`. - void revertDefaultStream(int device) { res_->revertDefaultStream(device); }; - - /// Called to change the work ordering streams to the null stream - /// for all devices - void setDefaultNullStreamAllDevices() { res_->setDefaultNullStreamAllDevices(); }; - - /// Export a description of memory used for Python - std::map>> getMemoryInfo() const - { - return res_->getMemoryInfo(); - }; - - /// Returns the current default stream - cudaStream_t getDefaultStream(int device) { return res_->getDefaultStream(device); }; - - /// Returns the current amount of temp memory available - size_t getTempMemoryAvailable(int device) const { return res_->getTempMemoryAvailable(device); }; - - /// Synchronize our default stream with the CPU - void syncDefaultStreamCurrentDevice() { res_->syncDefaultStreamCurrentDevice(); }; - - /// If enabled, will print every GPU memory allocation and deallocation to - /// standard output - void setLogMemoryAllocations(bool enable) { res_->setLogMemoryAllocations(enable); }; - - private: - std::shared_ptr res_; -}; - -} // namespace knn -} // namespace spatial -} // namespace raft \ No newline at end of file diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt index acfb470bd8..696a271c65 100644 --- a/cpp/test/CMakeLists.txt +++ b/cpp/test/CMakeLists.txt @@ -271,7 +271,6 @@ if(BUILD_TESTS) test/neighbors/tiled_knn.cu test/neighbors/haversine.cu test/neighbors/ball_cover.cu - test/neighbors/faiss_mr.cu test/neighbors/epsilon_neighborhood.cu test/neighbors/refine.cu test/neighbors/selection.cu diff --git a/cpp/test/neighbors/faiss_mr.cu b/cpp/test/neighbors/faiss_mr.cu deleted file mode 100644 index 89f012db0f..0000000000 --- a/cpp/test/neighbors/faiss_mr.cu +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. - * - * 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 "../test_utils.cuh" - -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include - -namespace raft { -namespace spatial { -namespace knn { - -using namespace faiss::gpu; - -struct AllocInputs { - size_t size; -}; - -template -class FAISS_MR_Test : public ::testing::TestWithParam { - public: - FAISS_MR_Test() - : params_(::testing::TestWithParam::GetParam()), stream(handle.get_stream()) - { - } - - protected: - size_t getFreeMemory(MemorySpace mem_space) - { - if (mem_space == MemorySpace::Device) { - rmm::mr::cuda_memory_resource cmr; - rmm::mr::device_memory_resource* dmr = &cmr; - return dmr->get_mem_info(stream).first; - } else if (mem_space == MemorySpace::Unified) { - rmm::mr::managed_memory_resource mmr; - rmm::mr::device_memory_resource* dmr = &mmr; - return dmr->get_mem_info(stream).first; - } - return 0; - } - - void testAllocs(MemorySpace mem_space) - { - raft::spatial::knn::RmmGpuResources faiss_mr; - auto faiss_mr_impl = faiss_mr.getResources(); - size_t free_before = getFreeMemory(mem_space); - AllocRequest req(AllocType::Other, 0, mem_space, stream, params_.size); - void* ptr = faiss_mr_impl->allocMemory(req); - size_t free_after_alloc = getFreeMemory(mem_space); - faiss_mr_impl->deallocMemory(0, ptr); - ASSERT_TRUE(free_after_alloc <= free_before - params_.size); - } - - raft::device_resources handle; - cudaStream_t stream; - AllocInputs params_; -}; - -const std::vector inputs = {{19687}}; - -typedef FAISS_MR_Test FAISS_MR_TestF; -TEST_P(FAISS_MR_TestF, TestAllocs) -{ - testAllocs(MemorySpace::Device); - testAllocs(MemorySpace::Unified); -} - -INSTANTIATE_TEST_CASE_P(FAISS_MR_Test, FAISS_MR_TestF, ::testing::ValuesIn(inputs)); - -} // namespace knn -} // namespace spatial -} // namespace raft From c4671ed33439c6776bd91a45d6583b84c08d37dd Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Sat, 18 Mar 2023 11:17:33 -0400 Subject: [PATCH 02/25] Removing FAISS from build (#1340) This depends on https://github.com/rapidsai/raft/pull/1202 so those changes are also included here. This should also not be merged until that PR is merged. Authors: - Corey J. Nolet (https://github.com/cjnolet) - Ben Frederickson (https://github.com/benfred) Approvers: - Ben Frederickson (https://github.com/benfred) - Vyas Ramasubramani (https://github.com/vyasr) - Ray Douglass (https://github.com/raydouglass) URL: https://github.com/rapidsai/raft/pull/1340 --- README.md | 10 +-- build.sh | 13 +-- .../all_cuda-118_arch-x86_64.yaml | 2 - conda/recipes/libraft/conda_build_config.yaml | 3 - conda/recipes/libraft/meta.yaml | 4 - cpp/CMakeLists.txt | 23 +---- cpp/cmake/thirdparty/get_faiss.cmake | 89 ------------------- dependencies.yaml | 2 - docs/source/build.md | 19 ++-- 9 files changed, 13 insertions(+), 152 deletions(-) delete mode 100644 cpp/cmake/thirdparty/get_faiss.cmake diff --git a/README.md b/README.md index a178d90008..ff1066492e 100755 --- a/README.md +++ b/README.md @@ -263,12 +263,12 @@ find_and_configure_raft(VERSION ${RAFT_VERSION}.00 Several CMake targets can be made available by adding components in the table below to the `RAFT_COMPONENTS` list above, separated by spaces. The `raft::raft` target will always be available. RAFT headers require, at a minimum, the CUDA toolkit libraries and RMM dependencies. -| Component | Target | Description | Base Dependencies | -| --- | --- | --- | --- | +| Component | Target | Description | Base Dependencies | +| --- | --- | --- |------------------------------------------------------------------| | n/a | `raft::raft` | Full RAFT header library | CUDA toolkit library, RMM, Thrust (optional), NVTools (optional) | -| distance | `raft::distance` | Pre-compiled template specializations for raft::distance | raft::raft, cuCollections (optional) | -| nn | `raft::nn` | Pre-compiled template specializations for raft::neighbors | raft::raft, FAISS (optional) | -| distributed | `raft::distributed` | No specializations | raft::raft, UCX, NCCL | +| distance | `raft::distance` | Pre-compiled template specializations for raft::distance | raft::raft, cuCollections (optional) | +| nn | `raft::nn` | Pre-compiled template specializations for raft::neighbors | raft::raft | +| distributed | `raft::distributed` | No specializations | raft::raft, UCX, NCCL | ### Source diff --git a/build.sh b/build.sh index 575f6bdaa1..7215b8199a 100755 --- a/build.sh +++ b/build.sh @@ -18,7 +18,7 @@ ARGS=$* # script, and that this script resides in the repo dir! REPODIR=$(cd $(dirname $0); pwd) -VALIDARGS="clean libraft pylibraft raft-dask docs tests bench clean --uninstall -v -g -n --compile-libs --compile-nn --compile-dist --allgpuarch --no-nvtx --show_depr_warn -h --buildfaiss --minimal-deps" +VALIDARGS="clean libraft pylibraft raft-dask docs tests bench clean --uninstall -v -g -n --compile-libs --compile-nn --compile-dist --allgpuarch --no-nvtx --show_depr_warn -h --minimal-deps" HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=] [--limit-tests=] [--limit-bench=] where is: clean - remove all existing build artifacts and configuration (start over) @@ -45,7 +45,6 @@ HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=\\\" - pass arbitrary list of CMake configuration options (escape all quotes in argument) @@ -69,11 +68,9 @@ BUILD_ALL_GPU_ARCH=0 BUILD_TESTS=OFF BUILD_TYPE=Release BUILD_BENCH=OFF -BUILD_STATIC_FAISS=OFF COMPILE_LIBRARIES=OFF COMPILE_NN_LIBRARY=OFF COMPILE_DIST_LIBRARY=OFF -ENABLE_NN_DEPENDENCIES=OFF INSTALL_TARGET=install TEST_TARGETS="CLUSTER_TEST;CORE_TEST;DISTANCE_TEST;LABEL_TEST;LINALG_TEST;MATRIX_TEST;RANDOM_TEST;SOLVERS_TEST;SPARSE_TEST;SPARSE_DIST_TEST;SPARSE_NEIGHBORS_TEST;NEIGHBORS_TEST;STATS_TEST;UTILS_TEST" @@ -278,7 +275,6 @@ if hasArg --compile-libs || (( ${NUMARGS} == 0 )); then fi if hasArg --compile-nn || hasArg --compile-libs || (( ${NUMARGS} == 0 )); then - ENABLE_NN_DEPENDENCIES=ON COMPILE_NN_LIBRARY=ON CMAKE_TARGET="${CMAKE_TARGET};raft_nn_lib" fi @@ -299,7 +295,6 @@ if hasArg tests || (( ${NUMARGS} == 0 )); then $CMAKE_TARGET == *"NEIGHBORS_TEST"* || \ $CMAKE_TARGET == *"STATS_TEST"* ]]; then echo "-- Enabling nearest neighbors lib for gtests" - ENABLE_NN_DEPENDENCIES=ON COMPILE_NN_LIBRARY=ON fi @@ -324,7 +319,6 @@ if hasArg bench || (( ${NUMARGS} == 0 )); then if [[ $CMAKE_TARGET == *"CLUSTER_BENCH"* || \ $CMAKE_TARGET == *"NEIGHBORS_BENCH"* ]]; then echo "-- Enabling nearest neighbors lib for benchmarks" - ENABLE_NN_DEPENDENCIES=ON COMPILE_NN_LIBRARY=ON fi @@ -338,9 +332,6 @@ if hasArg bench || (( ${NUMARGS} == 0 )); then fi -if hasArg --buildfaiss; then - BUILD_STATIC_FAISS=ON -fi if hasArg --no-nvtx; then NVTX=OFF fi @@ -402,7 +393,6 @@ if (( ${NUMARGS} == 0 )) || hasArg libraft || hasArg docs || hasArg tests || has -DCMAKE_CUDA_ARCHITECTURES=${RAFT_CMAKE_CUDA_ARCHITECTURES} \ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ -DRAFT_COMPILE_LIBRARIES=${COMPILE_LIBRARIES} \ - -DRAFT_ENABLE_NN_DEPENDENCIES=${ENABLE_NN_DEPENDENCIES} \ -DRAFT_NVTX=${NVTX} \ -DDISABLE_DEPRECATION_WARNINGS=${DISABLE_DEPRECATION_WARNINGS} \ -DBUILD_TESTS=${BUILD_TESTS} \ @@ -410,7 +400,6 @@ if (( ${NUMARGS} == 0 )) || hasArg libraft || hasArg docs || hasArg tests || has -DCMAKE_MESSAGE_LOG_LEVEL=${CMAKE_LOG_LEVEL} \ -DRAFT_COMPILE_NN_LIBRARY=${COMPILE_NN_LIBRARY} \ -DRAFT_COMPILE_DIST_LIBRARY=${COMPILE_DIST_LIBRARY} \ - -DRAFT_USE_FAISS_STATIC=${BUILD_STATIC_FAISS} \ -DRAFT_ENABLE_thrust_DEPENDENCY=${ENABLE_thrust_DEPENDENCY} \ ${CACHE_ARGS} \ ${EXTRA_CMAKE_ARGS} diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 47af29d9d2..39f1fef4d5 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -22,7 +22,6 @@ dependencies: - dask>=2023.1.1 - distributed>=2023.1.1 - doxygen>=1.8.20 -- faiss-proc=*=cuda - gcc_linux-64=11.* - graphviz - ipython @@ -34,7 +33,6 @@ dependencies: - libcusolver=11.4.1.48 - libcusparse-dev=11.7.5.86 - libcusparse=11.7.5.86 -- libfaiss>=1.7.1=cuda* - ninja - numpydoc - pydata-sphinx-theme diff --git a/conda/recipes/libraft/conda_build_config.yaml b/conda/recipes/libraft/conda_build_config.yaml index ca213dc317..e1079f4db8 100644 --- a/conda/recipes/libraft/conda_build_config.yaml +++ b/conda/recipes/libraft/conda_build_config.yaml @@ -19,9 +19,6 @@ nccl_version: gtest_version: - "=1.10.0" -libfaiss_version: - - "1.7.2 *_cuda" - # The CTK libraries below are missing from the conda-forge::cudatoolkit # package. The "*_host_*" version specifiers correspond to `11.8` packages and the # "*_run_*" version specifiers correspond to `11.x` packages. diff --git a/conda/recipes/libraft/meta.yaml b/conda/recipes/libraft/meta.yaml index 771c7d55b8..158016ddf0 100644 --- a/conda/recipes/libraft/meta.yaml +++ b/conda/recipes/libraft/meta.yaml @@ -130,7 +130,6 @@ outputs: host: - {{ pin_subpackage('libraft-headers', exact=True) }} - cuda-profiler-api {{ cuda_profiler_api_host_version }} - - faiss-proc=*=cuda - lapack - libcublas {{ libcublas_host_version }} - libcublas-dev {{ libcublas_host_version }} @@ -140,10 +139,7 @@ outputs: - libcusolver-dev {{ libcusolver_host_version }} - libcusparse {{ libcusparse_host_version }} - libcusparse-dev {{ libcusparse_host_version }} - - libfaiss {{ libfaiss_version }} run: - - faiss-proc=*=cuda - - libfaiss {{ libfaiss_version }} - {{ pin_subpackage('libraft-headers', exact=True) }} about: home: https://rapids.ai/ diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 2999045a0c..3889c39e6e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -71,9 +71,6 @@ option( option(RAFT_COMPILE_DIST_LIBRARY "Enable building raft distant shared library instantiations" ${RAFT_COMPILE_LIBRARIES} ) -option(RAFT_ENABLE_NN_DEPENDENCIES "Search for raft::nn dependencies like faiss" - ${RAFT_COMPILE_NN_LIBRARY} -) option(RAFT_ENABLE_thrust_DEPENDENCY "Enable Thrust dependency" ON) @@ -89,17 +86,6 @@ if(BUILD_TESTS AND NOT RAFT_ENABLE_thrust_DEPENDENCY) set(RAFT_ENABLE_thrust_DEPENDENCY ON) endif() -option(RAFT_EXCLUDE_FAISS_FROM_ALL "Exclude FAISS targets from RAFT's 'all' target" ON) - -include(CMakeDependentOption) -cmake_dependent_option( - RAFT_USE_FAISS_STATIC - "Build and statically link the FAISS library for nearest neighbors search on GPU" - ON - RAFT_COMPILE_LIBRARIES - OFF -) - message(VERBOSE "RAFT: Building optional components: ${raft_FIND_COMPONENTS}") message(VERBOSE "RAFT: Build RAFT unit-tests: ${BUILD_TESTS}") message(VERBOSE "RAFT: Building raft C++ benchmarks: ${BUILD_BENCH}") @@ -183,7 +169,6 @@ rapids_cpm_init() # thrust before rmm/cuco so we get the right version of thrust/cub include(cmake/thirdparty/get_thrust.cmake) include(cmake/thirdparty/get_rmm.cmake) -include(cmake/thirdparty/get_faiss.cmake) include(cmake/thirdparty/get_cutlass.cmake) if(RAFT_ENABLE_cuco_DEPENDENCY) @@ -502,7 +487,7 @@ if(RAFT_COMPILE_NN_LIBRARY) target_link_libraries( raft_nn_lib - PUBLIC faiss::faiss raft::raft + PUBLIC raft::raft PRIVATE nvidia::cutlass::cutlass ) target_compile_options( @@ -710,12 +695,6 @@ endif() if(nn IN_LIST raft_FIND_COMPONENTS) enable_language(CUDA) - - if(TARGET faiss AND (NOT TARGET faiss::faiss)) - add_library(faiss::faiss ALIAS faiss) - elseif(TARGET faiss::faiss AND (NOT TARGET faiss)) - add_library(faiss ALIAS faiss::faiss) - endif() endif() ]=] ) diff --git a/cpp/cmake/thirdparty/get_faiss.cmake b/cpp/cmake/thirdparty/get_faiss.cmake deleted file mode 100644 index e6f06a00a5..0000000000 --- a/cpp/cmake/thirdparty/get_faiss.cmake +++ /dev/null @@ -1,89 +0,0 @@ -#============================================================================= -# Copyright (c) 2021-2022, NVIDIA CORPORATION. -# -# 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. -#============================================================================= - -function(find_and_configure_faiss) - set(oneValueArgs VERSION REPOSITORY PINNED_TAG BUILD_STATIC_LIBS EXCLUDE_FROM_ALL) - cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" - "${multiValueArgs}" ${ARGN} ) - - if(RAFT_ENABLE_NN_DEPENDENCIES OR RAFT_COMPILE_LIBRARIES) - rapids_find_generate_module(faiss - HEADER_NAMES faiss/IndexFlat.h - LIBRARY_NAMES faiss - ) - - set(BUILD_SHARED_LIBS ON) - if (PKG_BUILD_STATIC_LIBS) - set(BUILD_SHARED_LIBS OFF) - set(CPM_DOWNLOAD_faiss ON) - endif() - - rapids_cpm_find(faiss ${PKG_VERSION} - GLOBAL_TARGETS faiss::faiss - CPM_ARGS - GIT_REPOSITORY ${PKG_REPOSITORY} - GIT_TAG ${PKG_PINNED_TAG} - EXCLUDE_FROM_ALL ${PKG_EXCLUDE_FROM_ALL} - OPTIONS - "FAISS_ENABLE_PYTHON OFF" - "CUDAToolkit_ROOT ${CUDAToolkit_LIBRARY_DIR}" - "FAISS_ENABLE_GPU ON" - "BUILD_TESTING OFF" - "CMAKE_MESSAGE_LOG_LEVEL VERBOSE" - "FAISS_USE_CUDA_TOOLKIT_STATIC ${CUDA_STATIC_RUNTIME}" - ) - - if(TARGET faiss AND NOT TARGET faiss::faiss) - add_library(faiss::faiss ALIAS faiss) - endif() - - if(faiss_ADDED) - rapids_export(BUILD faiss - EXPORT_SET faiss-targets - GLOBAL_TARGETS faiss - NAMESPACE faiss::) - endif() - endif() - - # We generate the faiss-config files when we built faiss locally, so always do `find_dependency` - rapids_export_package(BUILD OpenMP raft-nn-lib-exports) # faiss uses openMP but doesn't export a need for it - rapids_export_package(BUILD faiss raft-nn-lib-exports GLOBAL_TARGETS faiss::faiss faiss) - rapids_export_package(INSTALL faiss raft-nn-lib-exports GLOBAL_TARGETS faiss::faiss faiss) - - # Tell cmake where it can find the generated faiss-config.cmake we wrote. - include("${rapids-cmake-dir}/export/find_package_root.cmake") - rapids_export_find_package_root(BUILD faiss [=[${CMAKE_CURRENT_LIST_DIR}]=] raft-nn-lib-exports) -endfunction() - -if(NOT RAFT_FAISS_GIT_TAG) - # TODO: Remove this once faiss supports FAISS_USE_CUDA_TOOLKIT_STATIC - # (https://github.com/facebookresearch/faiss/pull/2446) - set(RAFT_FAISS_GIT_TAG fea/statically-link-ctk-v1.7.0) - # set(RAFT_FAISS_GIT_TAG bde7c0027191f29c9dadafe4f6e68ca0ee31fb30) -endif() - -if(NOT RAFT_FAISS_GIT_REPOSITORY) - # TODO: Remove this once faiss supports FAISS_USE_CUDA_TOOLKIT_STATIC - # (https://github.com/facebookresearch/faiss/pull/2446) - set(RAFT_FAISS_GIT_REPOSITORY https://github.com/trxcllnt/faiss.git) - # set(RAFT_FAISS_GIT_REPOSITORY https://github.com/facebookresearch/faiss.git) -endif() - -find_and_configure_faiss(VERSION 1.7.0 - REPOSITORY ${RAFT_FAISS_GIT_REPOSITORY} - PINNED_TAG ${RAFT_FAISS_GIT_TAG} - BUILD_STATIC_LIBS ${RAFT_USE_FAISS_STATIC} - EXCLUDE_FROM_ALL ${RAFT_EXCLUDE_FAISS_FROM_ALL}) diff --git a/dependencies.yaml b/dependencies.yaml index 93893d07af..9fbf26bcd1 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -179,8 +179,6 @@ dependencies: - ucx-py=0.31.* - ucx-proc=*=gpu - rmm=23.04 - - libfaiss>=1.7.1=cuda* - - faiss-proc=*=cuda - dask-cuda=23.04 test_python: common: diff --git a/docs/source/build.md b/docs/source/build.md index 70b07f4e81..5ba1e75fad 100644 --- a/docs/source/build.md +++ b/docs/source/build.md @@ -47,7 +47,6 @@ In addition to the libraries included with cudatoolkit 11.0+, there are some oth - [cuCollections](https://github.com/NVIDIA/cuCollections) - Used in `raft::sparse::distance` API. - [Libcu++](https://github.com/NVIDIA/libcudacxx) v1.7.0 - Used by cuCollections - [CUTLASS](https://github.com/NVIDIA/cutlass) v2.9.1 - Used in `raft::distance` API. -- [FAISS](https://github.com/facebookresearch/faiss) v1.7.0 - Used in `raft::neighbors` API. - [NCCL](https://github.com/NVIDIA/nccl) - Used in `raft::comms` API and needed to build `raft-dask`. - [UCX](https://github.com/openucx/ucx) - Used in `raft::comms` API and needed to build `raft-dask`. - [Googletest](https://github.com/google/googletest) - Needed to build tests @@ -60,14 +59,14 @@ The recommended way to build and install RAFT is to use the `build.sh` script in ### Header-only C++ -`build.sh` uses [rapids-cmake](https://github.com/rapidsai/rapids-cmake), which will automatically download any dependencies which are not already installed. It's important to note that while all the headers will be installed and available, some parts of the RAFT API depend on libraries like `FAISS`, which will need to be explicitly enabled in `build.sh`. +`build.sh` uses [rapids-cmake](https://github.com/rapidsai/rapids-cmake), which will automatically download any dependencies which are not already installed. It's important to note that while all the headers will be installed and available, some parts of the RAFT API depend on libraries like CUTLASS, which will need to be explicitly enabled in `build.sh`. The following example will download the needed dependencies and install the RAFT headers into `$INSTALL_PREFIX/include/raft`. ```bash ./build.sh libraft ``` -The `-n` flag can be passed to just have the build download the needed dependencies. Since RAFT is primarily used at build-time, the dependencies will never be installed by the RAFT build, with the exception of building FAISS statically into the shared libraries. +The `-n` flag can be passed to just have the build download the needed dependencies. Since RAFT is primarily used at build-time, the dependencies will never be installed by the RAFT build. ```bash ./build.sh libraft -n ``` @@ -167,15 +166,13 @@ RAFT's cmake has the following configurable flags available:. | RAFT_COMPILE_LIBRARIES | ON, OFF | ON if either BUILD_TESTS or BUILD_BENCH is ON; otherwise OFF | Compiles all `libraft` shared libraries (these are required for Googletests) | | RAFT_COMPILE_NN_LIBRARY | ON, OFF | OFF | Compiles the `libraft-nn` shared library | | RAFT_COMPILE_DIST_LIBRARY | ON, OFF | OFF | Compiles the `libraft-distance` shared library | -| RAFT_ENABLE_NN_DEPENDENCIES | ON, OFF | OFF | Searches for dependencies of nearest neighbors API, such as FAISS, and compiles them if not found. Needed for `raft::spatial::knn` | -| RAFT_USE_FAISS_STATIC | ON, OFF | OFF | Statically link FAISS into `libraft-nn` | | DETECT_CONDA_ENV | ON, OFF | ON | Enable detection of conda environment for dependencies | | RAFT_NVTX | ON, OFF | OFF | Enable NVTX Markers | | CUDA_ENABLE_KERNELINFO | ON, OFF | OFF | Enables `kernelinfo` in nvcc. This is useful for `compute-sanitizer` | | CUDA_ENABLE_LINEINFO | ON, OFF | OFF | Enable the -lineinfo option for nvcc | | CUDA_STATIC_RUNTIME | ON, OFF | OFF | Statically link the CUDA runtime | -Currently, shared libraries are provided for the `libraft-nn` and `libraft-distance` components. The `libraft-nn` component depends upon [FAISS](https://github.com/facebookresearch/faiss) and the `RAFT_ENABLE_NN_DEPENDENCIES` option will build it from source if it is not already installed. +Currently, shared libraries are provided for the `libraft-nn` and `libraft-distance` components. ### Python @@ -277,7 +274,7 @@ If RAFT has already been installed, such as by using the `build.sh` script, use ### Using C++ pre-compiled shared libraries -Use `find_package(raft COMPONENTS nn distance)` to enable the shared libraries and transitively pass dependencies through separate targets for each component. In this example, the `raft::distance` and `raft::nn` targets will be available for configuring linking paths in addition to `raft::raft`. These targets will also pass through any transitive dependencies (such as FAISS for the `nn` package). +Use `find_package(raft COMPONENTS nn distance)` to enable the shared libraries and transitively pass dependencies through separate targets for each component. In this example, the `raft::distance` and `raft::nn` targets will be available for configuring linking paths in addition to `raft::raft`. These targets will also pass through any transitive dependencies (such as CUTLASS for the `distance` package). The pre-compiled libraries contain template specializations for commonly used types, such as single- and double-precision floating-point. In order to use the symbols in the pre-compiled libraries, the compiler needs to be told not to instantiate templates that are already contained in the shared libraries. By convention, these header files are named `specializations.cuh` and located in the base directory for the packages that contain specializations. @@ -302,8 +299,8 @@ set(RAFT_FORK "rapidsai") set(RAFT_PINNED_TAG "branch-${RAFT_VERSION}") function(find_and_configure_raft) - set(oneValueArgs VERSION FORK PINNED_TAG USE_FAISS_STATIC - COMPILE_LIBRARIES ENABLE_NN_DEPENDENCIES CLONE_ON_PIN + set(oneValueArgs VERSION FORK PINNED_TAG + COMPILE_LIBRARIES CLONE_ON_PIN USE_NN_LIBRARY USE_DISTANCE_LIBRARY ENABLE_thrust_DEPENDENCY) cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" @@ -346,8 +343,6 @@ function(find_and_configure_raft) OPTIONS "BUILD_TESTS OFF" "BUILD_BENCH OFF" - "RAFT_ENABLE_NN_DEPENDENCIES ${PKG_ENABLE_NN_DEPENDENCIES}" - "RAFT_USE_FAISS_STATIC ${PKG_USE_FAISS_STATIC}" "RAFT_COMPILE_LIBRARIES ${PKG_COMPILE_LIBRARIES}" "RAFT_ENABLE_thrust_DEPENDENCY ${PKG_ENABLE_thrust_DEPENDENCY}" ) @@ -369,8 +364,6 @@ find_and_configure_raft(VERSION ${RAFT_VERSION}.00 COMPILE_LIBRARIES NO USE_NN_LIBRARY NO USE_DISTANCE_LIBRARY NO - ENABLE_NN_DEPENDENCIES NO # This builds FAISS if not installed - USE_FAISS_STATIC NO ENABLE_thrust_DEPENDENCY YES ) ``` From 81fd1e0f87f50f6501cdbd1e9b95baea35a1bb4b Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Sun, 19 Mar 2023 22:19:50 -0400 Subject: [PATCH 03/25] Adding small readme image (#1354) Authors: - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Divye Gala (https://github.com/divyegala) - Mark Sadang (https://github.com/msadang) URL: https://github.com/rapidsai/raft/pull/1354 --- README.md | 3 +++ img/raft.png | Bin 0 -> 506874 bytes 2 files changed, 3 insertions(+) create mode 100644 img/raft.png diff --git a/README.md b/README.md index ff1066492e..6d9de4c8b7 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ #
 RAFT: Reusable Accelerated Functions and Tools
+ +![Navigating the canyons of accelerated possibilities](img/raft.png) + ## Resources - [RAFT Reference Documentation](https://docs.rapids.ai/api/raft/stable/): API Documentation. diff --git a/img/raft.png b/img/raft.png new file mode 100644 index 0000000000000000000000000000000000000000..45589614f5cb780169b5388d3e8fdfe2a1de6cd1 GIT binary patch literal 506874 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

oIK}t7P%p~x3|*Uk3~8i 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 kFUOV07*qoM6N<$f=9&XhX4Qo literal 0 HcmV?d00001 From 97f8ad71f26174c18598ab1a34f4d3043d6df2a0 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Mon, 20 Mar 2023 15:35:00 -0400 Subject: [PATCH 04/25] Consolidate pre-compiled specializations into single `libraft` binary (#1333) Now that we no longer need to rely on the FAISS dependency, this PR: 1. Consolidates the `libraft-distance` and `libraft-nn` targets into a single `libraft` artifact 2. Introduces a new `raft::compiled` target to link against the new `libraft.so` binary. Removes `raft::distance`and `raft::nn` targets. 3. Consolidates `RAFT_DISTANCE_COMPILED` and `RAFT_NN_COMPILED` pre-processor vars into a single `RAFT_COMPILED` (to match similar pattern implementated by `spdlog`) 4. Consolidates `specializations.cuh` headers 5. Updates all docs, scripts, and build infra This change has been a long time coming and is intended to be a 23.04 feature. This is further going to require updates to several projects downstream. Here's a checklist to track that progress: - [x] offline announcement - [x] cuml https://github.com/rapidsai/cuml/pull/5272 - [x] cugraph https://github.com/rapidsai/cugraph/pull/3348 - [x] cuopt https://github.com/rapidsai/cuopt/pull/1023 - [x] cugraph-ops https://github.com/rapidsai/cugraph-ops/pull/429 This PR depended on https://github.com/rapidsai/raft/pull/1340 (removing FAISS from the build) and on https://github.com/rapidsai/raft/pull/1202 (replacing the FAISS bfknn w/ our own), both of which have been merged. Closes #824 Authors: - Corey J. Nolet (https://github.com/cjnolet) - Ben Frederickson (https://github.com/benfred) Approvers: - Sevag H (https://github.com/sevagh) - Divye Gala (https://github.com/divyegala) - Ben Frederickson (https://github.com/benfred) - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/raft/pull/1333 --- README.md | 20 +- build.sh | 79 +-- ci/build_docs.sh | 3 +- ci/test_cpp.sh | 2 +- ci/test_python.sh | 2 +- .../{build_libraft_nn.sh => build_libraft.sh} | 2 +- .../recipes/libraft/build_libraft_distance.sh | 4 - conda/recipes/libraft/meta.yaml | 47 +- conda/recipes/pylibraft/meta.yaml | 4 +- cpp/CMakeLists.txt | 528 ++++++++---------- cpp/bench/CMakeLists.txt | 14 +- cpp/bench/cluster/kmeans.cu | 2 +- cpp/bench/cluster/kmeans_balanced.cu | 2 +- cpp/bench/distance/distance_common.cuh | 2 +- cpp/bench/distance/fused_l2_nn.cu | 2 +- cpp/bench/distance/kernels.cu | 2 +- cpp/bench/distance/masked_nn.cu | 2 +- cpp/bench/matrix/select_k.cu | 2 +- cpp/bench/neighbors/knn.cuh | 7 +- cpp/bench/neighbors/refine_float_int64_t.cu | 5 +- cpp/bench/neighbors/refine_uint8_t_int64_t.cu | 9 +- cpp/cmake/modules/raft_export.cmake | 4 +- cpp/cmake/thirdparty/get_cutlass.cmake | 23 +- cpp/cmake/thirdparty/get_thrust.cmake | 6 +- .../raft/neighbors/specializations.cuh | 6 +- .../{knn.cuh => brute_force.cuh} | 25 - .../raft/spatial/knn/specializations.cuh | 7 +- .../raft/spatial/knn/specializations/knn.cuh | 43 ++ cpp/include/raft/spectral/specializations.cuh | 4 +- cpp/include/raft/stats/specializations.cuh | 4 +- .../raft_runtime/neighbors/brute_force.hpp | 39 ++ .../raft_internal/matrix/select_k.cuh | 2 +- .../raft_internal/neighbors/naive_knn.cuh | 2 +- .../{distance => }/cluster/cluster_cost.cuh | 0 .../cluster/cluster_cost_double.cu | 0 .../cluster/cluster_cost_float.cu | 0 .../cluster/kmeans_fit_double.cu | 0 .../cluster/kmeans_fit_float.cu | 0 .../cluster/kmeans_init_plus_plus_double.cu | 0 .../cluster/kmeans_init_plus_plus_float.cu | 0 .../cluster/update_centroids.cuh | 0 .../cluster/update_centroids_double.cu | 0 .../cluster/update_centroids_float.cu | 0 .../{distance => }/fused_l2_min_arg.cu | 0 .../{distance => }/pairwise_distance.cu | 0 .../canberra_double_double_double_int.cu | 0 .../detail/canberra_float_float_float_int.cu | 0 .../correlation_double_double_double_int.cu | 0 .../correlation_float_float_float_int.cu | 0 .../detail/cosine_double_double_double_int.cu | 0 .../detail/cosine_float_float_float_int.cu | 0 ...ing_unexpanded_double_double_double_int.cu | 0 ...amming_unexpanded_float_float_float_int.cu | 0 ...inger_expanded_double_double_double_int.cu | 0 ...ellinger_expanded_float_float_float_int.cu | 0 .../inner_product_double_double_double_int.cu | 0 .../inner_product_float_float_float_int.cu | 0 ...jensen_shannon_double_double_double_int.cu | 0 .../jensen_shannon_float_float_float_int.cu | 0 .../detail/kernels/gram_matrix_base_double.cu | 0 .../detail/kernels/gram_matrix_base_float.cu | 0 .../kernels/polynomial_kernel_double_int.cu | 0 .../kernels/polynomial_kernel_float_int.cu | 0 .../detail/kernels/rbf_kernel_double.cu | 0 .../detail/kernels/rbf_kernel_float.cu | 0 .../detail/kernels/tanh_kernel_double.cu | 0 .../detail/kernels/tanh_kernel_float.cu | 0 .../kl_divergence_double_double_double_int.cu | 0 .../kl_divergence_float_float_float_int.cu | 0 .../detail/l1_double_double_double_int.cu | 0 .../detail/l1_float_float_float_int.cu | 0 .../l2_expanded_double_double_double_int.cu | 0 .../l2_expanded_float_float_float_int.cu | 0 ..._sqrt_expanded_double_double_double_int.cu | 0 .../l2_sqrt_expanded_float_float_float_int.cu | 0 ...qrt_unexpanded_double_double_double_int.cu | 0 ...2_sqrt_unexpanded_float_float_float_int.cu | 0 .../l2_unexpanded_double_double_double_int.cu | 0 .../l2_unexpanded_float_float_float_int.cu | 0 .../detail/l_inf_double_double_double_int.cu | 0 .../detail/l_inf_float_float_float_int.cu | 0 .../lp_unexpanded_double_double_double_int.cu | 0 .../lp_unexpanded_float_float_float_int.cu | 0 .../russel_rao_double_double_double_int.cu | 0 .../russel_rao_float_float_float_int.cu | 0 .../specializations/fused_l2_nn_double_int.cu | 0 .../fused_l2_nn_double_int64.cu | 0 .../specializations/fused_l2_nn_float_int.cu | 0 .../fused_l2_nn_float_int64.cu | 0 .../detail/select_k_float_int64_t.cu | 0 .../detail/select_k_float_uint32_t.cu | 0 .../detail/select_k_half_int64_t.cu | 0 .../detail/select_k_half_uint32_t.cu | 0 .../brute_force_knn_int64_t_float.cu | 54 ++ .../neighbors/ivf_flat_build.cu | 0 .../neighbors/ivf_flat_search.cu | 0 .../{distance => }/neighbors/ivfpq_build.cu | 0 .../neighbors/ivfpq_deserialize.cu | 0 .../neighbors/ivfpq_search_float_int64_t.cu | 0 .../neighbors/ivfpq_search_int8_t_int64_t.cu | 0 .../neighbors/ivfpq_search_uint8_t_int64_t.cu | 0 .../neighbors/ivfpq_serialize.cu | 0 .../neighbors/refine_d_int64_t_float.cu | 0 .../neighbors/refine_d_int64_t_int8_t.cu | 0 .../neighbors/refine_d_int64_t_uint8_t.cu | 0 .../neighbors/refine_h_int64_t_float.cu | 0 .../neighbors/refine_h_int64_t_int8_t.cu | 0 .../neighbors/refine_h_int64_t_uint8_t.cu | 0 .../ball_cover_all_knn_query.cu | 9 +- .../specializations/ball_cover_build_index.cu | 9 +- .../specializations/ball_cover_knn_query.cu | 12 +- .../detail/ball_cover_lowdim_pass_one_2d.cu | 4 +- .../detail/ball_cover_lowdim_pass_one_3d.cu | 4 +- .../detail/ball_cover_lowdim_pass_two_2d.cu | 5 +- .../detail/ball_cover_lowdim_pass_two_3d.cu | 4 +- .../brute_force_knn_impl_long_float_int.cu | 38 ++ .../brute_force_knn_impl_long_float_uint.cu | 38 ++ .../brute_force_knn_impl_uint_float_int.cu | 38 ++ .../brute_force_knn_impl_uint_float_uint.cu | 38 ++ .../compute_similarity_float_float_fast.cu | 0 ...pute_similarity_float_float_no_basediff.cu | 0 ...pute_similarity_float_float_no_smem_lut.cu | 0 .../compute_similarity_float_fp8s_fast.cu | 0 ...mpute_similarity_float_fp8s_no_basediff.cu | 0 ...mpute_similarity_float_fp8s_no_smem_lut.cu | 0 .../compute_similarity_float_fp8u_fast.cu | 0 ...mpute_similarity_float_fp8u_no_basediff.cu | 0 ...mpute_similarity_float_fp8u_no_smem_lut.cu | 0 .../compute_similarity_float_half_fast.cu | 0 ...mpute_similarity_float_half_no_basediff.cu | 0 ...mpute_similarity_float_half_no_smem_lut.cu | 0 .../compute_similarity_half_fp8s_fast.cu | 0 ...ompute_similarity_half_fp8s_no_basediff.cu | 0 ...ompute_similarity_half_fp8s_no_smem_lut.cu | 0 .../compute_similarity_half_fp8u_fast.cu | 0 ...ompute_similarity_half_fp8u_no_basediff.cu | 0 ...ompute_similarity_half_fp8u_no_smem_lut.cu | 0 .../compute_similarity_half_half_fast.cu | 0 ...ompute_similarity_half_half_no_basediff.cu | 0 ...ompute_similarity_half_half_no_smem_lut.cu | 0 ...mpute_similarity_float_half_no_smem_lut.cu | 0 .../fused_l2_knn_int_float_false.cu | 4 +- .../fused_l2_knn_int_float_true.cu | 4 +- .../fused_l2_knn_long_float_false.cu | 4 +- .../fused_l2_knn_long_float_true.cu | 4 +- .../ivfflat_build_float_int64_t.cu | 0 .../ivfflat_build_int8_t_int64_t.cu | 0 .../ivfflat_build_uint8_t_int64_t.cu | 0 .../ivfflat_extend_float_int64_t.cu | 0 .../ivfflat_extend_int8_t_int64_t.cu | 0 .../ivfflat_extend_uint8_t_int64_t.cu | 0 .../ivfflat_search_float_int64_t.cu | 0 .../ivfflat_search_int8_t_int64_t.cu | 0 .../ivfflat_search_uint8_t_int64_t.cu | 0 .../ivfpq_build_float_int64_t.cu | 0 .../ivfpq_build_int8_t_int64_t.cu | 0 .../ivfpq_build_uint8_t_int64_t.cu | 0 .../ivfpq_extend_float_int64_t.cu | 0 .../ivfpq_extend_int8_t_int64_t.cu | 0 .../ivfpq_extend_uint8_t_int64_t.cu | 0 .../ivfpq_search_float_int64_t.cu | 0 .../ivfpq_search_int8_t_int64_t.cu | 0 .../ivfpq_search_uint8_t_int64_t.cu | 0 .../specializations/refine_d_int64_t_float.cu | 0 .../refine_d_int64_t_int8_t.cu | 0 .../refine_d_int64_t_uint8_t.cu | 0 .../specializations/refine_h_int64_t_float.cu | 0 .../refine_h_int64_t_int8_t.cu | 0 .../refine_h_int64_t_uint8_t.cu | 0 cpp/src/{distance => }/random/common.cuh | 0 ...rmat_rectangular_generator_int64_double.cu | 2 +- .../rmat_rectangular_generator_int64_float.cu | 2 +- .../rmat_rectangular_generator_int_double.cu | 2 +- .../rmat_rectangular_generator_int_float.cu | 2 +- cpp/test/CMakeLists.txt | 25 +- cpp/test/cluster/cluster_solvers.cu | 2 +- cpp/test/cluster/kmeans.cu | 2 +- cpp/test/cluster/kmeans_balanced.cu | 2 +- cpp/test/cluster/kmeans_find_k.cu | 2 +- cpp/test/cluster/linkage.cu | 4 +- cpp/test/distance/distance_base.cuh | 2 +- cpp/test/distance/fused_l2_nn.cu | 2 +- cpp/test/distance/gram.cu | 2 +- cpp/test/distance/masked_nn.cu | 2 +- cpp/test/matrix/select_k.cu | 2 +- cpp/test/neighbors/ann_ivf_flat.cuh | 2 +- .../ann_ivf_flat/test_float_int64_t.cu | 2 +- .../ann_ivf_flat/test_int8_t_int64_t.cu | 2 +- .../ann_ivf_flat/test_uint8_t_int64_t.cu | 2 +- cpp/test/neighbors/ann_ivf_pq.cuh | 2 +- cpp/test/neighbors/ball_cover.cu | 5 +- cpp/test/neighbors/epsilon_neighborhood.cu | 2 +- cpp/test/neighbors/fused_l2_knn.cu | 8 +- cpp/test/neighbors/knn.cu | 6 +- cpp/test/neighbors/refine.cu | 2 +- cpp/test/neighbors/selection.cu | 2 +- cpp/test/neighbors/tiled_knn.cu | 3 +- cpp/test/sparse/neighbors/knn_graph.cu | 4 +- cpp/test/stats/silhouette_score.cu | 2 +- cpp/test/stats/trustworthiness.cu | 2 +- docs/source/build.md | 65 +-- python/pylibraft/CMakeLists.txt | 13 +- .../pylibraft/cluster/CMakeLists.txt | 4 +- .../pylibraft/distance/CMakeLists.txt | 4 +- .../pylibraft/neighbors/CMakeLists.txt | 2 +- .../neighbors/ivf_flat/CMakeLists.txt | 2 +- .../pylibraft/neighbors/ivf_pq/CMakeLists.txt | 4 +- .../pylibraft/pylibraft/random/CMakeLists.txt | 4 +- 208 files changed, 687 insertions(+), 686 deletions(-) rename conda/recipes/libraft/{build_libraft_nn.sh => build_libraft.sh} (54%) delete mode 100644 conda/recipes/libraft/build_libraft_distance.sh rename cpp/include/raft/neighbors/specializations/{knn.cuh => brute_force.cuh} (61%) create mode 100644 cpp/include/raft/spatial/knn/specializations/knn.cuh create mode 100644 cpp/include/raft_runtime/neighbors/brute_force.hpp rename cpp/src/{distance => }/cluster/cluster_cost.cuh (100%) rename cpp/src/{distance => }/cluster/cluster_cost_double.cu (100%) rename cpp/src/{distance => }/cluster/cluster_cost_float.cu (100%) rename cpp/src/{distance => }/cluster/kmeans_fit_double.cu (100%) rename cpp/src/{distance => }/cluster/kmeans_fit_float.cu (100%) rename cpp/src/{distance => }/cluster/kmeans_init_plus_plus_double.cu (100%) rename cpp/src/{distance => }/cluster/kmeans_init_plus_plus_float.cu (100%) rename cpp/src/{distance => }/cluster/update_centroids.cuh (100%) rename cpp/src/{distance => }/cluster/update_centroids_double.cu (100%) rename cpp/src/{distance => }/cluster/update_centroids_float.cu (100%) rename cpp/src/distance/{distance => }/fused_l2_min_arg.cu (100%) rename cpp/src/distance/{distance => }/pairwise_distance.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/canberra_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/canberra_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/correlation_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/correlation_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/cosine_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/cosine_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/hamming_unexpanded_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/hamming_unexpanded_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/hellinger_expanded_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/hellinger_expanded_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/inner_product_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/inner_product_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/jensen_shannon_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/jensen_shannon_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kernels/gram_matrix_base_double.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kernels/gram_matrix_base_float.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kernels/polynomial_kernel_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kernels/polynomial_kernel_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kernels/rbf_kernel_double.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kernels/rbf_kernel_float.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kernels/tanh_kernel_double.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kernels/tanh_kernel_float.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kl_divergence_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/kl_divergence_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l1_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l1_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l2_expanded_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l2_expanded_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l2_unexpanded_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l2_unexpanded_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l_inf_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/l_inf_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/lp_unexpanded_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/lp_unexpanded_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/russel_rao_double_double_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/detail/russel_rao_float_float_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/fused_l2_nn_double_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/fused_l2_nn_double_int64.cu (100%) rename cpp/src/distance/{distance => }/specializations/fused_l2_nn_float_int.cu (100%) rename cpp/src/distance/{distance => }/specializations/fused_l2_nn_float_int64.cu (100%) rename cpp/src/{distance => }/matrix/specializations/detail/select_k_float_int64_t.cu (100%) rename cpp/src/{distance => }/matrix/specializations/detail/select_k_float_uint32_t.cu (100%) rename cpp/src/{distance => }/matrix/specializations/detail/select_k_half_int64_t.cu (100%) rename cpp/src/{distance => }/matrix/specializations/detail/select_k_half_uint32_t.cu (100%) create mode 100644 cpp/src/neighbors/brute_force_knn_int64_t_float.cu rename cpp/src/{distance => }/neighbors/ivf_flat_build.cu (100%) rename cpp/src/{distance => }/neighbors/ivf_flat_search.cu (100%) rename cpp/src/{distance => }/neighbors/ivfpq_build.cu (100%) rename cpp/src/{distance => }/neighbors/ivfpq_deserialize.cu (100%) rename cpp/src/{distance => }/neighbors/ivfpq_search_float_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/ivfpq_search_int8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/ivfpq_search_uint8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/ivfpq_serialize.cu (100%) rename cpp/src/{distance => }/neighbors/refine_d_int64_t_float.cu (100%) rename cpp/src/{distance => }/neighbors/refine_d_int64_t_int8_t.cu (100%) rename cpp/src/{distance => }/neighbors/refine_d_int64_t_uint8_t.cu (100%) rename cpp/src/{distance => }/neighbors/refine_h_int64_t_float.cu (100%) rename cpp/src/{distance => }/neighbors/refine_h_int64_t_int8_t.cu (100%) rename cpp/src/{distance => }/neighbors/refine_h_int64_t_uint8_t.cu (100%) rename cpp/src/{nn => neighbors}/specializations/ball_cover_all_knn_query.cu (80%) rename cpp/src/{nn => neighbors}/specializations/ball_cover_build_index.cu (81%) rename cpp/src/{nn => neighbors}/specializations/ball_cover_knn_query.cu (80%) rename cpp/src/{nn => neighbors}/specializations/detail/ball_cover_lowdim_pass_one_2d.cu (91%) rename cpp/src/{nn => neighbors}/specializations/detail/ball_cover_lowdim_pass_one_3d.cu (91%) rename cpp/src/{nn => neighbors}/specializations/detail/ball_cover_lowdim_pass_two_2d.cu (91%) rename cpp/src/{nn => neighbors}/specializations/detail/ball_cover_lowdim_pass_two_3d.cu (91%) create mode 100644 cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_int.cu create mode 100644 cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_uint.cu create mode 100644 cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_int.cu create mode 100644 cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_uint.cu rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_float_fast.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_float_no_basediff.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_float_no_smem_lut.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_fp8s_fast.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_fp8s_no_basediff.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_fp8s_no_smem_lut.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_fp8u_fast.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_fp8u_no_basediff.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_fp8u_no_smem_lut.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_half_fast.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_half_no_basediff.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_float_half_no_smem_lut.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_half_fp8s_fast.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_half_fp8s_no_basediff.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_half_fp8s_no_smem_lut.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_half_fp8u_fast.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_half_fp8u_no_basediff.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_half_fp8u_no_smem_lut.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_half_half_fast.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_half_half_no_basediff.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/compute_similarity_half_half_no_smem_lut.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/detail/ivfpq_compute_similarity_float_half_no_smem_lut.cu (100%) rename cpp/src/{nn => neighbors}/specializations/fused_l2_knn_int_float_false.cu (93%) rename cpp/src/{nn => neighbors}/specializations/fused_l2_knn_int_float_true.cu (93%) rename cpp/src/{nn => neighbors}/specializations/fused_l2_knn_long_float_false.cu (93%) rename cpp/src/{nn => neighbors}/specializations/fused_l2_knn_long_float_true.cu (93%) rename cpp/src/{distance => }/neighbors/specializations/ivfflat_build_float_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfflat_build_int8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfflat_build_uint8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfflat_extend_float_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfflat_extend_int8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfflat_extend_uint8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfflat_search_float_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfpq_build_float_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfpq_build_int8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfpq_build_uint8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfpq_extend_float_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfpq_extend_int8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfpq_extend_uint8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfpq_search_float_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfpq_search_int8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/ivfpq_search_uint8_t_int64_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/refine_d_int64_t_float.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/refine_d_int64_t_int8_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/refine_d_int64_t_uint8_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/refine_h_int64_t_float.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/refine_h_int64_t_int8_t.cu (100%) rename cpp/src/{distance => }/neighbors/specializations/refine_h_int64_t_uint8_t.cu (100%) rename cpp/src/{distance => }/random/common.cuh (100%) rename cpp/src/{distance => }/random/rmat_rectangular_generator_int64_double.cu (93%) rename cpp/src/{distance => }/random/rmat_rectangular_generator_int64_float.cu (93%) rename cpp/src/{distance => }/random/rmat_rectangular_generator_int_double.cu (93%) rename cpp/src/{distance => }/random/rmat_rectangular_generator_int_float.cu (93%) diff --git a/README.md b/README.md index 6d9de4c8b7..8519ebcae1 100755 --- a/README.md +++ b/README.md @@ -195,8 +195,7 @@ RAFT itself can be installed through conda, [CMake Package Manager (CPM)](https: The easiest way to install RAFT is through conda and several packages are provided. - `libraft-headers` RAFT headers -- `libraft-nn` (optional) contains shared libraries for the nearest neighbors primitives. -- `libraft-distance` (optional) contains shared libraries for distance primitives. +- `libraft` (optional) shared library of pre-compiled template specializations and runtime APIs. - `pylibraft` (optional) Python wrappers around RAFT algorithms and primitives. - `raft-dask` (optional) enables deployment of multi-node multi-GPU algorithms that use RAFT `raft::comms` in Dask clusters. @@ -205,9 +204,9 @@ Use the following command to install all of the RAFT packages with conda (replac mamba install -c rapidsai -c conda-forge -c nvidia raft-dask pylibraft ``` -You can also install the `libraft-*` conda packages individually using the `mamba` command above. +You can also install the conda packages individually using the `mamba` command above. -After installing RAFT, `find_package(raft COMPONENTS nn distance)` can be used in your CUDA/C++ cmake build to compile and/or link against needed dependencies in your raft target. `COMPONENTS` are optional and will depend on the packages installed. +After installing RAFT, `find_package(raft COMPONENTS compiled distributed)` can be used in your CUDA/C++ cmake build to compile and/or link against needed dependencies in your raft target. `COMPONENTS` are optional and will depend on the packages installed. ### Pip @@ -266,12 +265,11 @@ find_and_configure_raft(VERSION ${RAFT_VERSION}.00 Several CMake targets can be made available by adding components in the table below to the `RAFT_COMPONENTS` list above, separated by spaces. The `raft::raft` target will always be available. RAFT headers require, at a minimum, the CUDA toolkit libraries and RMM dependencies. -| Component | Target | Description | Base Dependencies | -| --- | --- | --- |------------------------------------------------------------------| -| n/a | `raft::raft` | Full RAFT header library | CUDA toolkit library, RMM, Thrust (optional), NVTools (optional) | -| distance | `raft::distance` | Pre-compiled template specializations for raft::distance | raft::raft, cuCollections (optional) | -| nn | `raft::nn` | Pre-compiled template specializations for raft::neighbors | raft::raft | -| distributed | `raft::distributed` | No specializations | raft::raft, UCX, NCCL | +| Component | Target | Description | Base Dependencies | +|-------------|---------------------|-----------------------------------------------------------|---------------------------------------| +| n/a | `raft::raft` | Full RAFT header library | CUDA toolkit, RMM, NVTX, CCCL, CUTLASS | +| compiled | `raft::compiled` | Pre-compiled template specializations and runtime library | raft::raft | +| distributed | `raft::distributed` | Dependencies for `raft::comms` APIs | raft::raft, UCX, NCCL | ### Source @@ -282,7 +280,7 @@ mamba env create --name raft_dev_env -f conda/environments/all_cuda-118_arch-x86 mamba activate raft_dev_env ``` ``` -./build.sh raft-dask pylibraft libraft tests bench --compile-libs +./build.sh raft-dask pylibraft libraft tests bench --compile-lib ``` The [build](docs/source/build.md) instructions contain more details on building RAFT from source and including it in downstream projects. You can also find a more comprehensive version of the above CPM code snippet the [Building RAFT C++ from source](docs/source/build.md#building-raft-c-from-source-in-cmake) section of the build instructions. diff --git a/build.sh b/build.sh index 7215b8199a..b5a72f4205 100755 --- a/build.sh +++ b/build.sh @@ -18,7 +18,7 @@ ARGS=$* # script, and that this script resides in the repo dir! REPODIR=$(cd $(dirname $0); pwd) -VALIDARGS="clean libraft pylibraft raft-dask docs tests bench clean --uninstall -v -g -n --compile-libs --compile-nn --compile-dist --allgpuarch --no-nvtx --show_depr_warn -h --minimal-deps" +VALIDARGS="clean libraft pylibraft raft-dask docs tests bench clean --uninstall -v -g -n --compile-lib --allgpuarch --no-nvtx --show_depr_warn -h" HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=] [--limit-tests=] [--limit-bench=] where is: clean - remove all existing build artifacts and configuration (start over) @@ -35,12 +35,7 @@ HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool==11.7.1,<12.0 - cudatoolkit ={{ cuda_version }} - cython >=0.29,<0.30 - - libraft-distance {{ version }} + - libraft {{ version }} - libraft-headers {{ version }} - python x.x - rmm ={{ minor_version }} @@ -43,7 +43,7 @@ requirements: run: - {{ pin_compatible('cudatoolkit', max_pin='x', min_pin='x') }} - cuda-python >=11.7.1,<12.0 - - libraft-distance {{ version }} + - libraft {{ version }} - libraft-headers {{ version }} - python x.x diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 3889c39e6e..bdaacb4a85 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -57,22 +57,13 @@ option(DISABLE_DEPRECATION_WARNINGS "Disable deprecaction warnings " ON) option(DISABLE_OPENMP "Disable OpenMP" OFF) option(RAFT_NVTX "Enable nvtx markers" OFF) -set(RAFT_COMPILE_LIBRARIES_DEFAULT OFF) +set(RAFT_COMPILE_LIBRARY_DEFAULT OFF) if(BUILD_TESTS OR BUILD_BENCH) - set(RAFT_COMPILE_LIBRARIES_DEFAULT ON) + set(RAFT_COMPILE_LIBRARY_DEFAULT ON) endif() -option(RAFT_COMPILE_LIBRARIES "Enable building raft shared library instantiations" - ${RAFT_COMPILE_LIBRARIES_DEFAULT} +option(RAFT_COMPILE_LIBRARY "Enable building raft shared library instantiations" + ${RAFT_COMPILE_LIBRARY_DEFAULT} ) -option( - RAFT_COMPILE_NN_LIBRARY "Enable building raft nearest neighbors shared library instantiations" - ${RAFT_COMPILE_LIBRARIES} -) -option(RAFT_COMPILE_DIST_LIBRARY "Enable building raft distant shared library instantiations" - ${RAFT_COMPILE_LIBRARIES} -) - -option(RAFT_ENABLE_thrust_DEPENDENCY "Enable Thrust dependency" ON) if(BUILD_TESTS OR BUILD_BENCH) # Needed because GoogleBenchmark changes the state of FindThreads.cmake, causing subsequent runs @@ -81,10 +72,9 @@ if(BUILD_TESTS OR BUILD_BENCH) set(THREADS_PREFER_PTHREAD_FLAG ON) endif() -if(BUILD_TESTS AND NOT RAFT_ENABLE_thrust_DEPENDENCY) - message(VERBOSE "RAFT: BUILD_TESTS is enabled, overriding RAFT_ENABLE_thrust_DEPENDENCY") - set(RAFT_ENABLE_thrust_DEPENDENCY ON) -endif() +include(CMakeDependentOption) +# cmake_dependent_option( RAFT_USE_FAISS_STATIC "Build and statically link the FAISS library for +# nearest neighbors search on GPU" ON RAFT_COMPILE_LIBRARY OFF ) message(VERBOSE "RAFT: Building optional components: ${raft_FIND_COMPONENTS}") message(VERBOSE "RAFT: Build RAFT unit-tests: ${BUILD_TESTS}") @@ -154,15 +144,6 @@ include(cmake/modules/ConfigureCUDA.cmake) # ################################################################################################## # * Requirements ------------------------------------------------------------- -if(RAFT_COMPILE_LIBRARIES) - set(RAFT_COMPILE_DIST_LIBRARY ON) - set(RAFT_COMPILE_NN_LIBRARY ON) -endif() - -if(RAFT_COMPILE_DIST_LIBRARY OR distance IN_LIST raft_FIND_COMPONENTS) - set(RAFT_ENABLE_cuco_DEPENDENCY ON) -endif() - # add third party dependencies using CPM rapids_cpm_init() @@ -171,12 +152,8 @@ include(cmake/thirdparty/get_thrust.cmake) include(cmake/thirdparty/get_rmm.cmake) include(cmake/thirdparty/get_cutlass.cmake) -if(RAFT_ENABLE_cuco_DEPENDENCY) - include(${rapids-cmake-dir}/cpm/cuco.cmake) - rapids_cpm_cuco( - BUILD_EXPORT_SET raft-distance-lib-exports INSTALL_EXPORT_SET raft-distance-lib-exports - ) -endif() +include(${rapids-cmake-dir}/cpm/cuco.cmake) +rapids_cpm_cuco(BUILD_EXPORT_SET raft-exports INSTALL_EXPORT_SET raft-exports) if(BUILD_TESTS) include(cmake/thirdparty/get_gtest.cmake) @@ -200,11 +177,13 @@ target_include_directories( target_link_libraries( raft INTERFACE rmm::rmm + cuco::cuco + nvidia::cutlass::cutlass CUDA::cublas${_ctk_static_suffix} CUDA::curand${_ctk_static_suffix} CUDA::cusolver${_ctk_static_suffix} CUDA::cusparse${_ctk_static_suffix} - $<$:raft::Thrust> + raft::Thrust ) target_compile_features(raft INTERFACE cxx_std_17 $) @@ -222,7 +201,7 @@ else() target_compile_definitions(raft INTERFACE RAFT_SYSTEM_LITTLE_ENDIAN=1) endif() -if(RAFT_COMPILE_DIST_LIBRARY OR RAFT_COMPILE_NN_LIBRARY) +if(RAFT_COMPILE_LIBRARY) file( WRITE "${CMAKE_CURRENT_BINARY_DIR}/fatbin.ld" [=[ @@ -266,148 +245,203 @@ target_compile_definitions(raft::raft INTERFACE $<$:NVTX_ENAB endif() # ################################################################################################## -# * raft_distance ------------------------------------------------------------ TODO: Currently, this +# * raft_compiled ------------------------------------------------------------ TODO: Currently, this # package also contains the 'random' namespace (for rmat logic) We couldn't get this to work # properly due to strange CI failures as noticed in the PR#778. In the long term, we should rename # this package to `raft_compiled` in order to have a single pre-compiled raft package for those # who need it. -add_library(raft_distance INTERFACE) +add_library(raft_compiled INTERFACE) -if(TARGET raft_distance AND (NOT TARGET raft::distance)) - add_library(raft::distance ALIAS raft_distance) +if(TARGET raft_compiled AND (NOT TARGET raft::compiled)) + add_library(raft::compiled ALIAS raft_compiled) endif() -set_target_properties(raft_distance PROPERTIES EXPORT_NAME distance) +set_target_properties(raft_compiled PROPERTIES EXPORT_NAME compiled) -if(RAFT_COMPILE_DIST_LIBRARY) +if(RAFT_COMPILE_LIBRARY) add_library( - raft_distance_lib - src/distance/distance/pairwise_distance.cu - src/distance/distance/fused_l2_min_arg.cu - src/distance/cluster/update_centroids_float.cu - src/distance/cluster/update_centroids_double.cu - src/distance/cluster/cluster_cost_float.cu - src/distance/cluster/cluster_cost_double.cu - src/distance/neighbors/refine_d_int64_t_float.cu - src/distance/neighbors/refine_d_int64_t_int8_t.cu - src/distance/neighbors/refine_d_int64_t_uint8_t.cu - src/distance/neighbors/refine_h_int64_t_float.cu - src/distance/neighbors/refine_h_int64_t_int8_t.cu - src/distance/neighbors/refine_h_int64_t_uint8_t.cu - src/distance/neighbors/specializations/refine_d_int64_t_float.cu - src/distance/neighbors/specializations/refine_d_int64_t_int8_t.cu - src/distance/neighbors/specializations/refine_d_int64_t_uint8_t.cu - src/distance/neighbors/specializations/refine_h_int64_t_float.cu - src/distance/neighbors/specializations/refine_h_int64_t_int8_t.cu - src/distance/neighbors/specializations/refine_h_int64_t_uint8_t.cu - src/distance/cluster/kmeans_fit_float.cu - src/distance/cluster/kmeans_fit_double.cu - src/distance/cluster/kmeans_init_plus_plus_double.cu - src/distance/cluster/kmeans_init_plus_plus_float.cu - src/distance/distance/specializations/detail/canberra_double_double_double_int.cu - src/distance/distance/specializations/detail/canberra_float_float_float_int.cu - src/distance/distance/specializations/detail/correlation_double_double_double_int.cu - src/distance/distance/specializations/detail/correlation_float_float_float_int.cu - src/distance/distance/specializations/detail/cosine_double_double_double_int.cu - src/distance/distance/specializations/detail/cosine_float_float_float_int.cu - src/distance/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu - src/distance/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu - src/distance/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu - src/distance/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu - src/distance/distance/specializations/detail/inner_product_float_float_float_int.cu - src/distance/distance/specializations/detail/inner_product_double_double_double_int.cu - src/distance/distance/specializations/detail/jensen_shannon_float_float_float_int.cu - src/distance/distance/specializations/detail/jensen_shannon_double_double_double_int.cu - src/distance/distance/specializations/detail/kernels/gram_matrix_base_double.cu - src/distance/distance/specializations/detail/kernels/gram_matrix_base_float.cu - src/distance/distance/specializations/detail/kernels/polynomial_kernel_double_int.cu - src/distance/distance/specializations/detail/kernels/polynomial_kernel_float_int.cu + raft_lib + src/distance/pairwise_distance.cu + src/distance/fused_l2_min_arg.cu + src/cluster/update_centroids_float.cu + src/cluster/update_centroids_double.cu + src/cluster/cluster_cost_float.cu + src/cluster/cluster_cost_double.cu + src/neighbors/refine_d_int64_t_float.cu + src/neighbors/refine_d_int64_t_int8_t.cu + src/neighbors/refine_d_int64_t_uint8_t.cu + src/neighbors/refine_h_int64_t_float.cu + src/neighbors/refine_h_int64_t_int8_t.cu + src/neighbors/refine_h_int64_t_uint8_t.cu + src/neighbors/specializations/refine_d_int64_t_float.cu + src/neighbors/specializations/refine_d_int64_t_int8_t.cu + src/neighbors/specializations/refine_d_int64_t_uint8_t.cu + src/neighbors/specializations/refine_h_int64_t_float.cu + src/neighbors/specializations/refine_h_int64_t_int8_t.cu + src/neighbors/specializations/refine_h_int64_t_uint8_t.cu + src/cluster/kmeans_fit_float.cu + src/cluster/kmeans_fit_double.cu + src/cluster/kmeans_init_plus_plus_double.cu + src/cluster/kmeans_init_plus_plus_float.cu + src/distance/specializations/detail/canberra_double_double_double_int.cu + src/distance/specializations/detail/canberra_float_float_float_int.cu + src/distance/specializations/detail/correlation_double_double_double_int.cu + src/distance/specializations/detail/correlation_float_float_float_int.cu + src/distance/specializations/detail/cosine_double_double_double_int.cu + src/distance/specializations/detail/cosine_float_float_float_int.cu + src/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu + src/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu + src/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu + src/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu + src/distance/specializations/detail/inner_product_float_float_float_int.cu + src/distance/specializations/detail/inner_product_double_double_double_int.cu + src/distance/specializations/detail/jensen_shannon_float_float_float_int.cu + src/distance/specializations/detail/jensen_shannon_double_double_double_int.cu + src/distance/specializations/detail/kernels/gram_matrix_base_double.cu + src/distance/specializations/detail/kernels/gram_matrix_base_float.cu + src/distance/specializations/detail/kernels/polynomial_kernel_double_int.cu + src/distance/specializations/detail/kernels/polynomial_kernel_float_int.cu # These are somehow missing a kernel definition which is causing a compile error. # src/distance/specializations/detail/kernels/rbf_kernel_double.cu # src/distance/specializations/detail/kernels/rbf_kernel_float.cu - src/distance/distance/specializations/detail/kernels/tanh_kernel_double.cu - src/distance/distance/specializations/detail/kernels/tanh_kernel_float.cu - src/distance/distance/specializations/detail/kl_divergence_float_float_float_int.cu - src/distance/distance/specializations/detail/kl_divergence_double_double_double_int.cu - src/distance/distance/specializations/detail/l1_float_float_float_int.cu - src/distance/distance/specializations/detail/l1_double_double_double_int.cu - src/distance/distance/specializations/detail/l2_expanded_float_float_float_int.cu - src/distance/distance/specializations/detail/l2_expanded_double_double_double_int.cu - src/distance/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu - src/distance/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu - src/distance/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu - src/distance/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu - src/distance/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu - src/distance/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu - src/distance/distance/specializations/detail/l_inf_double_double_double_int.cu - src/distance/distance/specializations/detail/l_inf_float_float_float_int.cu - src/distance/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu - src/distance/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu - src/distance/distance/specializations/detail/russel_rao_double_double_double_int.cu - src/distance/distance/specializations/detail/russel_rao_float_float_float_int.cu - src/distance/distance/specializations/fused_l2_nn_double_int.cu - src/distance/distance/specializations/fused_l2_nn_double_int64.cu - src/distance/distance/specializations/fused_l2_nn_float_int.cu - src/distance/distance/specializations/fused_l2_nn_float_int64.cu - src/distance/matrix/specializations/detail/select_k_float_uint32_t.cu - src/distance/matrix/specializations/detail/select_k_float_int64_t.cu - src/distance/matrix/specializations/detail/select_k_half_uint32_t.cu - src/distance/matrix/specializations/detail/select_k_half_int64_t.cu - src/distance/neighbors/ivf_flat_search.cu - src/distance/neighbors/ivf_flat_build.cu - src/distance/neighbors/specializations/ivfflat_build_float_int64_t.cu - src/distance/neighbors/specializations/ivfflat_build_int8_t_int64_t.cu - src/distance/neighbors/specializations/ivfflat_build_uint8_t_int64_t.cu - src/distance/neighbors/specializations/ivfflat_extend_float_int64_t.cu - src/distance/neighbors/specializations/ivfflat_extend_int8_t_int64_t.cu - src/distance/neighbors/specializations/ivfflat_extend_uint8_t_int64_t.cu - src/distance/neighbors/specializations/ivfflat_search_float_int64_t.cu - src/distance/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu - src/distance/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu - src/distance/neighbors/ivfpq_build.cu - src/distance/neighbors/ivfpq_deserialize.cu - src/distance/neighbors/ivfpq_serialize.cu - src/distance/neighbors/ivfpq_search_float_int64_t.cu - src/distance/neighbors/ivfpq_search_int8_t_int64_t.cu - src/distance/neighbors/ivfpq_search_uint8_t_int64_t.cu - src/distance/neighbors/specializations/ivfpq_build_float_int64_t.cu - src/distance/neighbors/specializations/ivfpq_build_int8_t_int64_t.cu - src/distance/neighbors/specializations/ivfpq_build_uint8_t_int64_t.cu - src/distance/neighbors/specializations/ivfpq_extend_float_int64_t.cu - src/distance/neighbors/specializations/ivfpq_extend_int8_t_int64_t.cu - src/distance/neighbors/specializations/ivfpq_extend_uint8_t_int64_t.cu - src/distance/neighbors/specializations/ivfpq_search_float_int64_t.cu - src/distance/neighbors/specializations/ivfpq_search_int8_t_int64_t.cu - src/distance/neighbors/specializations/ivfpq_search_uint8_t_int64_t.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_float_fast.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_float_no_basediff.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_float_no_smem_lut.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_fp8s_fast.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_fp8s_no_basediff.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_fp8s_no_smem_lut.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_fp8u_fast.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_fp8u_no_basediff.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_fp8u_no_smem_lut.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_half_fast.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_half_no_basediff.cu - src/distance/neighbors/specializations/detail/compute_similarity_float_half_no_smem_lut.cu - src/distance/neighbors/specializations/detail/compute_similarity_half_fp8s_fast.cu - src/distance/neighbors/specializations/detail/compute_similarity_half_fp8s_no_basediff.cu - src/distance/neighbors/specializations/detail/compute_similarity_half_fp8s_no_smem_lut.cu - src/distance/neighbors/specializations/detail/compute_similarity_half_fp8u_fast.cu - src/distance/neighbors/specializations/detail/compute_similarity_half_fp8u_no_basediff.cu - src/distance/neighbors/specializations/detail/compute_similarity_half_fp8u_no_smem_lut.cu - src/distance/neighbors/specializations/detail/compute_similarity_half_half_fast.cu - src/distance/neighbors/specializations/detail/compute_similarity_half_half_no_basediff.cu - src/distance/neighbors/specializations/detail/compute_similarity_half_half_no_smem_lut.cu - src/distance/random/rmat_rectangular_generator_int_double.cu - src/distance/random/rmat_rectangular_generator_int64_double.cu - src/distance/random/rmat_rectangular_generator_int_float.cu - src/distance/random/rmat_rectangular_generator_int64_float.cu + src/distance/specializations/detail/kernels/tanh_kernel_double.cu + src/distance/specializations/detail/kernels/tanh_kernel_float.cu + src/distance/specializations/detail/kl_divergence_float_float_float_int.cu + src/distance/specializations/detail/kl_divergence_double_double_double_int.cu + src/distance/specializations/detail/l1_float_float_float_int.cu + src/distance/specializations/detail/l1_double_double_double_int.cu + src/distance/specializations/detail/l2_expanded_float_float_float_int.cu + src/distance/specializations/detail/l2_expanded_double_double_double_int.cu + src/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu + src/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu + src/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu + src/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu + src/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu + src/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu + src/distance/specializations/detail/l_inf_double_double_double_int.cu + src/distance/specializations/detail/l_inf_float_float_float_int.cu + src/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu + src/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu + src/distance/specializations/detail/russel_rao_double_double_double_int.cu + src/distance/specializations/detail/russel_rao_float_float_float_int.cu + src/distance/specializations/fused_l2_nn_double_int.cu + src/distance/specializations/fused_l2_nn_double_int64.cu + src/distance/specializations/fused_l2_nn_float_int.cu + src/distance/specializations/fused_l2_nn_float_int64.cu + src/matrix/specializations/detail/select_k_float_uint32_t.cu + src/matrix/specializations/detail/select_k_float_int64_t.cu + src/matrix/specializations/detail/select_k_half_uint32_t.cu + src/matrix/specializations/detail/select_k_half_int64_t.cu + src/neighbors/ivfpq_build.cu + src/neighbors/ivfpq_deserialize.cu + src/neighbors/ivfpq_serialize.cu + src/neighbors/ivfpq_search_float_int64_t.cu + src/neighbors/ivfpq_search_int8_t_int64_t.cu + src/neighbors/ivfpq_search_uint8_t_int64_t.cu + src/neighbors/specializations/ivfpq_build_float_int64_t.cu + src/neighbors/specializations/ivfpq_build_int8_t_int64_t.cu + src/neighbors/specializations/ivfpq_build_uint8_t_int64_t.cu + src/neighbors/specializations/ivfpq_extend_float_int64_t.cu + src/neighbors/specializations/ivfpq_extend_int8_t_int64_t.cu + src/neighbors/specializations/ivfpq_extend_uint8_t_int64_t.cu + src/neighbors/specializations/ivfpq_search_float_int64_t.cu + src/neighbors/specializations/ivfpq_search_int8_t_int64_t.cu + src/neighbors/specializations/ivfpq_search_uint8_t_int64_t.cu + src/neighbors/specializations/detail/brute_force_knn_impl_long_float_int.cu + src/neighbors/specializations/detail/brute_force_knn_impl_long_float_uint.cu + src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_int.cu + src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_uint.cu + src/neighbors/specializations/detail/compute_similarity_float_float_fast.cu + src/neighbors/specializations/detail/compute_similarity_float_float_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_float_float_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8s_fast.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8s_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8s_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8u_fast.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8u_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8u_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_float_half_fast.cu + src/neighbors/specializations/detail/compute_similarity_float_half_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_float_half_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8s_fast.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8s_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8s_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8u_fast.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8u_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8u_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_half_half_fast.cu + src/neighbors/specializations/detail/compute_similarity_half_half_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_half_half_no_smem_lut.cu + src/random/rmat_rectangular_generator_int_double.cu + src/random/rmat_rectangular_generator_int64_double.cu + src/random/rmat_rectangular_generator_int_float.cu + src/random/rmat_rectangular_generator_int64_float.cu + src/neighbors/specializations/detail/ball_cover_lowdim_pass_one_2d.cu + src/neighbors/specializations/detail/ball_cover_lowdim_pass_two_2d.cu + src/neighbors/specializations/detail/ball_cover_lowdim_pass_one_3d.cu + src/neighbors/specializations/detail/ball_cover_lowdim_pass_two_3d.cu + src/neighbors/specializations/ball_cover_all_knn_query.cu + src/neighbors/specializations/ball_cover_build_index.cu + src/neighbors/specializations/ball_cover_knn_query.cu + src/neighbors/specializations/fused_l2_knn_long_float_true.cu + src/neighbors/specializations/fused_l2_knn_long_float_false.cu + src/neighbors/specializations/fused_l2_knn_int_float_true.cu + src/neighbors/specializations/fused_l2_knn_int_float_false.cu + src/neighbors/ivf_flat_search.cu + src/neighbors/ivf_flat_build.cu + src/neighbors/specializations/ivfflat_build_float_int64_t.cu + src/neighbors/specializations/ivfflat_build_int8_t_int64_t.cu + src/neighbors/specializations/ivfflat_build_uint8_t_int64_t.cu + src/neighbors/specializations/ivfflat_extend_float_int64_t.cu + src/neighbors/specializations/ivfflat_extend_int8_t_int64_t.cu + src/neighbors/specializations/ivfflat_extend_uint8_t_int64_t.cu + src/neighbors/specializations/ivfflat_search_float_int64_t.cu + src/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu + src/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu + src/neighbors/ivfpq_build.cu + src/neighbors/ivfpq_deserialize.cu + src/neighbors/ivfpq_serialize.cu + src/neighbors/ivfpq_search_float_int64_t.cu + src/neighbors/ivfpq_search_int8_t_int64_t.cu + src/neighbors/ivfpq_search_uint8_t_int64_t.cu + src/neighbors/specializations/ivfpq_build_float_int64_t.cu + src/neighbors/specializations/ivfpq_build_int8_t_int64_t.cu + src/neighbors/specializations/ivfpq_build_uint8_t_int64_t.cu + src/neighbors/specializations/ivfpq_extend_float_int64_t.cu + src/neighbors/specializations/ivfpq_extend_int8_t_int64_t.cu + src/neighbors/specializations/ivfpq_extend_uint8_t_int64_t.cu + src/neighbors/specializations/ivfpq_search_float_int64_t.cu + src/neighbors/specializations/ivfpq_search_int8_t_int64_t.cu + src/neighbors/specializations/ivfpq_search_uint8_t_int64_t.cu + src/neighbors/specializations/detail/compute_similarity_float_float_fast.cu + src/neighbors/specializations/detail/compute_similarity_float_float_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_float_float_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8s_fast.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8s_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8s_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8u_fast.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8u_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_float_fp8u_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_float_half_fast.cu + src/neighbors/specializations/detail/compute_similarity_float_half_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_float_half_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8s_fast.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8s_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8s_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8u_fast.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8u_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_half_fp8u_no_smem_lut.cu + src/neighbors/specializations/detail/compute_similarity_half_half_fast.cu + src/neighbors/specializations/detail/compute_similarity_half_half_no_basediff.cu + src/neighbors/specializations/detail/compute_similarity_half_half_no_smem_lut.cu + src/random/rmat_rectangular_generator_int_double.cu + src/random/rmat_rectangular_generator_int64_double.cu + src/random/rmat_rectangular_generator_int_float.cu + src/random/rmat_rectangular_generator_int64_float.cu ) set_target_properties( - raft_distance_lib - PROPERTIES OUTPUT_NAME raft_distance + raft_lib + PROPERTIES OUTPUT_NAME raft BUILD_RPATH "\$ORIGIN" INSTALL_RPATH "\$ORIGIN" CXX_STANDARD 17 @@ -418,95 +452,23 @@ if(RAFT_COMPILE_DIST_LIBRARY) INTERFACE_POSITION_INDEPENDENT_CODE ON ) - target_link_libraries( - raft_distance_lib - PUBLIC raft::raft cuco::cuco - PRIVATE nvidia::cutlass::cutlass $ - ) + target_link_libraries(raft_lib PUBLIC raft::raft $) target_compile_options( - raft_distance_lib PRIVATE "$<$:${RAFT_CXX_FLAGS}>" - "$<$:${RAFT_CUDA_FLAGS}>" - ) - target_compile_definitions(raft_distance_lib INTERFACE "RAFT_DISTANCE_COMPILED") - - # ensure CUDA symbols aren't relocated to the middle of the debug build binaries - target_link_options(raft_distance_lib PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/fatbin.ld") - -endif() - -if(TARGET raft_distance_lib AND (NOT TARGET raft::raft_distance_lib)) - add_library(raft::raft_distance_lib ALIAS raft_distance_lib) -endif() - -target_link_libraries( - raft_distance INTERFACE raft::raft $ - nvidia::cutlass::cutlass -) - -# ################################################################################################## -# * raft_nn ------------------------------------------------------------------ -add_library(raft_nn INTERFACE) - -if(TARGET raft_nn AND (NOT TARGET raft::nn)) - add_library(raft::nn ALIAS raft_nn) -endif() - -set_target_properties(raft_nn PROPERTIES EXPORT_NAME nn) - -if(RAFT_COMPILE_NN_LIBRARY) - add_library( - raft_nn_lib - src/nn/specializations/detail/ball_cover_lowdim_pass_one_2d.cu - src/nn/specializations/detail/ball_cover_lowdim_pass_two_2d.cu - src/nn/specializations/detail/ball_cover_lowdim_pass_one_3d.cu - src/nn/specializations/detail/ball_cover_lowdim_pass_two_3d.cu - src/nn/specializations/ball_cover_all_knn_query.cu - src/nn/specializations/ball_cover_build_index.cu - src/nn/specializations/ball_cover_knn_query.cu - src/nn/specializations/fused_l2_knn_long_float_true.cu - src/nn/specializations/fused_l2_knn_long_float_false.cu - src/nn/specializations/fused_l2_knn_int_float_true.cu - src/nn/specializations/fused_l2_knn_int_float_false.cu - src/nn/specializations/brute_force_knn_long_float_int.cu - src/nn/specializations/brute_force_knn_long_float_uint.cu - src/nn/specializations/brute_force_knn_uint32_t_float_int.cu - src/nn/specializations/brute_force_knn_uint32_t_float_uint.cu - ) - set_target_properties( - raft_nn_lib - PROPERTIES OUTPUT_NAME raft_nn - BUILD_RPATH "\$ORIGIN" - INSTALL_RPATH "\$ORIGIN" - CXX_STANDARD 17 - CXX_STANDARD_REQUIRED ON - CUDA_STANDARD 17 - CUDA_STANDARD_REQUIRED ON - POSITION_INDEPENDENT_CODE ON - INTERFACE_POSITION_INDEPENDENT_CODE ON + raft_lib PRIVATE "$<$:${RAFT_CXX_FLAGS}>" + "$<$:${RAFT_CUDA_FLAGS}>" ) + target_compile_definitions(raft_lib INTERFACE "RAFT_COMPILED") - target_link_libraries( - raft_nn_lib - PUBLIC raft::raft - PRIVATE nvidia::cutlass::cutlass - ) - target_compile_options( - raft_nn_lib PRIVATE "$<$:${RAFT_CXX_FLAGS}>" - "$<$:${RAFT_CUDA_FLAGS}>" - ) # ensure CUDA symbols aren't relocated to the middle of the debug build binaries - target_link_options(raft_nn_lib PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/fatbin.ld") + target_link_options(raft_lib PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/fatbin.ld") - target_compile_definitions(raft_nn_lib INTERFACE "RAFT_NN_COMPILED") endif() -if(TARGET raft_nn_lib AND (NOT TARGET raft::raft_nn_lib)) - add_library(raft::raft_nn_lib ALIAS raft_nn_lib) +if(TARGET raft_lib AND (NOT TARGET raft::raft_lib)) + add_library(raft::raft_lib ALIAS raft_lib) endif() -target_link_libraries( - raft_nn INTERFACE raft::raft $ nvidia::cutlass::cutlass -) +target_link_libraries(raft_compiled INTERFACE raft::raft $) # ################################################################################################## # * raft_distributed ------------------------------------------------------------------------------- @@ -547,39 +509,23 @@ install( ) install( - TARGETS raft_distance + TARGETS raft_compiled DESTINATION ${lib_dir} COMPONENT raft - EXPORT raft-distance-exports + EXPORT raft-compiled-exports ) -install( - TARGETS raft_nn - DESTINATION ${lib_dir} - COMPONENT raft - EXPORT raft-nn-exports -) - -if(TARGET raft_distance_lib) +if(TARGET raft_lib) install( - TARGETS raft_distance_lib + TARGETS raft_lib DESTINATION ${lib_dir} - COMPONENT distance - EXPORT raft-distance-lib-exports + COMPONENT compiled + EXPORT raft-compiled-lib-exports ) install( DIRECTORY include/raft_runtime DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - COMPONENT distance - ) -endif() - -if(TARGET raft_nn_lib) - install( - TARGETS raft_nn_lib - DESTINATION ${lib_dir} - COMPONENT nn - EXPORT raft-nn-lib-exports + COMPONENT compiled ) endif() @@ -614,15 +560,11 @@ install( include("${rapids-cmake-dir}/export/write_dependencies.cmake") -set(raft_components distance nn distributed) -set(raft_install_comp raft raft raft) -if(TARGET raft_distance_lib) - list(APPEND raft_components distance-lib) - list(APPEND raft_install_comp distance) -endif() -if(TARGET raft_nn_lib) - list(APPEND raft_components nn-lib) - list(APPEND raft_install_comp nn) +set(raft_components compiled distributed) +set(raft_install_comp raft raft) +if(TARGET raft_lib) + list(APPEND raft_components compiled-lib) + list(APPEND raft_install_comp compiled) endif() foreach(comp install_comp IN ZIP_LISTS raft_components raft_install_comp) @@ -658,14 +600,12 @@ RAFT contains fundamental widely-used algorithms and primitives for data science and machine learning. Optional Components: - - nn - - distance + - compiled - distributed Imported Targets: - raft::raft - - raft::nn brought in by the `nn` optional component - - raft::distance brought in by the `distance` optional component + - raft::compiled brought in by the `compiled` optional component - raft::distributed brought in by the `distributed` optional component ]=] @@ -673,27 +613,21 @@ Imported Targets: set(code_string ${nvtx_export_string}) -if(RAFT_ENABLE_thrust_DEPENDENCY) - string( - APPEND - code_string - [=[ - if(NOT TARGET raft::Thrust) - thrust_create_target(raft::Thrust FROM_OPTIONS) - endif() - ]=] - ) -endif() - string( APPEND code_string [=[ -if(distance IN_LIST raft_FIND_COMPONENTS) - enable_language(CUDA) +if(NOT TARGET raft::Thrust) + thrust_create_target(raft::Thrust FROM_OPTIONS) endif() +]=] +) -if(nn IN_LIST raft_FIND_COMPONENTS) +string( + APPEND + code_string + [=[ +if(compiled IN_LIST raft_FIND_COMPONENTS) enable_language(CUDA) endif() ]=] @@ -702,15 +636,15 @@ endif() # Use `rapids_export` for 22.04 as it will have COMPONENT support include(cmake/modules/raft_export.cmake) raft_export( - INSTALL raft COMPONENTS nn distance distributed EXPORT_SET raft-exports GLOBAL_TARGETS raft nn - distance distributed NAMESPACE raft:: DOCUMENTATION doc_string FINAL_CODE_BLOCK code_string + INSTALL raft COMPONENTS compiled distributed EXPORT_SET raft-exports GLOBAL_TARGETS raft compiled + distributed NAMESPACE raft:: DOCUMENTATION doc_string FINAL_CODE_BLOCK code_string ) # ################################################################################################## # * build export ------------------------------------------------------------- raft_export( - BUILD raft EXPORT_SET raft-exports COMPONENTS nn distance distributed GLOBAL_TARGETS raft - distance distributed nn DOCUMENTATION doc_string NAMESPACE raft:: FINAL_CODE_BLOCK code_string + BUILD raft EXPORT_SET raft-exports COMPONENTS compiled distributed GLOBAL_TARGETS raft compiled + distributed DOCUMENTATION doc_string NAMESPACE raft:: FINAL_CODE_BLOCK code_string ) # ################################################################################################## diff --git a/cpp/bench/CMakeLists.txt b/cpp/bench/CMakeLists.txt index e2324de654..8049074c09 100644 --- a/cpp/bench/CMakeLists.txt +++ b/cpp/bench/CMakeLists.txt @@ -17,7 +17,7 @@ function(ConfigureBench) - set(options OPTIONAL DIST NN) + set(options OPTIONAL LIB) set(oneValueArgs NAME) set(multiValueArgs PATH TARGETS CONFIGURATIONS) @@ -31,8 +31,7 @@ function(ConfigureBench) ${BENCH_NAME} PRIVATE raft::raft raft_internal - $<$:raft::distance> - $<$:raft::nn> + $<$:raft::compiled> benchmark::benchmark Threads::Threads $ @@ -70,7 +69,7 @@ endfunction() if(BUILD_BENCH) ConfigureBench( NAME CLUSTER_BENCH PATH bench/cluster/kmeans_balanced.cu bench/cluster/kmeans.cu bench/main.cpp - OPTIONAL DIST NN + OPTIONAL LIB ) ConfigureBench( @@ -86,7 +85,7 @@ if(BUILD_BENCH) bench/distance/kernels.cu bench/main.cpp OPTIONAL - DIST + LIB ) ConfigureBench( @@ -106,7 +105,7 @@ if(BUILD_BENCH) ConfigureBench( NAME MATRIX_BENCH PATH bench/matrix/argmin.cu bench/matrix/gather.cu bench/matrix/select_k.cu - bench/main.cpp OPTIONAL DIST + bench/main.cpp OPTIONAL LIB ) ConfigureBench( @@ -132,7 +131,6 @@ if(BUILD_BENCH) bench/neighbors/refine_uint8_t_int64_t.cu bench/main.cpp OPTIONAL - DIST - NN + LIB ) endif() diff --git a/cpp/bench/cluster/kmeans.cu b/cpp/bench/cluster/kmeans.cu index f593ec090d..af7afb8037 100644 --- a/cpp/bench/cluster/kmeans.cu +++ b/cpp/bench/cluster/kmeans.cu @@ -18,7 +18,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/bench/cluster/kmeans_balanced.cu b/cpp/bench/cluster/kmeans_balanced.cu index 8dda155a59..6bda43bdb2 100644 --- a/cpp/bench/cluster/kmeans_balanced.cu +++ b/cpp/bench/cluster/kmeans_balanced.cu @@ -18,7 +18,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/bench/distance/distance_common.cuh b/cpp/bench/distance/distance_common.cuh index 906271bf5a..9b5d67a46f 100644 --- a/cpp/bench/distance/distance_common.cuh +++ b/cpp/bench/distance/distance_common.cuh @@ -17,7 +17,7 @@ #include #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif #include diff --git a/cpp/bench/distance/fused_l2_nn.cu b/cpp/bench/distance/fused_l2_nn.cu index 7531784707..1c45572782 100644 --- a/cpp/bench/distance/fused_l2_nn.cu +++ b/cpp/bench/distance/fused_l2_nn.cu @@ -17,7 +17,7 @@ #include #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif #include diff --git a/cpp/bench/distance/kernels.cu b/cpp/bench/distance/kernels.cu index 027f93171e..4407bdcf83 100644 --- a/cpp/bench/distance/kernels.cu +++ b/cpp/bench/distance/kernels.cu @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/bench/distance/masked_nn.cu b/cpp/bench/distance/masked_nn.cu index 1fecb455c3..f9f234187d 100644 --- a/cpp/bench/distance/masked_nn.cu +++ b/cpp/bench/distance/masked_nn.cu @@ -30,7 +30,7 @@ #include #include -#ifdef RAFT_DISTANCE_COMPILED +#ifdef RAFT_COMPILED #include #endif diff --git a/cpp/bench/matrix/select_k.cu b/cpp/bench/matrix/select_k.cu index 3b6f031c77..d4873e2640 100644 --- a/cpp/bench/matrix/select_k.cu +++ b/cpp/bench/matrix/select_k.cu @@ -23,7 +23,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/bench/neighbors/knn.cuh b/cpp/bench/neighbors/knn.cuh index fe8c2c10d8..6caf355034 100644 --- a/cpp/bench/neighbors/knn.cuh +++ b/cpp/bench/neighbors/knn.cuh @@ -24,15 +24,10 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif -#if defined RAFT_NN_COMPILED -// TODO: Legacy. Remove when FAISS is removed -#include -#endif - #include #include diff --git a/cpp/bench/neighbors/refine_float_int64_t.cu b/cpp/bench/neighbors/refine_float_int64_t.cu index 40ab2bc0ca..43be330e9b 100644 --- a/cpp/bench/neighbors/refine_float_int64_t.cu +++ b/cpp/bench/neighbors/refine_float_int64_t.cu @@ -17,11 +17,8 @@ #include "refine.cuh" #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include -#endif - -#if defined RAFT_NN_COMPILED #include #endif diff --git a/cpp/bench/neighbors/refine_uint8_t_int64_t.cu b/cpp/bench/neighbors/refine_uint8_t_int64_t.cu index 92806f84a7..1d7cb8c8aa 100644 --- a/cpp/bench/neighbors/refine_uint8_t_int64_t.cu +++ b/cpp/bench/neighbors/refine_uint8_t_int64_t.cu @@ -17,13 +17,8 @@ #include "refine.cuh" #include -#if defined RAFT_DISTANCE_COMPILED -#include -#include -#endif - -#if defined RAFT_NN_COMPILED -#include +#if defined RAFT_COMPILED +#include #endif using namespace raft::neighbors; diff --git a/cpp/cmake/modules/raft_export.cmake b/cpp/cmake/modules/raft_export.cmake index bcc3578bf8..0a43f9451c 100644 --- a/cpp/cmake/modules/raft_export.cmake +++ b/cpp/cmake/modules/raft_export.cmake @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2021-2022, NVIDIA CORPORATION. +# Copyright (c) 2021-2023, NVIDIA CORPORATION. # # 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 @@ -213,7 +213,7 @@ function(raft_export type project_name) DESTINATION "${install_location}" COMPONENT raft ) - foreach(comp nn distance) + foreach(comp compiled) set(scratch_dir "${PROJECT_BINARY_DIR}/rapids-cmake/${project_name}/export/${comp}/") file(MAKE_DIRECTORY "${scratch_dir}") install( diff --git a/cpp/cmake/thirdparty/get_cutlass.cmake b/cpp/cmake/thirdparty/get_cutlass.cmake index 3e02ce064e..cb809de445 100644 --- a/cpp/cmake/thirdparty/get_cutlass.cmake +++ b/cpp/cmake/thirdparty/get_cutlass.cmake @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2021-2022, NVIDIA CORPORATION. +# Copyright (c) 2021-2023, NVIDIA CORPORATION. # # 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 @@ -61,32 +61,19 @@ function(find_and_configure_cutlass) # We generate the cutlass-config files when we built cutlass locally, so always do # `find_dependency` rapids_export_package( - BUILD NvidiaCutlass raft-distance-exports GLOBAL_TARGETS nvidia::cutlass::cutlass + BUILD NvidiaCutlass raft-exports GLOBAL_TARGETS nvidia::cutlass::cutlass ) rapids_export_package( - INSTALL NvidiaCutlass raft-distance-exports GLOBAL_TARGETS nvidia::cutlass::cutlass - ) - rapids_export_package( - BUILD NvidiaCutlass raft-nn-exports GLOBAL_TARGETS nvidia::cutlass::cutlass - ) - rapids_export_package( - INSTALL NvidiaCutlass raft-nn-exports GLOBAL_TARGETS nvidia::cutlass::cutlass + INSTALL NvidiaCutlass raft-exports GLOBAL_TARGETS nvidia::cutlass::cutlass ) # Tell cmake where it can find the generated NvidiaCutlass-config.cmake we wrote. include("${rapids-cmake-dir}/export/find_package_root.cmake") rapids_export_find_package_root( - INSTALL NvidiaCutlass [=[${CMAKE_CURRENT_LIST_DIR}/../]=] raft-distance-exports - ) - rapids_export_find_package_root( - BUILD NvidiaCutlass [=[${CMAKE_CURRENT_LIST_DIR}]=] raft-distance-exports - ) - include("${rapids-cmake-dir}/export/find_package_root.cmake") - rapids_export_find_package_root( - INSTALL NvidiaCutlass [=[${CMAKE_CURRENT_LIST_DIR}/../]=] raft-nn-exports + INSTALL NvidiaCutlass [=[${CMAKE_CURRENT_LIST_DIR}/../]=] raft-exports ) rapids_export_find_package_root( - BUILD NvidiaCutlass [=[${CMAKE_CURRENT_LIST_DIR}]=] raft-nn-exports + BUILD NvidiaCutlass [=[${CMAKE_CURRENT_LIST_DIR}]=] raft-exports ) endfunction() diff --git a/cpp/cmake/thirdparty/get_thrust.cmake b/cpp/cmake/thirdparty/get_thrust.cmake index 12360b9482..6e37aab40d 100644 --- a/cpp/cmake/thirdparty/get_thrust.cmake +++ b/cpp/cmake/thirdparty/get_thrust.cmake @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2023, NVIDIA CORPORATION. # # 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 @@ -21,6 +21,4 @@ function(find_and_configure_thrust) INSTALL_EXPORT_SET raft-exports) endfunction() -if(RAFT_ENABLE_thrust_DEPENDENCY) - find_and_configure_thrust() -endif() +find_and_configure_thrust() diff --git a/cpp/include/raft/neighbors/specializations.cuh b/cpp/include/raft/neighbors/specializations.cuh index 27105b6eab..9da5649ef8 100644 --- a/cpp/include/raft/neighbors/specializations.cuh +++ b/cpp/include/raft/neighbors/specializations.cuh @@ -16,10 +16,14 @@ #pragma once +#include +#include +#include + #include #include #include #include #include -#include \ No newline at end of file +#include diff --git a/cpp/include/raft/neighbors/specializations/knn.cuh b/cpp/include/raft/neighbors/specializations/brute_force.cuh similarity index 61% rename from cpp/include/raft/neighbors/specializations/knn.cuh rename to cpp/include/raft/neighbors/specializations/brute_force.cuh index e0b64415fe..d418d40185 100644 --- a/cpp/include/raft/neighbors/specializations/knn.cuh +++ b/cpp/include/raft/neighbors/specializations/brute_force.cuh @@ -17,31 +17,6 @@ #pragma once #include -#include - -namespace raft::spatial::knn { -#define RAFT_INST(IdxT, T, IntT) \ - extern template void brute_force_knn(raft::device_resources const& handle, \ - std::vector& input, \ - std::vector& sizes, \ - IntT D, \ - T* search_items, \ - IntT n, \ - IdxT* res_I, \ - T* res_D, \ - IntT k, \ - bool rowMajorIndex, \ - bool rowMajorQuery, \ - std::vector* translations, \ - distance::DistanceType metric, \ - float metric_arg); - -RAFT_INST(long, float, int); -RAFT_INST(long, float, unsigned int); -RAFT_INST(uint32_t, float, int); -RAFT_INST(uint32_t, float, unsigned int); -#undef RAFT_INST -}; // namespace raft::spatial::knn // also define the detail api, which is used by raft::neighbors::brute_force // (not doing the public api, since has extra template params on index_layout, matrix_index, diff --git a/cpp/include/raft/spatial/knn/specializations.cuh b/cpp/include/raft/spatial/knn/specializations.cuh index 34b7b742e9..5f0a39a61b 100644 --- a/cpp/include/raft/spatial/knn/specializations.cuh +++ b/cpp/include/raft/spatial/knn/specializations.cuh @@ -14,13 +14,8 @@ * limitations under the License. */ -#ifndef __KNN_SPECIALIZATIONS_H -#define __KNN_SPECIALIZATIONS_H - #pragma once #include +#include #include -#include - -#endif diff --git a/cpp/include/raft/spatial/knn/specializations/knn.cuh b/cpp/include/raft/spatial/knn/specializations/knn.cuh new file mode 100644 index 0000000000..e045487597 --- /dev/null +++ b/cpp/include/raft/spatial/knn/specializations/knn.cuh @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * + * 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. + */ + +#pragma once + +#include + +namespace raft::spatial::knn { +#define RAFT_INST(IdxT, T, IntT) \ + extern template void brute_force_knn(raft::device_resources const& handle, \ + std::vector& input, \ + std::vector& sizes, \ + IntT D, \ + T* search_items, \ + IntT n, \ + IdxT* res_I, \ + T* res_D, \ + IntT k, \ + bool rowMajorIndex, \ + bool rowMajorQuery, \ + std::vector* translations, \ + distance::DistanceType metric, \ + float metric_arg); + +RAFT_INST(long, float, int); +RAFT_INST(long, float, unsigned int); +RAFT_INST(uint32_t, float, int); +RAFT_INST(uint32_t, float, unsigned int); +#undef RAFT_INST +}; // namespace raft::spatial::knn diff --git a/cpp/include/raft/spectral/specializations.cuh b/cpp/include/raft/spectral/specializations.cuh index 2303b426fd..0ce5f0c653 100644 --- a/cpp/include/raft/spectral/specializations.cuh +++ b/cpp/include/raft/spectral/specializations.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,6 @@ #pragma once #include -#include +#include #endif \ No newline at end of file diff --git a/cpp/include/raft/stats/specializations.cuh b/cpp/include/raft/stats/specializations.cuh index 660eee783f..e6622469d3 100644 --- a/cpp/include/raft/stats/specializations.cuh +++ b/cpp/include/raft/stats/specializations.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,6 @@ #pragma once #include -#include +#include #endif \ No newline at end of file diff --git a/cpp/include/raft_runtime/neighbors/brute_force.hpp b/cpp/include/raft_runtime/neighbors/brute_force.hpp new file mode 100644 index 0000000000..19904f4f78 --- /dev/null +++ b/cpp/include/raft_runtime/neighbors/brute_force.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ + +#pragma once + +#include +#include + +namespace raft::runtime::neighbors::brute_force { + +#define RAFT_INST_BFKNN(IDX_T, DATA_T, MATRIX_IDX_T, INDEX_LAYOUT, SEARCH_LAYOUT) \ + void knn(raft::device_resources const& handle, \ + std::vector> index, \ + raft::device_matrix_view search, \ + raft::device_matrix_view indices, \ + raft::device_matrix_view distances, \ + int k, \ + distance::DistanceType metric = distance::DistanceType::L2Unexpanded, \ + std::optional metric_arg = std::make_optional(2.0f), \ + std::optional global_id_offset = std::nullopt); + +RAFT_INST_BFKNN(int64_t, float, uint32_t, raft::row_major, raft::row_major); + +#undef RAFT_INST_BFKNN + +} // namespace raft::runtime::neighbors::brute_force diff --git a/cpp/internal/raft_internal/matrix/select_k.cuh b/cpp/internal/raft_internal/matrix/select_k.cuh index 59cbff9dfb..ede6382c33 100644 --- a/cpp/internal/raft_internal/matrix/select_k.cuh +++ b/cpp/internal/raft_internal/matrix/select_k.cuh @@ -20,7 +20,7 @@ #include #include -#ifdef RAFT_DISTANCE_COMPILED +#ifdef RAFT_COMPILED #include #endif diff --git a/cpp/internal/raft_internal/neighbors/naive_knn.cuh b/cpp/internal/raft_internal/neighbors/naive_knn.cuh index 942c096e58..47d6f068e3 100644 --- a/cpp/internal/raft_internal/neighbors/naive_knn.cuh +++ b/cpp/internal/raft_internal/neighbors/naive_knn.cuh @@ -21,7 +21,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/src/distance/cluster/cluster_cost.cuh b/cpp/src/cluster/cluster_cost.cuh similarity index 100% rename from cpp/src/distance/cluster/cluster_cost.cuh rename to cpp/src/cluster/cluster_cost.cuh diff --git a/cpp/src/distance/cluster/cluster_cost_double.cu b/cpp/src/cluster/cluster_cost_double.cu similarity index 100% rename from cpp/src/distance/cluster/cluster_cost_double.cu rename to cpp/src/cluster/cluster_cost_double.cu diff --git a/cpp/src/distance/cluster/cluster_cost_float.cu b/cpp/src/cluster/cluster_cost_float.cu similarity index 100% rename from cpp/src/distance/cluster/cluster_cost_float.cu rename to cpp/src/cluster/cluster_cost_float.cu diff --git a/cpp/src/distance/cluster/kmeans_fit_double.cu b/cpp/src/cluster/kmeans_fit_double.cu similarity index 100% rename from cpp/src/distance/cluster/kmeans_fit_double.cu rename to cpp/src/cluster/kmeans_fit_double.cu diff --git a/cpp/src/distance/cluster/kmeans_fit_float.cu b/cpp/src/cluster/kmeans_fit_float.cu similarity index 100% rename from cpp/src/distance/cluster/kmeans_fit_float.cu rename to cpp/src/cluster/kmeans_fit_float.cu diff --git a/cpp/src/distance/cluster/kmeans_init_plus_plus_double.cu b/cpp/src/cluster/kmeans_init_plus_plus_double.cu similarity index 100% rename from cpp/src/distance/cluster/kmeans_init_plus_plus_double.cu rename to cpp/src/cluster/kmeans_init_plus_plus_double.cu diff --git a/cpp/src/distance/cluster/kmeans_init_plus_plus_float.cu b/cpp/src/cluster/kmeans_init_plus_plus_float.cu similarity index 100% rename from cpp/src/distance/cluster/kmeans_init_plus_plus_float.cu rename to cpp/src/cluster/kmeans_init_plus_plus_float.cu diff --git a/cpp/src/distance/cluster/update_centroids.cuh b/cpp/src/cluster/update_centroids.cuh similarity index 100% rename from cpp/src/distance/cluster/update_centroids.cuh rename to cpp/src/cluster/update_centroids.cuh diff --git a/cpp/src/distance/cluster/update_centroids_double.cu b/cpp/src/cluster/update_centroids_double.cu similarity index 100% rename from cpp/src/distance/cluster/update_centroids_double.cu rename to cpp/src/cluster/update_centroids_double.cu diff --git a/cpp/src/distance/cluster/update_centroids_float.cu b/cpp/src/cluster/update_centroids_float.cu similarity index 100% rename from cpp/src/distance/cluster/update_centroids_float.cu rename to cpp/src/cluster/update_centroids_float.cu diff --git a/cpp/src/distance/distance/fused_l2_min_arg.cu b/cpp/src/distance/fused_l2_min_arg.cu similarity index 100% rename from cpp/src/distance/distance/fused_l2_min_arg.cu rename to cpp/src/distance/fused_l2_min_arg.cu diff --git a/cpp/src/distance/distance/pairwise_distance.cu b/cpp/src/distance/pairwise_distance.cu similarity index 100% rename from cpp/src/distance/distance/pairwise_distance.cu rename to cpp/src/distance/pairwise_distance.cu diff --git a/cpp/src/distance/distance/specializations/detail/canberra_double_double_double_int.cu b/cpp/src/distance/specializations/detail/canberra_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/canberra_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/canberra_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/canberra_float_float_float_int.cu b/cpp/src/distance/specializations/detail/canberra_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/canberra_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/canberra_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/correlation_double_double_double_int.cu b/cpp/src/distance/specializations/detail/correlation_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/correlation_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/correlation_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/correlation_float_float_float_int.cu b/cpp/src/distance/specializations/detail/correlation_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/correlation_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/correlation_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/cosine_double_double_double_int.cu b/cpp/src/distance/specializations/detail/cosine_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/cosine_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/cosine_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/cosine_float_float_float_int.cu b/cpp/src/distance/specializations/detail/cosine_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/cosine_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/cosine_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/inner_product_double_double_double_int.cu b/cpp/src/distance/specializations/detail/inner_product_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/inner_product_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/inner_product_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/inner_product_float_float_float_int.cu b/cpp/src/distance/specializations/detail/inner_product_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/inner_product_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/inner_product_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/jensen_shannon_double_double_double_int.cu b/cpp/src/distance/specializations/detail/jensen_shannon_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/jensen_shannon_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/jensen_shannon_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/jensen_shannon_float_float_float_int.cu b/cpp/src/distance/specializations/detail/jensen_shannon_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/jensen_shannon_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/jensen_shannon_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/kernels/gram_matrix_base_double.cu b/cpp/src/distance/specializations/detail/kernels/gram_matrix_base_double.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kernels/gram_matrix_base_double.cu rename to cpp/src/distance/specializations/detail/kernels/gram_matrix_base_double.cu diff --git a/cpp/src/distance/distance/specializations/detail/kernels/gram_matrix_base_float.cu b/cpp/src/distance/specializations/detail/kernels/gram_matrix_base_float.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kernels/gram_matrix_base_float.cu rename to cpp/src/distance/specializations/detail/kernels/gram_matrix_base_float.cu diff --git a/cpp/src/distance/distance/specializations/detail/kernels/polynomial_kernel_double_int.cu b/cpp/src/distance/specializations/detail/kernels/polynomial_kernel_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kernels/polynomial_kernel_double_int.cu rename to cpp/src/distance/specializations/detail/kernels/polynomial_kernel_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/kernels/polynomial_kernel_float_int.cu b/cpp/src/distance/specializations/detail/kernels/polynomial_kernel_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kernels/polynomial_kernel_float_int.cu rename to cpp/src/distance/specializations/detail/kernels/polynomial_kernel_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/kernels/rbf_kernel_double.cu b/cpp/src/distance/specializations/detail/kernels/rbf_kernel_double.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kernels/rbf_kernel_double.cu rename to cpp/src/distance/specializations/detail/kernels/rbf_kernel_double.cu diff --git a/cpp/src/distance/distance/specializations/detail/kernels/rbf_kernel_float.cu b/cpp/src/distance/specializations/detail/kernels/rbf_kernel_float.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kernels/rbf_kernel_float.cu rename to cpp/src/distance/specializations/detail/kernels/rbf_kernel_float.cu diff --git a/cpp/src/distance/distance/specializations/detail/kernels/tanh_kernel_double.cu b/cpp/src/distance/specializations/detail/kernels/tanh_kernel_double.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kernels/tanh_kernel_double.cu rename to cpp/src/distance/specializations/detail/kernels/tanh_kernel_double.cu diff --git a/cpp/src/distance/distance/specializations/detail/kernels/tanh_kernel_float.cu b/cpp/src/distance/specializations/detail/kernels/tanh_kernel_float.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kernels/tanh_kernel_float.cu rename to cpp/src/distance/specializations/detail/kernels/tanh_kernel_float.cu diff --git a/cpp/src/distance/distance/specializations/detail/kl_divergence_double_double_double_int.cu b/cpp/src/distance/specializations/detail/kl_divergence_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kl_divergence_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/kl_divergence_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/kl_divergence_float_float_float_int.cu b/cpp/src/distance/specializations/detail/kl_divergence_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/kl_divergence_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/kl_divergence_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l1_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l1_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l1_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/l1_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l1_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l1_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l1_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/l1_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l2_expanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l2_expanded_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l2_expanded_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/l2_expanded_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l2_expanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l2_expanded_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l2_expanded_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/l2_expanded_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l_inf_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l_inf_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l_inf_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/l_inf_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/l_inf_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l_inf_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/l_inf_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/l_inf_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/russel_rao_double_double_double_int.cu b/cpp/src/distance/specializations/detail/russel_rao_double_double_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/russel_rao_double_double_double_int.cu rename to cpp/src/distance/specializations/detail/russel_rao_double_double_double_int.cu diff --git a/cpp/src/distance/distance/specializations/detail/russel_rao_float_float_float_int.cu b/cpp/src/distance/specializations/detail/russel_rao_float_float_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/detail/russel_rao_float_float_float_int.cu rename to cpp/src/distance/specializations/detail/russel_rao_float_float_float_int.cu diff --git a/cpp/src/distance/distance/specializations/fused_l2_nn_double_int.cu b/cpp/src/distance/specializations/fused_l2_nn_double_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/fused_l2_nn_double_int.cu rename to cpp/src/distance/specializations/fused_l2_nn_double_int.cu diff --git a/cpp/src/distance/distance/specializations/fused_l2_nn_double_int64.cu b/cpp/src/distance/specializations/fused_l2_nn_double_int64.cu similarity index 100% rename from cpp/src/distance/distance/specializations/fused_l2_nn_double_int64.cu rename to cpp/src/distance/specializations/fused_l2_nn_double_int64.cu diff --git a/cpp/src/distance/distance/specializations/fused_l2_nn_float_int.cu b/cpp/src/distance/specializations/fused_l2_nn_float_int.cu similarity index 100% rename from cpp/src/distance/distance/specializations/fused_l2_nn_float_int.cu rename to cpp/src/distance/specializations/fused_l2_nn_float_int.cu diff --git a/cpp/src/distance/distance/specializations/fused_l2_nn_float_int64.cu b/cpp/src/distance/specializations/fused_l2_nn_float_int64.cu similarity index 100% rename from cpp/src/distance/distance/specializations/fused_l2_nn_float_int64.cu rename to cpp/src/distance/specializations/fused_l2_nn_float_int64.cu diff --git a/cpp/src/distance/matrix/specializations/detail/select_k_float_int64_t.cu b/cpp/src/matrix/specializations/detail/select_k_float_int64_t.cu similarity index 100% rename from cpp/src/distance/matrix/specializations/detail/select_k_float_int64_t.cu rename to cpp/src/matrix/specializations/detail/select_k_float_int64_t.cu diff --git a/cpp/src/distance/matrix/specializations/detail/select_k_float_uint32_t.cu b/cpp/src/matrix/specializations/detail/select_k_float_uint32_t.cu similarity index 100% rename from cpp/src/distance/matrix/specializations/detail/select_k_float_uint32_t.cu rename to cpp/src/matrix/specializations/detail/select_k_float_uint32_t.cu diff --git a/cpp/src/distance/matrix/specializations/detail/select_k_half_int64_t.cu b/cpp/src/matrix/specializations/detail/select_k_half_int64_t.cu similarity index 100% rename from cpp/src/distance/matrix/specializations/detail/select_k_half_int64_t.cu rename to cpp/src/matrix/specializations/detail/select_k_half_int64_t.cu diff --git a/cpp/src/distance/matrix/specializations/detail/select_k_half_uint32_t.cu b/cpp/src/matrix/specializations/detail/select_k_half_uint32_t.cu similarity index 100% rename from cpp/src/distance/matrix/specializations/detail/select_k_half_uint32_t.cu rename to cpp/src/matrix/specializations/detail/select_k_half_uint32_t.cu diff --git a/cpp/src/neighbors/brute_force_knn_int64_t_float.cu b/cpp/src/neighbors/brute_force_knn_int64_t_float.cu new file mode 100644 index 0000000000..b0411a59ce --- /dev/null +++ b/cpp/src/neighbors/brute_force_knn_int64_t_float.cu @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ + +#pragma once + +#include +#include +#include + +#include + +#include + +namespace raft::runtime::neighbors::brute_force { + +#define RAFT_INST_BFKNN(IDX_T, DATA_T, MATRIX_IDX_T, INDEX_LAYOUT, SEARCH_LAYOUT) \ + void knn(raft::device_resources const& handle, \ + std::vector> index, \ + raft::device_matrix_view search, \ + raft::device_matrix_view indices, \ + raft::device_matrix_view distances, \ + distance::DistanceType metric = distance::DistanceType::L2Unexpanded, \ + std::optional metric_arg = std::make_optional(2.0f), \ + std::optional global_id_offset = std::nullopt) \ + { \ + raft::neighbors::brute_force::knn(handle, \ + index, \ + search, \ + indices, \ + distances, \ + static_cast(indices.extent(1)), \ + metric, \ + metric_arg, \ + global_id_offset); \ + } + +RAFT_INST_BFKNN(int64_t, float, uint32_t, raft::row_major, raft::row_major); + +#undef RAFT_INST_BFKNN + +} // namespace raft::runtime::neighbors::brute_force diff --git a/cpp/src/distance/neighbors/ivf_flat_build.cu b/cpp/src/neighbors/ivf_flat_build.cu similarity index 100% rename from cpp/src/distance/neighbors/ivf_flat_build.cu rename to cpp/src/neighbors/ivf_flat_build.cu diff --git a/cpp/src/distance/neighbors/ivf_flat_search.cu b/cpp/src/neighbors/ivf_flat_search.cu similarity index 100% rename from cpp/src/distance/neighbors/ivf_flat_search.cu rename to cpp/src/neighbors/ivf_flat_search.cu diff --git a/cpp/src/distance/neighbors/ivfpq_build.cu b/cpp/src/neighbors/ivfpq_build.cu similarity index 100% rename from cpp/src/distance/neighbors/ivfpq_build.cu rename to cpp/src/neighbors/ivfpq_build.cu diff --git a/cpp/src/distance/neighbors/ivfpq_deserialize.cu b/cpp/src/neighbors/ivfpq_deserialize.cu similarity index 100% rename from cpp/src/distance/neighbors/ivfpq_deserialize.cu rename to cpp/src/neighbors/ivfpq_deserialize.cu diff --git a/cpp/src/distance/neighbors/ivfpq_search_float_int64_t.cu b/cpp/src/neighbors/ivfpq_search_float_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/ivfpq_search_float_int64_t.cu rename to cpp/src/neighbors/ivfpq_search_float_int64_t.cu diff --git a/cpp/src/distance/neighbors/ivfpq_search_int8_t_int64_t.cu b/cpp/src/neighbors/ivfpq_search_int8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/ivfpq_search_int8_t_int64_t.cu rename to cpp/src/neighbors/ivfpq_search_int8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/ivfpq_search_uint8_t_int64_t.cu b/cpp/src/neighbors/ivfpq_search_uint8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/ivfpq_search_uint8_t_int64_t.cu rename to cpp/src/neighbors/ivfpq_search_uint8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/ivfpq_serialize.cu b/cpp/src/neighbors/ivfpq_serialize.cu similarity index 100% rename from cpp/src/distance/neighbors/ivfpq_serialize.cu rename to cpp/src/neighbors/ivfpq_serialize.cu diff --git a/cpp/src/distance/neighbors/refine_d_int64_t_float.cu b/cpp/src/neighbors/refine_d_int64_t_float.cu similarity index 100% rename from cpp/src/distance/neighbors/refine_d_int64_t_float.cu rename to cpp/src/neighbors/refine_d_int64_t_float.cu diff --git a/cpp/src/distance/neighbors/refine_d_int64_t_int8_t.cu b/cpp/src/neighbors/refine_d_int64_t_int8_t.cu similarity index 100% rename from cpp/src/distance/neighbors/refine_d_int64_t_int8_t.cu rename to cpp/src/neighbors/refine_d_int64_t_int8_t.cu diff --git a/cpp/src/distance/neighbors/refine_d_int64_t_uint8_t.cu b/cpp/src/neighbors/refine_d_int64_t_uint8_t.cu similarity index 100% rename from cpp/src/distance/neighbors/refine_d_int64_t_uint8_t.cu rename to cpp/src/neighbors/refine_d_int64_t_uint8_t.cu diff --git a/cpp/src/distance/neighbors/refine_h_int64_t_float.cu b/cpp/src/neighbors/refine_h_int64_t_float.cu similarity index 100% rename from cpp/src/distance/neighbors/refine_h_int64_t_float.cu rename to cpp/src/neighbors/refine_h_int64_t_float.cu diff --git a/cpp/src/distance/neighbors/refine_h_int64_t_int8_t.cu b/cpp/src/neighbors/refine_h_int64_t_int8_t.cu similarity index 100% rename from cpp/src/distance/neighbors/refine_h_int64_t_int8_t.cu rename to cpp/src/neighbors/refine_h_int64_t_int8_t.cu diff --git a/cpp/src/distance/neighbors/refine_h_int64_t_uint8_t.cu b/cpp/src/neighbors/refine_h_int64_t_uint8_t.cu similarity index 100% rename from cpp/src/distance/neighbors/refine_h_int64_t_uint8_t.cu rename to cpp/src/neighbors/refine_h_int64_t_uint8_t.cu diff --git a/cpp/src/nn/specializations/ball_cover_all_knn_query.cu b/cpp/src/neighbors/specializations/ball_cover_all_knn_query.cu similarity index 80% rename from cpp/src/nn/specializations/ball_cover_all_knn_query.cu rename to cpp/src/neighbors/specializations/ball_cover_all_knn_query.cu index d9cb836bfc..305dd6796e 100644 --- a/cpp/src/nn/specializations/ball_cover_all_knn_query.cu +++ b/cpp/src/neighbors/specializations/ball_cover_all_knn_query.cu @@ -16,14 +16,7 @@ #include #include - -// Ignore upstream specializations to avoid unnecessary recompiling -#ifdef RAFT_DISTANCE_COMPILED -#include -#endif - -// TODO: Change this to proper specializations after FAISS is removed -#include +#include #include diff --git a/cpp/src/nn/specializations/ball_cover_build_index.cu b/cpp/src/neighbors/specializations/ball_cover_build_index.cu similarity index 81% rename from cpp/src/nn/specializations/ball_cover_build_index.cu rename to cpp/src/neighbors/specializations/ball_cover_build_index.cu index 76c5a2bd5b..ec7f4bcf52 100644 --- a/cpp/src/nn/specializations/ball_cover_build_index.cu +++ b/cpp/src/neighbors/specializations/ball_cover_build_index.cu @@ -16,14 +16,7 @@ #include #include - -// Ignore upstream specializations to avoid unnecessary recompiling -#ifdef RAFT_DISTANCE_COMPILED -#include -#endif - -// TODO: Change this to proper specializations after FAISS is removed -#include +#include #include diff --git a/cpp/src/nn/specializations/ball_cover_knn_query.cu b/cpp/src/neighbors/specializations/ball_cover_knn_query.cu similarity index 80% rename from cpp/src/nn/specializations/ball_cover_knn_query.cu rename to cpp/src/neighbors/specializations/ball_cover_knn_query.cu index c01da452b2..634427200e 100644 --- a/cpp/src/nn/specializations/ball_cover_knn_query.cu +++ b/cpp/src/neighbors/specializations/ball_cover_knn_query.cu @@ -14,18 +14,10 @@ * limitations under the License. */ +#include #include #include - -// Ignore upstream specializations to avoid unnecessary recompiling -#ifdef RAFT_DISTANCE_COMPILED -#include -#endif - -// TODO: Change this to proper specializations after FAISS is removed -#include - -#include +#include namespace raft::neighbors::ball_cover { template void knn_query( diff --git a/cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_one_2d.cu b/cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_one_2d.cu similarity index 91% rename from cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_one_2d.cu rename to cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_one_2d.cu index 9a71ce4f9a..b69751a62a 100644 --- a/cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_one_2d.cu +++ b/cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_one_2d.cu @@ -15,11 +15,9 @@ */ #include +#include #include -// TODO: Change this to proper specializations after FAISS is removed -#include - namespace raft { namespace spatial { namespace knn { diff --git a/cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_one_3d.cu b/cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_one_3d.cu similarity index 91% rename from cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_one_3d.cu rename to cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_one_3d.cu index b1b3439e8f..ca44ad3165 100644 --- a/cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_one_3d.cu +++ b/cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_one_3d.cu @@ -15,11 +15,9 @@ */ #include +#include #include -// TODO: Change this to proper specializations after FAISS is removed -#include - namespace raft { namespace spatial { namespace knn { diff --git a/cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_two_2d.cu b/cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_two_2d.cu similarity index 91% rename from cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_two_2d.cu rename to cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_two_2d.cu index 9f512dcda1..ba44327653 100644 --- a/cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_two_2d.cu +++ b/cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_two_2d.cu @@ -15,11 +15,8 @@ */ #include +#include #include - -// TODO: Change this to proper specializations after FAISS is removed -#include - namespace raft { namespace spatial { namespace knn { diff --git a/cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_two_3d.cu b/cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_two_3d.cu similarity index 91% rename from cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_two_3d.cu rename to cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_two_3d.cu index 0eeb448d1e..59132c1f99 100644 --- a/cpp/src/nn/specializations/detail/ball_cover_lowdim_pass_two_3d.cu +++ b/cpp/src/neighbors/specializations/detail/ball_cover_lowdim_pass_two_3d.cu @@ -15,11 +15,9 @@ */ #include +#include #include -// TODO: Change this to proper specializations after FAISS is removed -#include - namespace raft { namespace spatial { namespace knn { diff --git a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_int.cu b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_int.cu new file mode 100644 index 0000000000..07810aa576 --- /dev/null +++ b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_int.cu @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 +#include + +namespace raft::neighbors::detail { +#define RAFT_INST(IdxT, T, IntT) \ + template void brute_force_knn_impl(raft::device_resources const& handle, \ + std::vector& input, \ + std::vector& sizes, \ + IntT D, \ + T* search_items, \ + IntT n, \ + IdxT* res_I, \ + T* res_D, \ + IntT k, \ + bool rowMajorIndex, \ + bool rowMajorQuery, \ + std::vector* translations, \ + raft::distance::DistanceType metric, \ + float metricArg); +RAFT_INST(long, float, int); +#undef RAFT_INST +} // namespace raft::neighbors::detail diff --git a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_uint.cu b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_uint.cu new file mode 100644 index 0000000000..0cb873b40a --- /dev/null +++ b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_uint.cu @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 +#include + +namespace raft::neighbors::detail { +#define RAFT_INST(IdxT, T, IntT) \ + template void brute_force_knn_impl(raft::device_resources const& handle, \ + std::vector& input, \ + std::vector& sizes, \ + IntT D, \ + T* search_items, \ + IntT n, \ + IdxT* res_I, \ + T* res_D, \ + IntT k, \ + bool rowMajorIndex, \ + bool rowMajorQuery, \ + std::vector* translations, \ + raft::distance::DistanceType metric, \ + float metricArg); +RAFT_INST(long, float, unsigned int); +#undef RAFT_INST +} // namespace raft::neighbors::detail diff --git a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_int.cu b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_int.cu new file mode 100644 index 0000000000..f8a69b896f --- /dev/null +++ b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_int.cu @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 +#include + +namespace raft::neighbors::detail { +#define RAFT_INST(IdxT, T, IntT) \ + template void brute_force_knn_impl(raft::device_resources const& handle, \ + std::vector& input, \ + std::vector& sizes, \ + IntT D, \ + T* search_items, \ + IntT n, \ + IdxT* res_I, \ + T* res_D, \ + IntT k, \ + bool rowMajorIndex, \ + bool rowMajorQuery, \ + std::vector* translations, \ + raft::distance::DistanceType metric, \ + float metricArg); +RAFT_INST(uint32_t, float, int); +#undef RAFT_INST +} // namespace raft::neighbors::detail diff --git a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_uint.cu b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_uint.cu new file mode 100644 index 0000000000..3c23d1f3e0 --- /dev/null +++ b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_uint.cu @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 +#include + +namespace raft::neighbors::detail { +#define RAFT_INST(IdxT, T, IntT) \ + template void brute_force_knn_impl(raft::device_resources const& handle, \ + std::vector& input, \ + std::vector& sizes, \ + IntT D, \ + T* search_items, \ + IntT n, \ + IdxT* res_I, \ + T* res_D, \ + IntT k, \ + bool rowMajorIndex, \ + bool rowMajorQuery, \ + std::vector* translations, \ + raft::distance::DistanceType metric, \ + float metricArg); +RAFT_INST(uint32_t, float, unsigned int); +#undef RAFT_INST +} // namespace raft::neighbors::detail diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_float_fast.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_float_fast.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_float_fast.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_float_fast.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_float_no_basediff.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_float_no_basediff.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_float_no_basediff.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_float_no_basediff.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_float_no_smem_lut.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_float_no_smem_lut.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_float_no_smem_lut.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_float_no_smem_lut.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8s_fast.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8s_fast.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8s_fast.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8s_fast.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8s_no_basediff.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8s_no_basediff.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8s_no_basediff.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8s_no_basediff.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8s_no_smem_lut.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8s_no_smem_lut.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8s_no_smem_lut.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8s_no_smem_lut.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8u_fast.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8u_fast.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8u_fast.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8u_fast.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8u_no_basediff.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8u_no_basediff.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8u_no_basediff.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8u_no_basediff.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8u_no_smem_lut.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8u_no_smem_lut.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_fp8u_no_smem_lut.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_fp8u_no_smem_lut.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_half_fast.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_half_fast.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_half_fast.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_half_fast.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_half_no_basediff.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_half_no_basediff.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_half_no_basediff.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_half_no_basediff.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_half_no_smem_lut.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_float_half_no_smem_lut.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_float_half_no_smem_lut.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_float_half_no_smem_lut.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8s_fast.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8s_fast.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8s_fast.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8s_fast.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8s_no_basediff.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8s_no_basediff.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8s_no_basediff.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8s_no_basediff.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8s_no_smem_lut.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8s_no_smem_lut.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8s_no_smem_lut.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8s_no_smem_lut.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8u_fast.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8u_fast.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8u_fast.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8u_fast.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8u_no_basediff.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8u_no_basediff.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8u_no_basediff.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8u_no_basediff.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8u_no_smem_lut.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8u_no_smem_lut.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_fp8u_no_smem_lut.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_half_fp8u_no_smem_lut.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_half_fast.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_half_half_fast.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_half_fast.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_half_half_fast.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_half_no_basediff.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_half_half_no_basediff.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_half_no_basediff.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_half_half_no_basediff.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_half_no_smem_lut.cu b/cpp/src/neighbors/specializations/detail/compute_similarity_half_half_no_smem_lut.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/compute_similarity_half_half_no_smem_lut.cu rename to cpp/src/neighbors/specializations/detail/compute_similarity_half_half_no_smem_lut.cu diff --git a/cpp/src/distance/neighbors/specializations/detail/ivfpq_compute_similarity_float_half_no_smem_lut.cu b/cpp/src/neighbors/specializations/detail/ivfpq_compute_similarity_float_half_no_smem_lut.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/detail/ivfpq_compute_similarity_float_half_no_smem_lut.cu rename to cpp/src/neighbors/specializations/detail/ivfpq_compute_similarity_float_half_no_smem_lut.cu diff --git a/cpp/src/nn/specializations/fused_l2_knn_int_float_false.cu b/cpp/src/neighbors/specializations/fused_l2_knn_int_float_false.cu similarity index 93% rename from cpp/src/nn/specializations/fused_l2_knn_int_float_false.cu rename to cpp/src/neighbors/specializations/fused_l2_knn_int_float_false.cu index 41cf409416..72fdac9526 100644 --- a/cpp/src/nn/specializations/fused_l2_knn_int_float_false.cu +++ b/cpp/src/neighbors/specializations/fused_l2_knn_int_float_false.cu @@ -15,11 +15,9 @@ */ #include +#include #include -// TODO: Change this to proper specializations after FAISS is removed -#include - namespace raft { namespace spatial { namespace knn { diff --git a/cpp/src/nn/specializations/fused_l2_knn_int_float_true.cu b/cpp/src/neighbors/specializations/fused_l2_knn_int_float_true.cu similarity index 93% rename from cpp/src/nn/specializations/fused_l2_knn_int_float_true.cu rename to cpp/src/neighbors/specializations/fused_l2_knn_int_float_true.cu index 7d183d7220..c7616462fe 100644 --- a/cpp/src/nn/specializations/fused_l2_knn_int_float_true.cu +++ b/cpp/src/neighbors/specializations/fused_l2_knn_int_float_true.cu @@ -15,11 +15,9 @@ */ #include +#include #include -// TODO: Change this to proper specializations after FAISS is removed -#include - namespace raft { namespace spatial { namespace knn { diff --git a/cpp/src/nn/specializations/fused_l2_knn_long_float_false.cu b/cpp/src/neighbors/specializations/fused_l2_knn_long_float_false.cu similarity index 93% rename from cpp/src/nn/specializations/fused_l2_knn_long_float_false.cu rename to cpp/src/neighbors/specializations/fused_l2_knn_long_float_false.cu index fa273986dc..16bf058238 100644 --- a/cpp/src/nn/specializations/fused_l2_knn_long_float_false.cu +++ b/cpp/src/neighbors/specializations/fused_l2_knn_long_float_false.cu @@ -15,11 +15,9 @@ */ #include +#include #include -// TODO: Change this to proper specializations after FAISS is removed -#include - namespace raft { namespace spatial { namespace knn { diff --git a/cpp/src/nn/specializations/fused_l2_knn_long_float_true.cu b/cpp/src/neighbors/specializations/fused_l2_knn_long_float_true.cu similarity index 93% rename from cpp/src/nn/specializations/fused_l2_knn_long_float_true.cu rename to cpp/src/neighbors/specializations/fused_l2_knn_long_float_true.cu index 5313a87786..06cf55eae3 100644 --- a/cpp/src/nn/specializations/fused_l2_knn_long_float_true.cu +++ b/cpp/src/neighbors/specializations/fused_l2_knn_long_float_true.cu @@ -15,11 +15,9 @@ */ #include +#include #include -// TODO: Change this to proper specializations after FAISS is removed -#include - namespace raft { namespace spatial { namespace knn { diff --git a/cpp/src/distance/neighbors/specializations/ivfflat_build_float_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_build_float_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfflat_build_float_int64_t.cu rename to cpp/src/neighbors/specializations/ivfflat_build_float_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfflat_build_int8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_build_int8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfflat_build_int8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfflat_build_int8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfflat_build_uint8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_build_uint8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfflat_build_uint8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfflat_build_uint8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfflat_extend_float_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_extend_float_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfflat_extend_float_int64_t.cu rename to cpp/src/neighbors/specializations/ivfflat_extend_float_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfflat_extend_int8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_extend_int8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfflat_extend_int8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfflat_extend_int8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfflat_extend_uint8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_extend_uint8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfflat_extend_uint8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfflat_extend_uint8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfflat_search_float_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_search_float_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfflat_search_float_int64_t.cu rename to cpp/src/neighbors/specializations/ivfflat_search_float_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfpq_build_float_int64_t.cu b/cpp/src/neighbors/specializations/ivfpq_build_float_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfpq_build_float_int64_t.cu rename to cpp/src/neighbors/specializations/ivfpq_build_float_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfpq_build_int8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfpq_build_int8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfpq_build_int8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfpq_build_int8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfpq_build_uint8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfpq_build_uint8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfpq_build_uint8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfpq_build_uint8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfpq_extend_float_int64_t.cu b/cpp/src/neighbors/specializations/ivfpq_extend_float_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfpq_extend_float_int64_t.cu rename to cpp/src/neighbors/specializations/ivfpq_extend_float_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfpq_extend_int8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfpq_extend_int8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfpq_extend_int8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfpq_extend_int8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfpq_extend_uint8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfpq_extend_uint8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfpq_extend_uint8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfpq_extend_uint8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfpq_search_float_int64_t.cu b/cpp/src/neighbors/specializations/ivfpq_search_float_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfpq_search_float_int64_t.cu rename to cpp/src/neighbors/specializations/ivfpq_search_float_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfpq_search_int8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfpq_search_int8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfpq_search_int8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfpq_search_int8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/ivfpq_search_uint8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfpq_search_uint8_t_int64_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/ivfpq_search_uint8_t_int64_t.cu rename to cpp/src/neighbors/specializations/ivfpq_search_uint8_t_int64_t.cu diff --git a/cpp/src/distance/neighbors/specializations/refine_d_int64_t_float.cu b/cpp/src/neighbors/specializations/refine_d_int64_t_float.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/refine_d_int64_t_float.cu rename to cpp/src/neighbors/specializations/refine_d_int64_t_float.cu diff --git a/cpp/src/distance/neighbors/specializations/refine_d_int64_t_int8_t.cu b/cpp/src/neighbors/specializations/refine_d_int64_t_int8_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/refine_d_int64_t_int8_t.cu rename to cpp/src/neighbors/specializations/refine_d_int64_t_int8_t.cu diff --git a/cpp/src/distance/neighbors/specializations/refine_d_int64_t_uint8_t.cu b/cpp/src/neighbors/specializations/refine_d_int64_t_uint8_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/refine_d_int64_t_uint8_t.cu rename to cpp/src/neighbors/specializations/refine_d_int64_t_uint8_t.cu diff --git a/cpp/src/distance/neighbors/specializations/refine_h_int64_t_float.cu b/cpp/src/neighbors/specializations/refine_h_int64_t_float.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/refine_h_int64_t_float.cu rename to cpp/src/neighbors/specializations/refine_h_int64_t_float.cu diff --git a/cpp/src/distance/neighbors/specializations/refine_h_int64_t_int8_t.cu b/cpp/src/neighbors/specializations/refine_h_int64_t_int8_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/refine_h_int64_t_int8_t.cu rename to cpp/src/neighbors/specializations/refine_h_int64_t_int8_t.cu diff --git a/cpp/src/distance/neighbors/specializations/refine_h_int64_t_uint8_t.cu b/cpp/src/neighbors/specializations/refine_h_int64_t_uint8_t.cu similarity index 100% rename from cpp/src/distance/neighbors/specializations/refine_h_int64_t_uint8_t.cu rename to cpp/src/neighbors/specializations/refine_h_int64_t_uint8_t.cu diff --git a/cpp/src/distance/random/common.cuh b/cpp/src/random/common.cuh similarity index 100% rename from cpp/src/distance/random/common.cuh rename to cpp/src/random/common.cuh diff --git a/cpp/src/distance/random/rmat_rectangular_generator_int64_double.cu b/cpp/src/random/rmat_rectangular_generator_int64_double.cu similarity index 93% rename from cpp/src/distance/random/rmat_rectangular_generator_int64_double.cu rename to cpp/src/random/rmat_rectangular_generator_int64_double.cu index 1b8fb8bd6d..657aa0533c 100644 --- a/cpp/src/distance/random/rmat_rectangular_generator_int64_double.cu +++ b/cpp/src/random/rmat_rectangular_generator_int64_double.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/src/distance/random/rmat_rectangular_generator_int64_float.cu b/cpp/src/random/rmat_rectangular_generator_int64_float.cu similarity index 93% rename from cpp/src/distance/random/rmat_rectangular_generator_int64_float.cu rename to cpp/src/random/rmat_rectangular_generator_int64_float.cu index 249e8c2ffb..9cd748da89 100644 --- a/cpp/src/distance/random/rmat_rectangular_generator_int64_float.cu +++ b/cpp/src/random/rmat_rectangular_generator_int64_float.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/src/distance/random/rmat_rectangular_generator_int_double.cu b/cpp/src/random/rmat_rectangular_generator_int_double.cu similarity index 93% rename from cpp/src/distance/random/rmat_rectangular_generator_int_double.cu rename to cpp/src/random/rmat_rectangular_generator_int_double.cu index 3333b87983..1f10dbc03c 100644 --- a/cpp/src/distance/random/rmat_rectangular_generator_int_double.cu +++ b/cpp/src/random/rmat_rectangular_generator_int_double.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/src/distance/random/rmat_rectangular_generator_int_float.cu b/cpp/src/random/rmat_rectangular_generator_int_float.cu similarity index 93% rename from cpp/src/distance/random/rmat_rectangular_generator_int_float.cu rename to cpp/src/random/rmat_rectangular_generator_int_float.cu index db8d024c04..fecc134326 100644 --- a/cpp/src/distance/random/rmat_rectangular_generator_int_float.cu +++ b/cpp/src/random/rmat_rectangular_generator_int_float.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt index 696a271c65..a778b0d195 100644 --- a/cpp/test/CMakeLists.txt +++ b/cpp/test/CMakeLists.txt @@ -17,7 +17,7 @@ function(ConfigureTest) - set(options OPTIONAL DIST NN) + set(options OPTIONAL LIB) set(oneValueArgs NAME) set(multiValueArgs PATH TARGETS CONFIGURATIONS) @@ -33,8 +33,7 @@ function(ConfigureTest) ${TEST_NAME} PRIVATE raft::raft raft_internal - $<$:raft::distance> - $<$:raft::nn> + $<$:raft::compiled> GTest::gtest GTest::gtest_main Threads::Threads @@ -87,8 +86,7 @@ if(BUILD_TESTS) test/cluster/linkage.cu test/cluster/kmeans_find_k.cu OPTIONAL - DIST - NN + LIB ) ConfigureTest( @@ -140,7 +138,7 @@ if(BUILD_TESTS) test/distance/fused_l2_nn.cu test/distance/gram.cu OPTIONAL - DIST + LIB ) ConfigureTest(NAME LABEL_TEST PATH test/label/label.cu test/label/merge_labels.cu) @@ -201,7 +199,7 @@ if(BUILD_TESTS) test/matrix/triangular.cu test/sparse/spectral_matrix.cu OPTIONAL - DIST + LIB ) ConfigureTest( @@ -221,7 +219,7 @@ if(BUILD_TESTS) ConfigureTest( NAME SOLVERS_TEST PATH test/cluster/cluster_solvers_deprecated.cu test/linalg/eigen_solvers.cu - test/lap/lap.cu test/sparse/mst.cu OPTIONAL DIST + test/lap/lap.cu test/sparse/mst.cu OPTIONAL LIB ) ConfigureTest( @@ -245,13 +243,12 @@ if(BUILD_TESTS) ) ConfigureTest( - NAME SPARSE_DIST_TEST PATH test/sparse/dist_coo_spmv.cu test/sparse/distance.cu OPTIONAL DIST - NN + NAME SPARSE_DIST_TEST PATH test/sparse/dist_coo_spmv.cu test/sparse/distance.cu OPTIONAL LIB ) ConfigureTest( NAME SPARSE_NEIGHBORS_TEST PATH test/sparse/neighbors/connect_components.cu - test/sparse/neighbors/brute_force.cu test/sparse/neighbors/knn_graph.cu OPTIONAL DIST NN + test/sparse/neighbors/brute_force.cu test/sparse/neighbors/knn_graph.cu OPTIONAL LIB ) ConfigureTest( @@ -275,8 +272,7 @@ if(BUILD_TESTS) test/neighbors/refine.cu test/neighbors/selection.cu OPTIONAL - DIST - NN + LIB ) ConfigureTest( @@ -309,8 +305,7 @@ if(BUILD_TESTS) test/stats/weighted_mean.cu test/stats/v_measure.cu OPTIONAL - DIST - NN + LIB ) ConfigureTest( diff --git a/cpp/test/cluster/cluster_solvers.cu b/cpp/test/cluster/cluster_solvers.cu index 5121cdf139..f26c598a2b 100644 --- a/cpp/test/cluster/cluster_solvers.cu +++ b/cpp/test/cluster/cluster_solvers.cu @@ -19,7 +19,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED && defined RAFT_NN_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/cluster/kmeans.cu b/cpp/test/cluster/kmeans.cu index 3e2153dcde..cfec84256b 100644 --- a/cpp/test/cluster/kmeans.cu +++ b/cpp/test/cluster/kmeans.cu @@ -29,7 +29,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/cluster/kmeans_balanced.cu b/cpp/test/cluster/kmeans_balanced.cu index ae06572061..220eba4186 100644 --- a/cpp/test/cluster/kmeans_balanced.cu +++ b/cpp/test/cluster/kmeans_balanced.cu @@ -30,7 +30,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/cluster/kmeans_find_k.cu b/cpp/test/cluster/kmeans_find_k.cu index e80cbaa93b..a865651f56 100644 --- a/cpp/test/cluster/kmeans_find_k.cu +++ b/cpp/test/cluster/kmeans_find_k.cu @@ -25,7 +25,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/cluster/linkage.cu b/cpp/test/cluster/linkage.cu index 20f2952e7d..4946d52f26 100644 --- a/cpp/test/cluster/linkage.cu +++ b/cpp/test/cluster/linkage.cu @@ -20,8 +20,8 @@ #include #include -#if defined RAFT_NN_COMPILED -#include +#if defined RAFT_COMPILED +#include #endif #include diff --git a/cpp/test/distance/distance_base.cuh b/cpp/test/distance/distance_base.cuh index 5fcaf07539..0e084f2ad8 100644 --- a/cpp/test/distance/distance_base.cuh +++ b/cpp/test/distance/distance_base.cuh @@ -22,7 +22,7 @@ #include #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif #include diff --git a/cpp/test/distance/fused_l2_nn.cu b/cpp/test/distance/fused_l2_nn.cu index af67214193..4a74d7f16a 100644 --- a/cpp/test/distance/fused_l2_nn.cu +++ b/cpp/test/distance/fused_l2_nn.cu @@ -24,7 +24,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/distance/gram.cu b/cpp/test/distance/gram.cu index a2f0e2385c..f99d02dc7f 100644 --- a/cpp/test/distance/gram.cu +++ b/cpp/test/distance/gram.cu @@ -14,7 +14,7 @@ * limitations under the License. */ -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/distance/masked_nn.cu b/cpp/test/distance/masked_nn.cu index 6f7d8bf44a..d01911206b 100644 --- a/cpp/test/distance/masked_nn.cu +++ b/cpp/test/distance/masked_nn.cu @@ -28,7 +28,7 @@ #include #include -#ifdef RAFT_DISTANCE_COMPILED +#ifdef RAFT_COMPILED #include #endif diff --git a/cpp/test/matrix/select_k.cu b/cpp/test/matrix/select_k.cu index a9fc4c8f40..392464eb27 100644 --- a/cpp/test/matrix/select_k.cu +++ b/cpp/test/matrix/select_k.cu @@ -18,7 +18,7 @@ #include -#ifdef RAFT_DISTANCE_COMPILED +#ifdef RAFT_COMPILED #include #endif diff --git a/cpp/test/neighbors/ann_ivf_flat.cuh b/cpp/test/neighbors/ann_ivf_flat.cuh index 486ff61724..fe6f9163a0 100644 --- a/cpp/test/neighbors/ann_ivf_flat.cuh +++ b/cpp/test/neighbors/ann_ivf_flat.cuh @@ -36,7 +36,7 @@ #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu b/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu index cee0d03c99..e430af89df 100644 --- a/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu +++ b/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu @@ -18,7 +18,7 @@ #include "../ann_ivf_flat.cuh" -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu b/cpp/test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu index 95876f9165..e4e7a207fb 100644 --- a/cpp/test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu +++ b/cpp/test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu @@ -18,7 +18,7 @@ #include "../ann_ivf_flat.cuh" -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu b/cpp/test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu index ebee20c2b6..ef7980401a 100644 --- a/cpp/test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu +++ b/cpp/test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu @@ -18,7 +18,7 @@ #include "../ann_ivf_flat.cuh" -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/neighbors/ann_ivf_pq.cuh b/cpp/test/neighbors/ann_ivf_pq.cuh index c368192b03..c331081314 100644 --- a/cpp/test/neighbors/ann_ivf_pq.cuh +++ b/cpp/test/neighbors/ann_ivf_pq.cuh @@ -24,7 +24,7 @@ #include #include #include -#ifdef RAFT_DISTANCE_COMPILED +#ifdef RAFT_COMPILED #include #else #pragma message("NN specializations are not enabled; expect very long building times.") diff --git a/cpp/test/neighbors/ball_cover.cu b/cpp/test/neighbors/ball_cover.cu index 6dcae8e34d..9b51d585de 100644 --- a/cpp/test/neighbors/ball_cover.cu +++ b/cpp/test/neighbors/ball_cover.cu @@ -22,8 +22,9 @@ #include #include #include -#if defined RAFT_NN_COMPILED -#include + +#ifdef RAFT_COMPILED +#include #endif #include diff --git a/cpp/test/neighbors/epsilon_neighborhood.cu b/cpp/test/neighbors/epsilon_neighborhood.cu index 977e8f3ce8..769cb7ec2d 100644 --- a/cpp/test/neighbors/epsilon_neighborhood.cu +++ b/cpp/test/neighbors/epsilon_neighborhood.cu @@ -23,7 +23,7 @@ #include #include -#ifdef RAFT_DISTANCE_COMPILED +#ifdef RAFT_COMPILED #include #endif diff --git a/cpp/test/neighbors/fused_l2_knn.cu b/cpp/test/neighbors/fused_l2_knn.cu index a5fead8093..ab05b41cc9 100644 --- a/cpp/test/neighbors/fused_l2_knn.cu +++ b/cpp/test/neighbors/fused_l2_knn.cu @@ -23,12 +23,8 @@ #include #include -#if defined RAFT_NN_COMPILED -#include -#endif - -#ifdef RAFT_DISTANCE_COMPILED -#include +#ifdef RAFT_COMPILED +#include #endif #include diff --git a/cpp/test/neighbors/knn.cu b/cpp/test/neighbors/knn.cu index 7976725c65..4bb977432c 100644 --- a/cpp/test/neighbors/knn.cu +++ b/cpp/test/neighbors/knn.cu @@ -21,14 +21,10 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#ifdef RAFT_COMPILED #include #endif -#if defined RAFT_NN_COMPILED -#include -#endif - #include #include diff --git a/cpp/test/neighbors/refine.cu b/cpp/test/neighbors/refine.cu index 8866c404a9..dd3491673e 100644 --- a/cpp/test/neighbors/refine.cu +++ b/cpp/test/neighbors/refine.cu @@ -31,7 +31,7 @@ #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/neighbors/selection.cu b/cpp/test/neighbors/selection.cu index 25939f65c3..9f13de357c 100644 --- a/cpp/test/neighbors/selection.cu +++ b/cpp/test/neighbors/selection.cu @@ -24,7 +24,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/neighbors/tiled_knn.cu b/cpp/test/neighbors/tiled_knn.cu index 4784f915f3..ccc3a64edd 100644 --- a/cpp/test/neighbors/tiled_knn.cu +++ b/cpp/test/neighbors/tiled_knn.cu @@ -25,8 +25,7 @@ #include #include -#if defined RAFT_NN_COMPILED -#include +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/sparse/neighbors/knn_graph.cu b/cpp/test/sparse/neighbors/knn_graph.cu index 3b025fc082..8873445c37 100644 --- a/cpp/test/sparse/neighbors/knn_graph.cu +++ b/cpp/test/sparse/neighbors/knn_graph.cu @@ -22,8 +22,8 @@ #include #include -#if defined RAFT_NN_COMPILED -#include +#if defined RAFT_COMPILED +#include #endif #include diff --git a/cpp/test/stats/silhouette_score.cu b/cpp/test/stats/silhouette_score.cu index 80e60a4884..40b7e59d81 100644 --- a/cpp/test/stats/silhouette_score.cu +++ b/cpp/test/stats/silhouette_score.cu @@ -20,7 +20,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED && defined RAFT_NN_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/cpp/test/stats/trustworthiness.cu b/cpp/test/stats/trustworthiness.cu index a2f72516eb..2fde6b29c1 100644 --- a/cpp/test/stats/trustworthiness.cu +++ b/cpp/test/stats/trustworthiness.cu @@ -20,7 +20,7 @@ #include #include -#if defined RAFT_DISTANCE_COMPILED && defined RAFT_NN_COMPILED +#if defined RAFT_COMPILED #include #endif diff --git a/docs/source/build.md b/docs/source/build.md index 5ba1e75fad..bbb454736a 100644 --- a/docs/source/build.md +++ b/docs/source/build.md @@ -4,8 +4,7 @@ The easiest way to install RAFT is through conda and several packages are provided. - `libraft-headers` RAFT headers -- `libraft-nn` (optional) contains shared libraries for the nearest neighbors primitives. -- `libraft-distance` (optional) contains shared libraries for distance primitives. +- `libraft` (optional) shared library containing pre-compiled template specializations and runtime API. - `pylibraft` (optional) Python wrappers around RAFT algorithms and primitives. - `raft-dask` (optional) enables deployment of multi-node multi-GPU algorithms that use RAFT `raft::comms` in Dask clusters. @@ -14,7 +13,7 @@ Use the following command to install all of the RAFT packages with conda (replac mamba install -c rapidsai -c conda-forge -c nvidia raft-dask pylibraft ``` -You can also install the `libraft-*` conda packages individually using the `mamba` command above. +You can also install the conda packages individually using the `mamba` command above. After installing RAFT, `find_package(raft COMPONENTS nn distance)` can be used in your CUDA/C++ cmake build to compile and/or link against needed dependencies in your raft target. `COMPONENTS` are optional and will depend on the packages installed. @@ -42,11 +41,10 @@ In addition to the libraries included with cudatoolkit 11.0+, there are some oth #### Required - [RMM](https://github.com/rapidsai/rmm) corresponding to RAFT version. - [Thrust](https://github.com/NVIDIA/thrust) v1.17 / [CUB](https://github.com/NVIDIA/cub) - -#### Optional - [cuCollections](https://github.com/NVIDIA/cuCollections) - Used in `raft::sparse::distance` API. -- [Libcu++](https://github.com/NVIDIA/libcudacxx) v1.7.0 - Used by cuCollections - [CUTLASS](https://github.com/NVIDIA/cutlass) v2.9.1 - Used in `raft::distance` API. + +#### Optional - [NCCL](https://github.com/NVIDIA/nccl) - Used in `raft::comms` API and needed to build `raft-dask`. - [UCX](https://github.com/openucx/ucx) - Used in `raft::comms` API and needed to build `raft-dask`. - [Googletest](https://github.com/google/googletest) - Needed to build tests @@ -79,19 +77,14 @@ Once installed, `libraft` headers (and dependencies which were downloaded and in ### C++ Shared Libraries (optional) -For larger projects which make heavy use of the pairwise distances or nearest neighbors APIs, shared libraries can be built to speed up compile times. These shared libraries can also significantly improve re-compile times both while developing RAFT and developing against the APIs. Build all of the available shared libraries by passing `--compile-libs` flag to `build.sh`: +A shared library can be built for speeding up compile times. The shared library also contains a runtime API that allows you to invoke RAFT APIs directly from C++ source files (without `nvcc`). The shared library can also significantly improve re-compile times both while developing RAFT and using its APIs to develop applications. Pass the `--compile-lib` flag to `build.sh` to build the library: ```bash -./build.sh libraft --compile-libs +./build.sh libraft --compile-lib ``` -Individual shared libraries have their own flags and multiple can be used (though currently only the `nn` and `distance` packages contain shared libraries): -```bash -./build.sh libraft --compile-nn --compile-dist -``` +In above example the shared library is installed by default into `$INSTALL_PREFIX/lib`. To disable this, pass `-n` flag. -In above example the shared libraries are installed by default into `$INSTALL_PREFIX/lib`. To disable this, pass `-n` flag. - -Once installed, the shared libraries, headers (and any dependencies downloaded and installed via `rapids-cmake`) can be uninstalled using `build.sh`: +Once installed, the shared library, headers (and any dependencies downloaded and installed via `rapids-cmake`) can be uninstalled using `build.sh`: ```bash ./build.sh libraft --uninstall ``` @@ -152,7 +145,7 @@ Use `CMAKE_INSTALL_PREFIX` to install RAFT into a specific location. The snippet cd cpp mkdir build cd build -cmake -D BUILD_TESTS=ON -DRAFT_COMPILE_LIBRARIES=ON -DRAFT_ENABLE_NN_DEPENDENCIES=ON -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX ../ +cmake -D BUILD_TESTS=ON -DRAFT_COMPILE_LIBRARY=ON -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX ../ make -j install ``` @@ -187,9 +180,9 @@ The Python APIs can be built and installed using the `build.sh` script: ```bash # to build pylibraft -./build.sh libraft pylibraft --compile-libs +./build.sh libraft pylibraft --compile-lib # to build raft-dask -./build.sh libraft raft-dask --compile-libs +./build.sh libraft pylibraft raft-dask --compile-lib ``` `setup.py` can also be used to build the Python APIs manually: @@ -225,7 +218,7 @@ The documentation requires that the C++ headers and python packages have been bu The following will build the docs along with the C++ and Python packages: ``` -./build.sh libraft pylibraft raft-dask docs --compile-libs +./build.sh libraft pylibraft raft-dask docs --compile-lib ``` @@ -274,11 +267,11 @@ If RAFT has already been installed, such as by using the `build.sh` script, use ### Using C++ pre-compiled shared libraries -Use `find_package(raft COMPONENTS nn distance)` to enable the shared libraries and transitively pass dependencies through separate targets for each component. In this example, the `raft::distance` and `raft::nn` targets will be available for configuring linking paths in addition to `raft::raft`. These targets will also pass through any transitive dependencies (such as CUTLASS for the `distance` package). +Use `find_package(raft COMPONENTS compiled distributed)` to enable the shared library and transitively pass dependencies through separate targets for each component. In this example, the `raft::compiled` and `raft::distributed` targets will be available for configuring linking paths in addition to `raft::raft`. These targets will also pass through any transitive dependencies (such as NCCL for the `distributed` component). The pre-compiled libraries contain template specializations for commonly used types, such as single- and double-precision floating-point. In order to use the symbols in the pre-compiled libraries, the compiler needs to be told not to instantiate templates that are already contained in the shared libraries. By convention, these header files are named `specializations.cuh` and located in the base directory for the packages that contain specializations. -The following example tells the compiler to ignore the pre-compiled templates for the `libraft-distance` API so any symbols already compiled into pre-compiled shared library will be used instead: +The following example tells the compiler to ignore the pre-compiled templates for the `raft::distance` API so any symbols already compiled into the `libraft` shared library will be used instead: ```c++ #include #include @@ -299,10 +292,7 @@ set(RAFT_FORK "rapidsai") set(RAFT_PINNED_TAG "branch-${RAFT_VERSION}") function(find_and_configure_raft) - set(oneValueArgs VERSION FORK PINNED_TAG - COMPILE_LIBRARIES CLONE_ON_PIN - USE_NN_LIBRARY USE_DISTANCE_LIBRARY - ENABLE_thrust_DEPENDENCY) + set(oneValueArgs VERSION FORK PINNED_TAG COMPILE_LIBRARY CLONE_ON_PIN) cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) @@ -315,18 +305,6 @@ function(find_and_configure_raft) set(CMAKE_IGNORE_PATH "${CMAKE_INSTALL_PREFIX}/include/raft;${CMAKE_IGNORE_PATH}) endif() - #----------------------------------------------------- - # Add components - #----------------------------------------------------- - - if(PKG_USE_NN_LIBRARY) - string(APPEND RAFT_COMPONENTS " nn") - endif() - - if(PKG_USE_DISTANCE_LIBRARY) - string(APPEND RAFT_COMPONENTS " distance") - endif() - #----------------------------------------------------- # Invoke CPM find_package() #----------------------------------------------------- @@ -339,12 +317,11 @@ function(find_and_configure_raft) GIT_REPOSITORY https://github.com/${PKG_FORK}/raft.git GIT_TAG ${PKG_PINNED_TAG} SOURCE_SUBDIR cpp - FIND_PACKAGE_ARGUMENTS "COMPONENTS ${RAFT_COMPONENTS}" + FIND_PACKAGE_ARGUMENTS "COMPONENTS compiled distributed" OPTIONS "BUILD_TESTS OFF" "BUILD_BENCH OFF" - "RAFT_COMPILE_LIBRARIES ${PKG_COMPILE_LIBRARIES}" - "RAFT_ENABLE_thrust_DEPENDENCY ${PKG_ENABLE_thrust_DEPENDENCY}" + "RAFT_COMPILE_LIBRARY ${PKG_COMPILE_LIBRARY}" ) endfunction() @@ -360,16 +337,10 @@ find_and_configure_raft(VERSION ${RAFT_VERSION}.00 # force local raft clone in build directory # even if it's already installed. CLONE_ON_PIN ON - - COMPILE_LIBRARIES NO - USE_NN_LIBRARY NO - USE_DISTANCE_LIBRARY NO - ENABLE_thrust_DEPENDENCY YES + COMPILE_LIBRARY NO ) ``` -If using the nearest neighbors APIs without the shared libraries, set `ENABLE_NN_DEPENDENCIES=ON` and keep `USE_NN_LIBRARY=OFF` - ## Uninstall Once built and installed, RAFT can be safely uninstalled using `build.sh` by specifying any or all of the installed components. Please note that since `pylibraft` depends on `libraft`, uninstalling `pylibraft` will also uninstall `libraft`: diff --git a/python/pylibraft/CMakeLists.txt b/python/pylibraft/CMakeLists.txt index b12d0a63ea..a87b798eae 100644 --- a/python/pylibraft/CMakeLists.txt +++ b/python/pylibraft/CMakeLists.txt @@ -36,11 +36,11 @@ option(RAFT_BUILD_WHEELS "Whether this build is generating a Python wheel." OFF) # If the user requested it we attempt to find RAFT. if(FIND_RAFT_CPP) - find_package(raft ${pylibraft_version} REQUIRED COMPONENTS distance) - if(NOT TARGET raft::raft_distance_lib) + find_package(raft ${pylibraft_version} REQUIRED COMPONENTS compiled) + if(NOT TARGET raft::raft_lib) message( FATAL_ERROR - "Building against a preexisting libraft library requires the distance components of that library to have been built!" + "Building against a preexisting libraft library requires the compiled libraft to have been built!" ) endif() @@ -62,8 +62,7 @@ if(NOT raft_FOUND) set(BUILD_TESTS OFF) set(BUILD_BENCH OFF) - set(RAFT_COMPILE_LIBRARIES OFF) - set(RAFT_COMPILE_DIST_LIBRARY ON) + set(RAFT_COMPILE_LIBRARY ON) set(_exclude_from_all "") if(RAFT_BUILD_WHEELS) @@ -75,11 +74,11 @@ if(NOT raft_FOUND) add_subdirectory(../../cpp raft-cpp ${_exclude_from_all}) - # When building the C++ libraries from source we must copy libraft_distance.so alongside the + # When building the C++ libraries from source we must copy libraft.so alongside the # pairwise_distance and random Cython libraries TODO: when we have a single 'compiled' raft # library, we shouldn't need this set(cython_lib_dir pylibraft) - install(TARGETS raft_distance_lib DESTINATION ${cython_lib_dir}) + install(TARGETS raft_lib DESTINATION ${cython_lib_dir}) endif() rapids_cython_init() diff --git a/python/pylibraft/pylibraft/cluster/CMakeLists.txt b/python/pylibraft/pylibraft/cluster/CMakeLists.txt index ba77403a5d..7d6e05d918 100644 --- a/python/pylibraft/pylibraft/cluster/CMakeLists.txt +++ b/python/pylibraft/pylibraft/cluster/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2023, NVIDIA CORPORATION. # # 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 @@ -14,7 +14,7 @@ # Set the list of Cython files to build set(cython_sources kmeans.pyx) -set(linked_libraries raft::distance) +set(linked_libraries raft::compiled) # Build all of the Cython targets rapids_cython_create_modules( diff --git a/python/pylibraft/pylibraft/distance/CMakeLists.txt b/python/pylibraft/pylibraft/distance/CMakeLists.txt index cae00007d6..14f0cc441a 100644 --- a/python/pylibraft/pylibraft/distance/CMakeLists.txt +++ b/python/pylibraft/pylibraft/distance/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2023, NVIDIA CORPORATION. # # 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 @@ -14,7 +14,7 @@ # Set the list of Cython files to build set(cython_sources pairwise_distance.pyx fused_l2_nn.pyx) -set(linked_libraries raft::raft raft::distance) +set(linked_libraries raft::raft raft::compiled) # Build all of the Cython targets rapids_cython_create_modules( diff --git a/python/pylibraft/pylibraft/neighbors/CMakeLists.txt b/python/pylibraft/pylibraft/neighbors/CMakeLists.txt index 572ea47f4e..98f0d7f67a 100644 --- a/python/pylibraft/pylibraft/neighbors/CMakeLists.txt +++ b/python/pylibraft/pylibraft/neighbors/CMakeLists.txt @@ -14,7 +14,7 @@ # Set the list of Cython files to build set(cython_sources common.pyx refine.pyx) -set(linked_libraries raft::raft raft::distance) +set(linked_libraries raft::raft raft::compiled) # Build all of the Cython targets rapids_cython_create_modules( diff --git a/python/pylibraft/pylibraft/neighbors/ivf_flat/CMakeLists.txt b/python/pylibraft/pylibraft/neighbors/ivf_flat/CMakeLists.txt index f183e17157..8f395faec9 100644 --- a/python/pylibraft/pylibraft/neighbors/ivf_flat/CMakeLists.txt +++ b/python/pylibraft/pylibraft/neighbors/ivf_flat/CMakeLists.txt @@ -14,7 +14,7 @@ # Set the list of Cython files to build set(cython_sources ivf_flat.pyx) -set(linked_libraries raft::raft raft::distance) +set(linked_libraries raft::raft raft::compiled) # Build all of the Cython targets rapids_cython_create_modules( diff --git a/python/pylibraft/pylibraft/neighbors/ivf_pq/CMakeLists.txt b/python/pylibraft/pylibraft/neighbors/ivf_pq/CMakeLists.txt index cfce37b560..e3d721a6ea 100644 --- a/python/pylibraft/pylibraft/neighbors/ivf_pq/CMakeLists.txt +++ b/python/pylibraft/pylibraft/neighbors/ivf_pq/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2023, NVIDIA CORPORATION. # # 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 @@ -14,7 +14,7 @@ # Set the list of Cython files to build set(cython_sources ivf_pq.pyx) -set(linked_libraries raft::raft raft::distance) +set(linked_libraries raft::raft raft::compiled) # Build all of the Cython targets rapids_cython_create_modules( diff --git a/python/pylibraft/pylibraft/random/CMakeLists.txt b/python/pylibraft/pylibraft/random/CMakeLists.txt index 49ca8627cc..fcc5ee6311 100644 --- a/python/pylibraft/pylibraft/random/CMakeLists.txt +++ b/python/pylibraft/pylibraft/random/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2023, NVIDIA CORPORATION. # # 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 @@ -17,7 +17,7 @@ set(cython_sources rmat_rectangular_generator.pyx) # TODO: should finally be replaced with 'compiled' library to be more generic, when that is # available -set(linked_libraries raft::raft raft::distance) +set(linked_libraries raft::raft raft::compiled) # Build all of the Cython targets rapids_cython_create_modules( From 56ac43ad93a319a61073dce1b3b937f6f13ade63 Mon Sep 17 00:00:00 2001 From: Allard Hendriksen Date: Mon, 20 Mar 2023 20:36:06 +0100 Subject: [PATCH 05/25] Fix ivf flat specialization header IdxT from uint64_t -> int64_t (#1358) The ivf_flat specialization header declarations used a wrong index type. The specializations for ivf flat are defined for int64_t. The raft_runtime interface also uses the int64_t instances. The ivf_flat specialization header, however, declared an interface using uint64_t. This is fixed with this PR. Should also reduce compile times for `src/distance/neighbors/ivf_flat_search.cu.o` Authors: - Allard Hendriksen (https://github.com/ahendriksen) Approvers: - Corey J. Nolet (https://github.com/cjnolet) - Divye Gala (https://github.com/divyegala) URL: https://github.com/rapidsai/raft/pull/1358 --- .../neighbors/specializations/ivf_flat.cuh | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/cpp/include/raft/neighbors/specializations/ivf_flat.cuh b/cpp/include/raft/neighbors/specializations/ivf_flat.cuh index 02e1cbebb0..013c7359e5 100644 --- a/cpp/include/raft/neighbors/specializations/ivf_flat.cuh +++ b/cpp/include/raft/neighbors/specializations/ivf_flat.cuh @@ -20,35 +20,35 @@ namespace raft::neighbors::ivf_flat { -#define RAFT_INST(T, IdxT) \ - extern template auto build(raft::device_resources const& handle, \ - const index_params& params, \ - raft::device_matrix_view dataset) \ - ->index; \ - \ - extern template auto extend( \ - raft::device_resources const& handle, \ - raft::device_matrix_view new_vectors, \ - std::optional> new_indices, \ - const index& orig_index) \ - ->index; \ - \ - extern template void extend( \ - raft::device_resources const& handle, \ - raft::device_matrix_view new_vectors, \ - std::optional> new_indices, \ - raft::neighbors::ivf_flat::index* idx); \ - \ - extern template void search(raft::device_resources const&, \ - raft::neighbors::ivf_flat::search_params const&, \ - const raft::neighbors::ivf_flat::index&, \ - raft::device_matrix_view, \ - raft::device_matrix_view, \ +#define RAFT_INST(T, IdxT) \ + extern template auto build(raft::device_resources const& handle, \ + const index_params& params, \ + raft::device_matrix_view dataset) \ + ->index; \ + \ + extern template auto extend( \ + raft::device_resources const& handle, \ + raft::device_matrix_view new_vectors, \ + std::optional> new_indices, \ + const index& orig_index) \ + ->index; \ + \ + extern template void extend( \ + raft::device_resources const& handle, \ + raft::device_matrix_view new_vectors, \ + std::optional> new_indices, \ + raft::neighbors::ivf_flat::index* idx); \ + \ + extern template void search(raft::device_resources const&, \ + raft::neighbors::ivf_flat::search_params const&, \ + const raft::neighbors::ivf_flat::index&, \ + raft::device_matrix_view, \ + raft::device_matrix_view, \ raft::device_matrix_view); -RAFT_INST(float, uint64_t); -RAFT_INST(int8_t, uint64_t); -RAFT_INST(uint8_t, uint64_t); +RAFT_INST(float, int64_t); +RAFT_INST(int8_t, int64_t); +RAFT_INST(uint8_t, int64_t); #undef RAFT_INST } // namespace raft::neighbors::ivf_flat From 05d899b36b76545d2439dbe47e4659d644ced227 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Tue, 21 Mar 2023 16:23:16 -0400 Subject: [PATCH 06/25] Stop setting package version attribute in wheels (#1359) This PR removes modification of the `__init__.py::version` attribute that occurs during the wheel build process. See https://github.com/rapidsai/ops/issues/2592 for more information. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Sevag H (https://github.com/sevagh) URL: https://github.com/rapidsai/raft/pull/1359 --- ci/release/apply_wheel_modifications.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ci/release/apply_wheel_modifications.sh b/ci/release/apply_wheel_modifications.sh index ed3d2a15fd..efc8f0c77c 100755 --- a/ci/release/apply_wheel_modifications.sh +++ b/ci/release/apply_wheel_modifications.sh @@ -6,10 +6,6 @@ VERSION=${1} CUDA_SUFFIX=${2} -# __init__.py versions -sed -i "s/__version__ = .*/__version__ = \"${VERSION}\"/g" python/pylibraft/pylibraft/__init__.py -sed -i "s/__version__ = .*/__version__ = \"${VERSION}\"/g" python/raft-dask/raft_dask/__init__.py - # pyproject.toml versions sed -i "s/^version = .*/version = \"${VERSION}\"/g" python/pylibraft/pyproject.toml sed -i "s/^version = .*/version = \"${VERSION}\"/g" python/raft-dask/pyproject.toml From a7e619cfec8b17a467122e0fd123aedad1bc5e06 Mon Sep 17 00:00:00 2001 From: Peter Andreas Entschev Date: Wed, 22 Mar 2023 22:53:40 +0100 Subject: [PATCH 07/25] Remove usage of Dask's `get_worker` (#1365) In dask/distributed#7580 get_worker was modified to return the worker of a task, thus it cannot be used by client.run, and we must now use dask_worker as the first argument to client.run to obtain the worker. Authors: - Peter Andreas Entschev (https://github.com/pentschev) Approvers: - Corey J. Nolet (https://github.com/cjnolet) - AJ Schmidt (https://github.com/ajschmidt8) URL: https://github.com/rapidsai/raft/pull/1365 --- ci/wheel_smoke_test_raft_dask.py | 21 ++- python/raft-dask/raft_dask/common/comms.py | 159 ++++++++++++------ python/raft-dask/raft_dask/test/test_comms.py | 24 ++- 3 files changed, 133 insertions(+), 71 deletions(-) diff --git a/ci/wheel_smoke_test_raft_dask.py b/ci/wheel_smoke_test_raft_dask.py index 32c13e61ca..5709ac901c 100644 --- a/ci/wheel_smoke_test_raft_dask.py +++ b/ci/wheel_smoke_test_raft_dask.py @@ -1,4 +1,19 @@ -from dask.distributed import Client, wait +# Copyright (c) 2019-2023, NVIDIA CORPORATION. +# +# 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. +# + +from dask.distributed import Client, get_worker, wait from dask_cuda import LocalCUDACluster, initialize from raft_dask.common import ( @@ -23,12 +38,12 @@ def func_test_send_recv(sessionId, n_trials): - handle = local_handle(sessionId) + handle = local_handle(sessionId, dask_worker=get_worker()) return perform_test_comms_send_recv(handle, n_trials) def func_test_collective(func, sessionId, root): - handle = local_handle(sessionId) + handle = local_handle(sessionId, dask_worker=get_worker()) return func(handle, root) diff --git a/python/raft-dask/raft_dask/common/comms.py b/python/raft-dask/raft_dask/common/comms.py index 56e40b98da..ebe9a8dc4f 100644 --- a/python/raft-dask/raft_dask/common/comms.py +++ b/python/raft-dask/raft_dask/common/comms.py @@ -19,7 +19,7 @@ import warnings from collections import OrderedDict -from dask.distributed import default_client, get_worker +from dask.distributed import default_client from pylibraft.common.handle import Handle @@ -242,7 +242,7 @@ def destroy(self): self.ucx_initialized = False -def local_handle(sessionId): +def local_handle(sessionId, dask_worker=None): """ Simple helper function for retrieving the local handle_t instance for a comms session on a worker. @@ -251,16 +251,19 @@ def local_handle(sessionId): ---------- sessionId : str session identifier from an initialized comms instance + dask_worker : dask_worker object + (Note: if called by client.run(), this is supplied by Dask + and not the client) Returns ------- handle : raft.Handle or None """ - state = get_raft_comm_state(sessionId, get_worker()) + state = get_raft_comm_state(sessionId, dask_worker) return state["handle"] if "handle" in state else None -def get_raft_comm_state(sessionId, state_object=None): +def get_raft_comm_state(sessionId, state_object=None, dask_worker=None): """ Retrieves cuML comms state on the scheduler node, for the given sessionId, creating a new session if it does not exist. If no session id is given, @@ -271,13 +274,16 @@ def get_raft_comm_state(sessionId, state_object=None): sessionId : SessionId value to retrieve from the dask_scheduler instances state_object : Object (either Worker, or Scheduler) on which the raft comm state will retrieved (or created) + dask_worker : dask_worker object + (Note: if called by client.run(), this is supplied by Dask + and not the client) Returns ------- session state : str session state associated with sessionId """ - state_object = state_object if state_object is not None else get_worker() + state_object = state_object if state_object is not None else dask_worker if not hasattr(state_object, "_raft_comm_state"): state_object._raft_comm_state = {} @@ -308,13 +314,19 @@ def set_nccl_root(sessionId, state_object): return raft_comm_state["nccl_uid"] -def get_ucx(): +def get_ucx(dask_worker=None): """ A simple convenience wrapper to make sure UCP listener and endpoints are only ever assigned once per worker. + + Parameters + ---------- + dask_worker : dask_worker object + (Note: if called by client.run(), this is supplied by Dask + and not the client) """ raft_comm_state = get_raft_comm_state( - sessionId="ucp", state_object=get_worker() + sessionId="ucp", state_object=dask_worker ) if "ucx" not in raft_comm_state: raft_comm_state["ucx"] = UCX.get() @@ -371,7 +383,7 @@ def _func_set_scheduler_as_nccl_root(sessionId, verbose, dask_scheduler): return nccl_uid -def _func_set_worker_as_nccl_root(sessionId, verbose): +def _func_set_worker_as_nccl_root(sessionId, verbose, dask_worker=None): """ Creates a persistent nccl uniqueId on the scheduler node. @@ -380,63 +392,74 @@ def _func_set_worker_as_nccl_root(sessionId, verbose): ---------- sessionId : Associated session to attach the unique ID to. verbose : Indicates whether or not to emit additional information + dask_worker : dask_worker object + (Note: if called by client.run(), this is supplied by Dask + and not the client) Return ------ uniqueId : byte str NCCL uniqueId, associating this DASK worker as its root node. """ - worker = get_worker() if verbose: - worker.log_event( + dask_worker.log_event( topic="info", msg=f"Setting worker as NCCL root for session, '{sessionId}'", ) - nccl_uid = set_nccl_root(sessionId=sessionId, state_object=worker) + nccl_uid = set_nccl_root(sessionId=sessionId, state_object=dask_worker) if verbose: - worker.log_event( + dask_worker.log_event( topic="info", msg="Done setting scheduler as NCCL root." ) return nccl_uid -def _func_ucp_listener_port(): - return get_ucx().listener_port() +def _func_ucp_listener_port(dask_worker=None): + return get_ucx(dask_worker=dask_worker).listener_port() async def _func_init_all( - sessionId, uniqueId, comms_p2p, worker_info, verbose, streams_per_handle + sessionId, + uniqueId, + comms_p2p, + worker_info, + verbose, + streams_per_handle, + dask_worker=None, ): - worker = get_worker() raft_comm_state = get_raft_comm_state( - sessionId=sessionId, state_object=worker + sessionId=sessionId, state_object=dask_worker ) raft_comm_state["nccl_uid"] = uniqueId - raft_comm_state["wid"] = worker_info[get_worker().address]["rank"] + raft_comm_state["wid"] = worker_info[dask_worker.address]["rank"] raft_comm_state["nworkers"] = len(worker_info) if verbose: - worker.log_event(topic="info", msg="Initializing NCCL.") + dask_worker.log_event(topic="info", msg="Initializing NCCL.") start = time.time() - _func_init_nccl(sessionId, uniqueId) + _func_init_nccl(sessionId, uniqueId, dask_worker=dask_worker) if verbose: elapsed = time.time() - start - worker.log_event( + dask_worker.log_event( topic="info", msg=f"NCCL Initialization took: {elapsed} seconds." ) if comms_p2p: if verbose: - worker.log_event(topic="info", msg="Initializing UCX Endpoints") + dask_worker.log_event( + topic="info", msg="Initializing UCX Endpoints" + ) if verbose: start = time.time() - await _func_ucp_create_endpoints(sessionId, worker_info) + await _func_ucp_create_endpoints( + sessionId, worker_info, dask_worker=dask_worker + ) if verbose: elapsed = time.time() - start @@ -444,18 +467,22 @@ async def _func_init_all( f"Done initializing UCX endpoints." f"Took: {elapsed} seconds.\nBuilding handle." ) - worker.log_event(topic="info", msg=msg) + dask_worker.log_event(topic="info", msg=msg) - _func_build_handle_p2p(sessionId, streams_per_handle, verbose) + _func_build_handle_p2p( + sessionId, streams_per_handle, verbose, dask_worker=dask_worker + ) if verbose: - worker.log_event(topic="info", msg="Done building handle.") + dask_worker.log_event(topic="info", msg="Done building handle.") else: - _func_build_handle(sessionId, streams_per_handle, verbose) + _func_build_handle( + sessionId, streams_per_handle, verbose, dask_worker=dask_worker + ) -def _func_init_nccl(sessionId, uniqueId): +def _func_init_nccl(sessionId, uniqueId, dask_worker=None): """ Initialize ncclComm_t on worker @@ -466,11 +493,13 @@ def _func_init_nccl(sessionId, uniqueId): uniqueId : array[byte] The NCCL unique Id generated from the client. + dask_worker : dask_worker object + (Note: if called by client.run(), this is supplied by Dask + and not the client) """ - worker = get_worker() raft_comm_state = get_raft_comm_state( - sessionId=sessionId, state_object=get_worker() + sessionId=sessionId, state_object=dask_worker, dask_worker=dask_worker ) wid = raft_comm_state["wid"] nWorkers = raft_comm_state["nworkers"] @@ -480,13 +509,15 @@ def _func_init_nccl(sessionId, uniqueId): n.init(nWorkers, uniqueId, wid) raft_comm_state["nccl"] = n except Exception as e: - worker.log_event( + dask_worker.log_event( topic="error", msg=f"An error occurred initializing NCCL: {e}." ) raise -def _func_build_handle_p2p(sessionId, streams_per_handle, verbose): +def _func_build_handle_p2p( + sessionId, streams_per_handle, verbose, dask_worker=None +): """ Builds a handle_t on the current worker given the initialized comms @@ -495,14 +526,16 @@ def _func_build_handle_p2p(sessionId, streams_per_handle, verbose): sessionId : str id to reference state for current comms instance. streams_per_handle : int number of internal streams to create verbose : bool print verbose logging output + dask_worker : dask_worker object + (Note: if called by client.run(), this is supplied by Dask + and not the client) """ - worker = get_worker() if verbose: - worker.log_event(topic="info", msg="Building p2p handle.") + dask_worker.log_event(topic="info", msg="Building p2p handle.") - ucp_worker = get_ucx().get_worker() + ucp_worker = get_ucx(dask_worker).get_worker() raft_comm_state = get_raft_comm_state( - sessionId=sessionId, state_object=worker + sessionId=sessionId, state_object=dask_worker ) handle = Handle(n_streams=streams_per_handle) @@ -512,21 +545,23 @@ def _func_build_handle_p2p(sessionId, streams_per_handle, verbose): workerId = raft_comm_state["wid"] if verbose: - worker.log_event(topic="info", msg="Injecting comms on handle.") + dask_worker.log_event(topic="info", msg="Injecting comms on handle.") inject_comms_on_handle( handle, nccl_comm, ucp_worker, eps, nWorkers, workerId, verbose ) if verbose: - worker.log_event( + dask_worker.log_event( topic="info", msg="Finished injecting comms on handle." ) raft_comm_state["handle"] = handle -def _func_build_handle(sessionId, streams_per_handle, verbose): +def _func_build_handle( + sessionId, streams_per_handle, verbose, dask_worker=None +): """ Builds a handle_t on the current worker given the initialized comms @@ -535,17 +570,19 @@ def _func_build_handle(sessionId, streams_per_handle, verbose): sessionId : str id to reference state for current comms instance. streams_per_handle : int number of internal streams to create verbose : bool print verbose logging output + dask_worker : dask_worker object + (Note: if called by client.run(), this is supplied by Dask + and not the client) """ - worker = get_worker() if verbose: - worker.log_event( + dask_worker.log_event( topic="info", msg="Finished injecting comms on handle." ) handle = Handle(n_streams=streams_per_handle) raft_comm_state = get_raft_comm_state( - sessionId=sessionId, state_object=worker + sessionId=sessionId, state_object=dask_worker ) workerId = raft_comm_state["wid"] @@ -558,16 +595,18 @@ def _func_build_handle(sessionId, streams_per_handle, verbose): raft_comm_state["handle"] = handle -def _func_store_initial_state(nworkers, sessionId, uniqueId, wid): +def _func_store_initial_state( + nworkers, sessionId, uniqueId, wid, dask_worker=None +): raft_comm_state = get_raft_comm_state( - sessionId=sessionId, state_object=get_worker() + sessionId=sessionId, state_object=dask_worker ) raft_comm_state["nccl_uid"] = uniqueId raft_comm_state["wid"] = wid raft_comm_state["nworkers"] = nworkers -async def _func_ucp_create_endpoints(sessionId, worker_info): +async def _func_ucp_create_endpoints(sessionId, worker_info, dask_worker): """ Runs on each worker to create ucp endpoints to all other workers @@ -577,6 +616,9 @@ async def _func_ucp_create_endpoints(sessionId, worker_info): uuid unique id for this instance worker_info : dict Maps worker addresses to NCCL ranks & UCX ports + dask_worker : dask_worker object + (Note: if called by client.run(), this is supplied by Dask + and not the client) """ eps = [None] * len(worker_info) count = 1 @@ -584,40 +626,47 @@ async def _func_ucp_create_endpoints(sessionId, worker_info): for k in worker_info: ip, port = parse_host_port(k) - ep = await get_ucx().get_endpoint(ip, worker_info[k]["port"]) + ep = await get_ucx(dask_worker=dask_worker).get_endpoint( + ip, worker_info[k]["port"] + ) eps[worker_info[k]["rank"]] = ep count += 1 raft_comm_state = get_raft_comm_state( - sessionId=sessionId, state_object=get_worker() + sessionId=sessionId, state_object=dask_worker ) raft_comm_state["ucp_eps"] = eps -async def _func_destroy_all(sessionId, comms_p2p, verbose=False): - worker = get_worker() +async def _func_destroy_all( + sessionId, comms_p2p, verbose=False, dask_worker=None +): if verbose: - worker.log_event(topic="info", msg="Destroying NCCL session state.") + dask_worker.log_event( + topic="info", msg="Destroying NCCL session state." + ) raft_comm_state = get_raft_comm_state( - sessionId=sessionId, state_object=worker + sessionId=sessionId, state_object=dask_worker ) if "nccl" in raft_comm_state: raft_comm_state["nccl"].destroy() del raft_comm_state["nccl"] if verbose: - worker.log_event(topic="info", msg="NCCL session state destroyed.") + dask_worker.log_event( + topic="info", msg="NCCL session state destroyed." + ) else: if verbose: - worker.log_event( + dask_worker.log_event( topic="warning", msg=f"Session state for, '{sessionId}', " f"does not contain expected 'nccl' element", ) if verbose: - worker.log_event( + dask_worker.log_event( topic="info", msg=f"Destroying CUDA handle for sessionId, '{sessionId}.'", ) @@ -626,7 +675,7 @@ async def _func_destroy_all(sessionId, comms_p2p, verbose=False): del raft_comm_state["handle"] else: if verbose: - worker.log_event( + dask_worker.log_event( topic="warning", msg=f"Session state for, '{sessionId}', " f"does not contain expected 'handle' element", diff --git a/python/raft-dask/raft_dask/test/test_comms.py b/python/raft-dask/raft_dask/test/test_comms.py index 74ec446e94..3a430f9270 100644 --- a/python/raft-dask/raft_dask/test/test_comms.py +++ b/python/raft-dask/raft_dask/test/test_comms.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2022, NVIDIA CORPORATION. +# Copyright (c) 2019-2023, NVIDIA CORPORATION. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import pytest -from dask.distributed import Client, wait +from dask.distributed import Client, get_worker, wait try: from raft_dask.common import ( @@ -60,32 +60,32 @@ def test_comms_init_no_p2p(cluster): def func_test_collective(func, sessionId, root): - handle = local_handle(sessionId) + handle = local_handle(sessionId, dask_worker=get_worker()) return func(handle, root) def func_test_send_recv(sessionId, n_trials): - handle = local_handle(sessionId) + handle = local_handle(sessionId, dask_worker=get_worker()) return perform_test_comms_send_recv(handle, n_trials) def func_test_device_send_or_recv(sessionId, n_trials): - handle = local_handle(sessionId) + handle = local_handle(sessionId, dask_worker=get_worker()) return perform_test_comms_device_send_or_recv(handle, n_trials) def func_test_device_sendrecv(sessionId, n_trials): - handle = local_handle(sessionId) + handle = local_handle(sessionId, dask_worker=get_worker()) return perform_test_comms_device_sendrecv(handle, n_trials) def func_test_device_multicast_sendrecv(sessionId, n_trials): - handle = local_handle(sessionId) + handle = local_handle(sessionId, dask_worker=get_worker()) return perform_test_comms_device_multicast_sendrecv(handle, n_trials) def func_test_comm_split(sessionId, n_trials): - handle = local_handle(sessionId) + handle = local_handle(sessionId, dask_worker=get_worker()) return perform_test_comm_split(handle, n_trials) @@ -114,11 +114,9 @@ def func_check_uid_on_scheduler(sessionId, uniqueId, dask_scheduler): ) -def func_check_uid_on_worker(sessionId, uniqueId): - from dask.distributed import get_worker - +def func_check_uid_on_worker(sessionId, uniqueId, dask_worker=None): return func_check_uid( - sessionId=sessionId, uniqueId=uniqueId, state_object=get_worker() + sessionId=sessionId, uniqueId=uniqueId, state_object=dask_worker ) @@ -127,7 +125,7 @@ def test_handles(cluster): client = Client(cluster) def _has_handle(sessionId): - return local_handle(sessionId) is not None + return local_handle(sessionId, dask_worker=get_worker()) is not None try: cb = Comms(verbose=True) From 08e7012bc00140f77732fd73b134f388edf119dd Mon Sep 17 00:00:00 2001 From: Allard Hendriksen Date: Thu, 23 Mar 2023 03:24:56 +0100 Subject: [PATCH 08/25] Reduce compile times of distance specializations (#1307) Following the findings in https://github.com/ahendriksen/raft/tree/investigate-compile-time-reduction-strategies#investigation-of-compile-times, this PR reduces the compile times of the pairwise distance specializations. This is achieved by: 1. Reducing the number of included files in the translation units where kernels are instantiated, specifically `spdlog` and `rmm` are avoided. 2. Limiting loop unrolling in kernels with expensive operations in the inner loop. Additional improvements geared towards iterative development: 1. The tests do not have to be recompiled when the internals of a pairwise distance kernel change. Before, a rebuilt was triggered due an include of `raft/distance/distance.cuh`. 2. Addition of a fine tuning benchmark for the pairwise distance kernels that separates building the kernel from the benchmark code. This dramatically speeds up development. Compiling an empty benchmark takes roughly 18 seconds on my machine. Whereas recompiling a kernel takes ~3.8 seconds. Without this addition, a commit like 35a2ad437 would require substantially more time to make sure that performance is not degraded. ![image](https://user-images.githubusercontent.com/4172822/225383120-5f8a82f9-0b46-4c39-bc1d-7b2a0551e881.png) ``` Parallel build time before: 270 seconds (6 cores, SMT, 12 jobs) Parallel build time before: 147 seconds (6 cores, SMT, 12 jobs) Sum of compile times before: 3022.6 seconds Sum of compile times after: 1816.2 seconds Comparison of compile times between headers and compiled: path before (s) after (s) change (s) change (%) pairwise_test None 0.486 None None ance/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu.o 101.1 10.3 -90.8 -89.8% src/distance/distance/specializations/detail/canberra_float_float_float_int.cu.o 52.9 6.3 -46.6 -88.0% /distance/distance/specializations/detail/canberra_double_double_double_int.cu.o 48.5 6.4 -42.1 -86.8% stance/distance/specializations/detail/jensen_shannon_float_float_float_int.cu.o 65.3 10.4 -55.0 -84.1% istance/distance/specializations/detail/kl_divergence_float_float_float_int.cu.o 70.2 12.6 -57.6 -82.0% stance/distance/specializations/detail/correlation_double_double_double_int.cu.o 46.7 8.9 -37.8 -80.9% distance/specializations/detail/hellinger_expanded_double_double_double_int.cu.o 41.6 8.1 -33.5 -80.6% nce/distance/specializations/detail/jensen_shannon_double_double_double_int.cu.o 74.6 15.1 -59.5 -79.7% ir/src/distance/distance/specializations/detail/l1_double_double_double_int.cu.o 40.9 8.4 -32.5 -79.4% ance/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu.o 40.7 8.6 -32.1 -78.8% distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu.o 40.8 9.0 -31.7 -77.8% istance/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu.o 45.9 10.2 -35.7 -77.8% src/distance/distance/specializations/detail/l_inf_double_double_double_int.cu.o 41.2 9.5 -31.8 -77.0% istance/distance/specializations/detail/russel_rao_double_double_double_int.cu.o 29.5 7.2 -22.3 -75.6% t.dir/src/distance/distance/specializations/detail/l1_float_float_float_int.cu.o 47.3 13.2 -34.1 -72.2% ce/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu.o 47.0 13.3 -33.7 -71.6% /distance/distance/specializations/detail/correlation_float_float_float_int.cu.o 49.4 14.1 -35.3 -71.5% ce/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu.o 43.6 12.5 -31.1 -71.4% c/distance/distance/specializations/detail/russel_rao_float_float_float_int.cu.o 28.5 8.2 -20.3 -71.2% ance/distance/specializations/detail/kl_divergence_double_double_double_int.cu.o 75.8 21.9 -53.9 -71.1% istance/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu.o 46.2 13.5 -32.7 -70.7% ir/src/distance/distance/specializations/detail/l_inf_float_float_float_int.cu.o 43.1 12.7 -30.4 -70.6% stance/distance/specializations/detail/l2_expanded_double_double_double_int.cu.o 52.3 24.9 -27.3 -52.3% /distance/distance/specializations/detail/l2_expanded_float_float_float_int.cu.o 75.8 40.3 -35.5 -46.8% rc/distance/distance/specializations/detail/cosine_double_double_double_int.cu.o 53.5 28.7 -24.8 -46.4% r/src/distance/distance/specializations/detail/cosine_float_float_float_int.cu.o 83.9 50.1 -33.8 -40.3% CMakeFiles/pairwise_test.dir/test/distance/fused_l2_nn.cu.o 85.1 64.1 -21.1 -24.7% wise_test.dir/src/distance/distance/specializations/fused_l2_nn_float_int64.cu.o 56.2 42.9 -13.3 -23.6% irwise_test.dir/src/distance/distance/specializations/fused_l2_nn_float_int.cu.o 52.5 40.2 -12.3 -23.5% CMakeFiles/pairwise_test.dir/test/distance/dist_lp_unexp.cu.o 56.3 43.3 -13.0 -23.1% CMakeFiles/pairwise_test.dir/test/distance/dist_russell_rao.cu.o 55.7 44.0 -11.7 -21.0% rwise_test.dir/src/distance/distance/specializations/fused_l2_nn_double_int.cu.o 45.3 36.4 -9.0 -19.8% CMakeFiles/pairwise_test.dir/test/distance/dist_l2_unexp.cu.o 54.6 44.1 -10.6 -19.3% CMakeFiles/pairwise_test.dir/test/distance/dist_canberra.cu.o 51.6 42.1 -9.6 -18.6% CMakeFiles/pairwise_test.dir/test/distance/dist_l2_exp.cu.o 53.1 43.4 -9.6 -18.2% CMakeFiles/pairwise_test.dir/test/distance/dist_l_inf.cu.o 53.2 43.9 -9.3 -17.5% CMakeFiles/pairwise_test.dir/test/distance/dist_hellinger.cu.o 53.1 44.0 -9.0 -17.0% CMakeFiles/pairwise_test.dir/test/distance/dist_hamming.cu.o 52.3 43.4 -8.9 -17.0% CMakeFiles/pairwise_test.dir/test/distance/dist_l2_sqrt_exp.cu.o 54.0 45.6 -8.4 -15.6% CMakeFiles/pairwise_test.dir/test/distance/dist_l1.cu.o 52.6 44.5 -8.1 -15.4% CMakeFiles/pairwise_test.dir/test/distance/dist_kl_divergence.cu.o 52.4 44.7 -7.7 -14.8% ise_test.dir/src/distance/distance/specializations/fused_l2_nn_double_int64.cu.o 43.5 37.2 -6.4 -14.7% CMakeFiles/pairwise_test.dir/test/distance/dist_cos.cu.o 52.4 44.8 -7.6 -14.5% CMakeFiles/pairwise_test.dir/test/distance/dist_jensen_shannon.cu.o 53.2 45.7 -7.6 -14.2% CMakeFiles/pairwise_test.dir/test/distance/dist_inner_product.cu.o 51.1 44.8 -6.3 -12.4% istance/distance/specializations/detail/inner_product_float_float_float_int.cu.o 39.5 35.1 -4.5 -11.3% CMakeFiles/pairwise_test.dir/test/distance/dist_correlation.cu.o 51.7 46.8 -4.9 -9.5% ance/distance/specializations/detail/inner_product_double_double_double_int.cu.o 37.1 33.9 -3.1 -8.5% src/distance/distance/specializations/detail/kernels/gram_matrix_base_float.cu.o 45.3 41.7 -3.6 -8.0% rc/distance/distance/specializations/detail/kernels/gram_matrix_base_double.cu.o 42.5 39.6 -2.9 -6.8% stance/distance/specializations/detail/kernels/polynomial_kernel_double_int.cu.o 40.4 38.5 -1.9 -4.8% CMakeFiles/pairwise_test.dir/test/distance/dist_adj.cu.o 123.3 117.8 -5.4 -4.4% CMakeFiles/pairwise_test.dir/test/distance/gram.cu.o 55.3 53.4 -1.9 -3.5% build.ninja 4.0 4.0 +0.0 +0.1% istance/distance/specializations/detail/kernels/polynomial_kernel_float_int.cu.o 45.2 45.6 +0.4 +0.8% .dir/src/distance/distance/specializations/detail/kernels/tanh_kernel_float.cu.o 45.2 46.0 +0.8 +1.7% dir/src/distance/distance/specializations/detail/kernels/tanh_kernel_double.cu.o 39.0 39.8 +0.8 +2.1% CMakeFiles/pairwise_test.dir/src/distance/distance/pairwise_distance.cu.o 39.6 50.1 +10.5 +26.6% ``` Authors: - Allard Hendriksen (https://github.com/ahendriksen) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) - Divye Gala (https://github.com/divyegala) URL: https://github.com/rapidsai/raft/pull/1307 --- cpp/CMakeLists.txt | 4 - cpp/bench/CMakeLists.txt | 5 + cpp/bench/distance/tune_pairwise/bench.cu | 151 +++++ cpp/bench/distance/tune_pairwise/kernel.cu | 88 +++ cpp/bench/distance/tune_pairwise/kernel.cuh | 44 ++ cpp/include/raft/core/kvp.hpp | 2 +- cpp/include/raft/distance/detail/distance.cuh | 107 ++-- .../distance/detail/distance_ops/canberra.cuh | 5 +- .../detail/distance_ops/correlation.cuh | 4 +- .../distance/detail/distance_ops/cosine.cuh | 11 +- .../distance/detail/distance_ops/cutlass.cuh | 6 +- .../distance/detail/distance_ops/hamming.cuh | 4 +- .../detail/distance_ops/hellinger.cuh | 4 +- .../detail/distance_ops/jensen_shannon.cuh | 5 +- .../detail/distance_ops/kl_divergence.cuh | 5 +- .../raft/distance/detail/distance_ops/l1.cuh | 4 +- .../distance/detail/distance_ops/l2_exp.cuh | 11 +- .../distance/detail/distance_ops/l2_unexp.cuh | 4 +- .../distance/detail/distance_ops/l_inf.cuh | 4 +- .../distance/detail/distance_ops/lp_unexp.cuh | 5 +- .../detail/distance_ops/russel_rao.cuh | 4 +- .../distance/detail/distance_ops/template.cuh | 10 +- .../raft/distance/detail/fused_l2_nn.cuh | 133 ++--- .../detail/pairwise_distance_base.cuh | 83 ++- .../detail/pairwise_distance_cutlass_base.cuh | 29 +- .../detail/pairwise_matrix/dispatch.cuh | 114 ++-- .../pairwise_matrix/dispatch_layout.cuh | 21 +- .../detail/pairwise_matrix/dispatch_sm60.cuh | 26 +- .../detail/pairwise_matrix/dispatch_sm80.cuh | 16 +- .../detail/pairwise_matrix/kernel_sm60.cuh | 69 +-- .../detail/00_write_template.py | 148 +++++ .../specializations/detail/canberra.cuh | 50 +- .../specializations/detail/correlation.cuh | 51 +- .../specializations/detail/cosine.cuh | 51 +- .../detail/hamming_unexpanded.cuh | 51 +- .../detail/hellinger_expanded.cuh | 50 +- .../specializations/detail/jensen_shannon.cuh | 50 +- .../specializations/detail/kl_divergence.cuh | 49 +- .../distance/specializations/detail/l1.cuh | 48 +- .../specializations/detail/l2_expanded.cuh | 49 +- .../detail/l2_sqrt_expanded.cuh | 54 -- .../detail/l2_sqrt_unexpanded.cuh | 54 -- .../specializations/detail/l2_unexpanded.cuh | 49 +- .../distance/specializations/detail/l_inf.cuh | 48 +- .../specializations/detail/lp_unexpanded.cuh | 49 +- .../specializations/detail/russel_rao.cuh | 50 +- .../distance/specializations/distance.cuh | 2 - .../raft/spatial/knn/detail/fused_l2_knn.cuh | 524 +++++++++--------- cpp/include/raft/util/arch.cuh | 23 +- cpp/include/raft/util/cuda_dev_essentials.cuh | 91 +++ cpp/include/raft/util/cuda_rt_essentials.hpp | 60 ++ cpp/include/raft/util/cuda_utils.cuh | 105 +--- cpp/include/raft/util/cudart_utils.hpp | 38 +- cpp/include/raft/util/device_loads_stores.cuh | 5 +- .../detail/00_write_template.py | 159 ++++++ .../canberra_double_double_double_int.cu | 36 +- .../detail/canberra_float_float_float_int.cu | 35 +- .../correlation_double_double_double_int.cu | 35 +- .../correlation_float_float_float_int.cu | 35 +- .../detail/cosine_double_double_double_int.cu | 35 +- .../detail/cosine_float_float_float_int.cu | 35 +- ...ing_unexpanded_double_double_double_int.cu | 35 +- ...amming_unexpanded_float_float_float_int.cu | 35 +- ...inger_expanded_double_double_double_int.cu | 35 +- ...ellinger_expanded_float_float_float_int.cu | 34 +- ...jensen_shannon_double_double_double_int.cu | 36 +- .../jensen_shannon_float_float_float_int.cu | 36 +- .../kl_divergence_double_double_double_int.cu | 35 +- .../kl_divergence_float_float_float_int.cu | 35 +- .../detail/l1_double_double_double_int.cu | 35 +- .../detail/l1_float_float_float_int.cu | 35 +- .../l2_expanded_double_double_double_int.cu | 37 +- .../l2_expanded_float_float_float_int.cu | 36 +- ..._sqrt_expanded_double_double_double_int.cu | 38 -- .../l2_sqrt_expanded_float_float_float_int.cu | 38 -- ...qrt_unexpanded_double_double_double_int.cu | 38 -- ...2_sqrt_unexpanded_float_float_float_int.cu | 38 -- .../l2_unexpanded_double_double_double_int.cu | 35 +- .../l2_unexpanded_float_float_float_int.cu | 35 +- .../detail/l_inf_double_double_double_int.cu | 34 +- .../detail/l_inf_float_float_float_int.cu | 35 +- .../lp_unexpanded_double_double_double_int.cu | 35 +- .../lp_unexpanded_float_float_float_int.cu | 35 +- .../russel_rao_double_double_double_int.cu | 36 +- .../russel_rao_float_float_float_int.cu | 35 +- cpp/test/distance/distance_base.cuh | 74 ++- cpp/test/distance/fused_l2_nn.cu | 30 +- 87 files changed, 2057 insertions(+), 2000 deletions(-) create mode 100644 cpp/bench/distance/tune_pairwise/bench.cu create mode 100644 cpp/bench/distance/tune_pairwise/kernel.cu create mode 100644 cpp/bench/distance/tune_pairwise/kernel.cuh create mode 100644 cpp/include/raft/distance/specializations/detail/00_write_template.py delete mode 100644 cpp/include/raft/distance/specializations/detail/l2_sqrt_expanded.cuh delete mode 100644 cpp/include/raft/distance/specializations/detail/l2_sqrt_unexpanded.cuh create mode 100644 cpp/include/raft/util/cuda_dev_essentials.cuh create mode 100644 cpp/include/raft/util/cuda_rt_essentials.hpp create mode 100644 cpp/src/distance/specializations/detail/00_write_template.py delete mode 100644 cpp/src/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu delete mode 100644 cpp/src/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu delete mode 100644 cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu delete mode 100644 cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index bdaacb4a85..034dc059b0 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -312,10 +312,6 @@ if(RAFT_COMPILE_LIBRARY) src/distance/specializations/detail/l1_double_double_double_int.cu src/distance/specializations/detail/l2_expanded_float_float_float_int.cu src/distance/specializations/detail/l2_expanded_double_double_double_int.cu - src/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu - src/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu - src/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu - src/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu src/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu src/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu src/distance/specializations/detail/l_inf_double_double_double_int.cu diff --git a/cpp/bench/CMakeLists.txt b/cpp/bench/CMakeLists.txt index 8049074c09..d92ccba8e3 100644 --- a/cpp/bench/CMakeLists.txt +++ b/cpp/bench/CMakeLists.txt @@ -72,6 +72,11 @@ if(BUILD_BENCH) OPTIONAL LIB ) + ConfigureBench( + NAME TUNE_DISTANCE PATH bench/distance/tune_pairwise/kernel.cu + bench/distance/tune_pairwise/bench.cu bench/main.cpp + ) + ConfigureBench( NAME DISTANCE_BENCH diff --git a/cpp/bench/distance/tune_pairwise/bench.cu b/cpp/bench/distance/tune_pairwise/bench.cu new file mode 100644 index 0000000000..87159ab1b1 --- /dev/null +++ b/cpp/bench/distance/tune_pairwise/bench.cu @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ + +// Tuning benchmarks. +// +// Goals: +// +// 1. Fast compile times to maintain iteration speed. +// 2. Create benchmarks that can inform the design of the kernels. +// +// Non-goals: +// +// 1. Measure every distance operation. Instead measures just one distance +// operation at the same time. +// 2. Be useful for finding performance regressions. This is handled by the +// normal benchmarks. +// +// So far, both goals are partly achieved. +// +// RE (1), COMPILE TIMES: kernel.cu is fast to compile. This file is not. +// When the internals of a pairwise distance kernel is changed, this file is not +// recompiled. +// +// RE 2, benchmarks with intent: this file contains a benchmark to check the +// maximal throughput of a kernel. Measuring other things, like performance on +// skinny or wide matrices is not yet implemented. + +#include "kernel.cuh" // launch_kernel +#include // std::min +#include // RAFT_BENCH_REGISTER +#include // pairwise_matrix_params +#include // rmm::device_uvector +#include // std::vector + +namespace raft::bench::distance::tune { + +// Max throughput benchmark. +// +// Goal: Measure the maximum distances/sec that can be computed. +// +// To achieve this, we make sure that: +// +// - Input data size is a multiple of the block tile size. +// +// - Perfect distribution of work between SMs, i.e. the number of block tiles is +// a large multiple (num_waves) of the number of blocks (#SMs * occupancy). +// +// - Multiple iterations over Kblk are executed (num_k_iters). +struct throughput_param { + int num_waves; + int occupancy; + int num_k_iters; +}; + +const std::vector throughput_params{ + // 32 waves, requested occupancy of 4, and 32 k iterations typically achieves + // maximum throughput. No need to pick higher values. + {32, 4, 32}, +}; + +struct throughput_bench : public fixture { + const throughput_param p; + + throughput_bench(const throughput_param& p_) : p(p_) {} + + void run_benchmark(::benchmark::State& state) override + { + // Get block size: + int block_m, block_n, block_k; + get_block_size(block_m, block_n, block_k); + + // Determine number of blocks that will be launched. This informs the size + // of the inputs as well as the grid size. + const int num_sms = raft::getMultiProcessorCount(); + const int max_occupancy = get_max_occupancy(); + const int occupancy = std::min(p.occupancy, max_occupancy); + const int num_blocks = occupancy * num_sms; + dim3 grid(num_blocks); + + // Create input sizes that are a multiple of the block tile size. + size_t m = block_m; + size_t n = block_n * p.num_waves * num_blocks; + size_t k = block_k * p.num_k_iters; + + // DataT, OutT, IdxT, etc, are defined in tuned_kernel.cuh + rmm::device_uvector x_vec(m * k, stream); + rmm::device_uvector y_vec(n * k, stream); + rmm::device_uvector x_norm_vec(m, stream); + rmm::device_uvector y_norm_vec(n, stream); + rmm::device_uvector out_vec(m * n, stream); + + auto x = x_vec.data(); + auto y = y_vec.data(); + auto x_norm = x_norm_vec.data(); + auto y_norm = y_norm_vec.data(); + auto out = out_vec.data(); + FinOpT fin_op{}; + + // Create kernel parameter struct. Flip x and y if column major. + IdxT ldx = row_major ? k : m; + IdxT ldy = row_major ? k : n; + IdxT ld_out = row_major ? n : m; + + // Template parameters of pairwise_matrix_params are defined in kernel.cuh + pairwise_matrix_params kparams{ + IdxT(m), IdxT(n), IdxT(k), ldx, ldy, ld_out, x, y, x_norm, y_norm, out, fin_op, row_major}; + + // Run benchmark + loop_on_state(state, [&]() { launch_kernel(kparams, grid, stream); }); + + // Report metrics. We don't report flop/s because we do not know for each + // distance operation how many flops it costs. For L2_unexp and l1, we can + // double this number to get the flop/s. For l2 expanded, core_ops/s should + // equal flop/s (modulo the sqrt and subtracting from the norm). + size_t num_core_ops = m * n * k; + size_t read_elts = n * k + m * k; + size_t write_elts = m * n; + + state.counters["m"] = benchmark::Counter(m); + state.counters["n"] = benchmark::Counter(n); + state.counters["k"] = benchmark::Counter(k); + state.counters["occupancy"] = benchmark::Counter(occupancy); + state.counters["# waves"] = benchmark::Counter(p.num_waves); + state.counters["# k iters"] = benchmark::Counter(p.num_k_iters); + + state.counters["core_ops/s"] = benchmark::Counter(num_core_ops, + benchmark::Counter::kIsIterationInvariantRate, + benchmark::Counter::OneK::kIs1000); + + state.counters["BW"] = benchmark::Counter(write_elts * sizeof(OutT) + read_elts * sizeof(DataT), + benchmark::Counter::kIsIterationInvariantRate, + benchmark::Counter::OneK::kIs1000); + } +}; + +RAFT_BENCH_REGISTER(throughput_bench, "", throughput_params); + +} // namespace raft::bench::distance::tune diff --git a/cpp/bench/distance/tune_pairwise/kernel.cu b/cpp/bench/distance/tune_pairwise/kernel.cu new file mode 100644 index 0000000000..3112e1ea9a --- /dev/null +++ b/cpp/bench/distance/tune_pairwise/kernel.cu @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 "kernel.cuh" +#include // pairwise_matrix_sm60_wrapper +#include // raft::linalg::Policy4x4 +#include // raft::util::arch::SM_compute_arch + +namespace raft::bench::distance::tune { + +// Distance op +using OpT = raft::distance::detail::ops::lp_unexp_distance_op; +constexpr float metric_arg = 2.0; +OpT distance_op{metric_arg}; + +// Kernel policy +constexpr int vec_len = 1; +using Policy = typename raft::linalg::Policy4x4::Policy; + +// Architecture +namespace arch = raft::util::arch; +constexpr auto sm_compat_range = arch::SM_range(arch::SM_min(), arch::SM_future()); + +void launch_kernel(pairwise_matrix_params params, dim3 grid, cudaStream_t stream) +{ + dim3 block(Policy::Nthreads); + int smem_size = OpT::shared_mem_size(); + + // Obtain function pointer to kernel + auto kernel = raft::distance::detail::pairwise_matrix_kernel; + + kernel<<>>(distance_op, params); + RAFT_CUDA_TRY(cudaGetLastError()); +} + +void get_block_size(int& m, int& n, int& k) +{ + m = Policy::Mblk; + n = Policy::Nblk; + k = Policy::Kblk; +} + +void* get_kernel_ptr() +{ + auto kernel = raft::distance::detail::pairwise_matrix_kernel; + return reinterpret_cast(kernel); +} + +int get_max_occupancy() +{ + void* kernel_ptr = get_kernel_ptr(); + int max_occupancy; + int smem_size = OpT::shared_mem_size(); + + RAFT_CUDA_TRY(cudaOccupancyMaxActiveBlocksPerMultiprocessor( + &max_occupancy, kernel_ptr, Policy::Nthreads, smem_size)); + + return max_occupancy; +} + +} // namespace raft::bench::distance::tune diff --git a/cpp/bench/distance/tune_pairwise/kernel.cuh b/cpp/bench/distance/tune_pairwise/kernel.cuh new file mode 100644 index 0000000000..5da54a343c --- /dev/null +++ b/cpp/bench/distance/tune_pairwise/kernel.cuh @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ + +#pragma once + +#include // lp_unexp_distance_op +#include // pairwise_matrix_params + +namespace raft::bench::distance::tune { + +// Launch one specific kernel with the following template parameters +constexpr bool row_major = true; +using DataT = float; +using AccT = float; +using OutT = DataT; +using IdxT = int; + +using FinOpT = raft::identity_op; + +using pairwise_matrix_params = + raft::distance::detail::pairwise_matrix_params; + +// Launches kernel +void launch_kernel(pairwise_matrix_params, dim3, cudaStream_t); + +// Describes the block size that is decided by the policy +void get_block_size(int& m, int& n, int& k); + +int get_max_occupancy(); + +} // namespace raft::bench::distance::tune diff --git a/cpp/include/raft/core/kvp.hpp b/cpp/include/raft/core/kvp.hpp index 8d3321eb77..192d160d45 100644 --- a/cpp/include/raft/core/kvp.hpp +++ b/cpp/include/raft/core/kvp.hpp @@ -20,7 +20,7 @@ #ifdef _RAFT_HAS_CUDA #include -#include +#include // raft::shfl_xor #endif namespace raft { /** diff --git a/cpp/include/raft/distance/detail/distance.cuh b/cpp/include/raft/distance/detail/distance.cuh index f469250b45..7493c4e558 100644 --- a/cpp/include/raft/distance/detail/distance.cuh +++ b/cpp/include/raft/distance/detail/distance.cuh @@ -16,25 +16,18 @@ #pragma once -#include -#include - -#include -#include -#include -#include -#include - #include - +#include #include #include - +#include +#include #include #include -#include -#include -#include +#include +#include +#include +#include namespace raft { namespace distance { @@ -140,14 +133,14 @@ void distance_impl(raft::resources const& handle, cudaStream_t stream = raft::resource::get_cuda_stream(handle); - AccT* norm_col_vec = workspace; - AccT* norm_row_vec = workspace; - AccT* sq_norm_col_vec = workspace; - AccT* sq_norm_row_vec = workspace; + AccT* x_norm = workspace; + AccT* y_norm = workspace; + AccT* sq_x_norm = workspace; + AccT* sq_y_norm = workspace; if (x != y) { - norm_row_vec += m; + y_norm += m; - raft::linalg::reduce(norm_col_vec, + raft::linalg::reduce(x_norm, x, k, m, @@ -158,7 +151,7 @@ void distance_impl(raft::resources const& handle, false, raft::identity_op(), raft::add_op()); - raft::linalg::reduce(norm_row_vec, + raft::linalg::reduce(y_norm, y, k, n, @@ -170,12 +163,12 @@ void distance_impl(raft::resources const& handle, raft::identity_op(), raft::add_op()); - sq_norm_col_vec += (m + n); - sq_norm_row_vec = sq_norm_col_vec + m; - raft::linalg::rowNorm(sq_norm_col_vec, x, k, m, raft::linalg::L2Norm, is_row_major, stream); - raft::linalg::rowNorm(sq_norm_row_vec, y, k, n, raft::linalg::L2Norm, is_row_major, stream); + sq_x_norm += (m + n); + sq_y_norm = sq_x_norm + m; + raft::linalg::rowNorm(sq_x_norm, x, k, m, raft::linalg::L2Norm, is_row_major, stream); + raft::linalg::rowNorm(sq_y_norm, y, k, n, raft::linalg::L2Norm, is_row_major, stream); } else { - raft::linalg::reduce(norm_col_vec, + raft::linalg::reduce(x_norm, x, k, m, @@ -186,15 +179,15 @@ void distance_impl(raft::resources const& handle, false, raft::identity_op(), raft::add_op()); - sq_norm_col_vec += m; - sq_norm_row_vec = sq_norm_col_vec; - raft::linalg::rowNorm(sq_norm_col_vec, x, k, m, raft::linalg::L2Norm, is_row_major, stream); + sq_x_norm += m; + sq_y_norm = sq_x_norm; + raft::linalg::rowNorm(sq_x_norm, x, k, m, raft::linalg::L2Norm, is_row_major, stream); } using OpT = ops::correlation_distance_op; - OpT corr_op(is_row_major, sq_norm_col_vec, sq_norm_row_vec, m, n, k); + OpT corr_op(is_row_major, sq_x_norm, sq_y_norm, m, n, k); pairwise_matrix_dispatch( - corr_op, m, n, k, x, y, norm_col_vec, norm_row_vec, out, fin_op, stream, is_row_major); + corr_op, m, n, k, x, y, x_norm, y_norm, out, fin_op, stream, is_row_major); } template @@ -223,22 +216,22 @@ void distance_impl(raft::resources const& handle, cudaStream_t stream = raft::resource::get_cuda_stream(handle); - DataT* norm_A = workspace; - DataT* norm_B = workspace; + DataT* x_norm = workspace; + DataT* y_norm = workspace; if (x != y) { - norm_B += m; + y_norm += m; raft::linalg::rowNorm( - norm_A, x, k, m, raft::linalg::L2Norm, is_row_major, stream, raft::sqrt_op{}); + x_norm, x, k, m, raft::linalg::L2Norm, is_row_major, stream, raft::sqrt_op{}); raft::linalg::rowNorm( - norm_B, y, k, n, raft::linalg::L2Norm, is_row_major, stream, raft::sqrt_op{}); + y_norm, y, k, n, raft::linalg::L2Norm, is_row_major, stream, raft::sqrt_op{}); } else { raft::linalg::rowNorm( - norm_A, x, k, m, raft::linalg::L2Norm, is_row_major, stream, raft::sqrt_op{}); + x_norm, x, k, m, raft::linalg::L2Norm, is_row_major, stream, raft::sqrt_op{}); } ops::cosine_distance_op distance_op{}; pairwise_matrix_dispatch( - distance_op, m, n, k, x, y, norm_A, norm_B, out, fin_op, stream, is_row_major); + distance_op, m, n, k, x, y, x_norm, y_norm, out, fin_op, stream, is_row_major); } template @@ -389,10 +382,6 @@ void distance_impl(raft::resources const& handle, return (!x_zero) * raft::exp(input); }; - // This op takes some shortcuts when x equals y. So its behavior changes based - // on this. - ops::kl_divergence_op kl_divergence{is_row_major, x == y}; - if (x != y) { raft::linalg::unaryOp( (DataT*)y, y, n * k, unaryOp_lambda, stream); @@ -401,8 +390,12 @@ void distance_impl(raft::resources const& handle, const DataT* x_norm = nullptr; const DataT* y_norm = nullptr; - pairwise_matrix_dispatch( - kl_divergence, m, n, k, x, y, x_norm, y_norm, out, fin_op, stream, is_row_major); + // This op takes some shortcuts when x equals y. So its behavior changes based + // on this. + ops::kl_divergence_op distance_op{is_row_major, x == y}; + + pairwise_matrix_dispatch( + distance_op, m, n, k, x, y, x_norm, y_norm, out, fin_op, stream, is_row_major); if (x != y) { // Now reverse previous log (x) back to x using (e ^ log(x)) @@ -464,22 +457,22 @@ void distance_impl_l2_expanded( // NOTE: different name "workspace size error"); ASSERT(workspace != nullptr, "workspace is null"); - DataT* norm_A = workspace; - DataT* norm_B = workspace; + DataT* x_norm = workspace; + DataT* y_norm = workspace; if (x != y) { - norm_B += m; + y_norm += m; raft::linalg::rowNorm( - norm_A, x, k, m, raft::linalg::L2Norm, is_row_major, stream, raft::identity_op{}); + x_norm, x, k, m, raft::linalg::L2Norm, is_row_major, stream, raft::identity_op{}); raft::linalg::rowNorm( - norm_B, y, k, n, raft::linalg::L2Norm, is_row_major, stream, raft::identity_op{}); + y_norm, y, k, n, raft::linalg::L2Norm, is_row_major, stream, raft::identity_op{}); } else { raft::linalg::rowNorm( - norm_A, x, k, m, raft::linalg::L2Norm, is_row_major, stream, raft::identity_op{}); + x_norm, x, k, m, raft::linalg::L2Norm, is_row_major, stream, raft::identity_op{}); } ops::l2_exp_distance_op distance_op{perform_sqrt}; pairwise_matrix_dispatch( - distance_op, m, n, k, x, y, norm_A, norm_B, out, fin_op, stream, is_row_major); + distance_op, m, n, k, x, y, x_norm, y_norm, out, fin_op, stream, is_row_major); } template @@ -543,13 +536,13 @@ void distance_impl(raft::resources const& handle, ops::l2_unexp_distance_op l2_op(perform_sqrt); // The unexpanded L2 does not require the norms of a and b to be calculated. - const DataT* norm_A = nullptr; - const DataT* norm_B = nullptr; + const DataT* x_norm = nullptr; + const DataT* y_norm = nullptr; cudaStream_t stream = raft::resource::get_cuda_stream(handle); pairwise_matrix_dispatch( - l2_op, m, n, k, x, y, norm_A, norm_B, out, fin_op, stream, is_row_major); + l2_op, m, n, k, x, y, x_norm, y_norm, out, fin_op, stream, is_row_major); } template @@ -571,13 +564,13 @@ void distance_impl(raft::resources const& handle, ops::l2_unexp_distance_op l2_op(perform_sqrt); // The unexpanded L2 does not require the norms of a and b to be calculated. - const DataT* norm_A = nullptr; - const DataT* norm_B = nullptr; + const DataT* x_norm = nullptr; + const DataT* y_norm = nullptr; cudaStream_t stream = raft::resource::get_cuda_stream(handle); pairwise_matrix_dispatch( - l2_op, m, n, k, x, y, norm_A, norm_B, out, fin_op, stream, is_row_major); + l2_op, m, n, k, x, y, x_norm, y_norm, out, fin_op, stream, is_row_major); } template diff --git a/cpp/include/raft/distance/detail/distance_ops/canberra.cuh b/cpp/include/raft/distance/detail/distance_ops/canberra.cuh index 930294ce31..eaf37b7e9c 100644 --- a/cpp/include/raft/distance/detail/distance_ops/canberra.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/canberra.cuh @@ -16,7 +16,8 @@ #pragma once -#include +#include // raft::abs +#include // DI namespace raft::distance::detail::ops { @@ -42,7 +43,7 @@ struct canberra_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/correlation.cuh b/cpp/include/raft/distance/detail/distance_ops/correlation.cuh index 289b69070a..4fc4bb8297 100644 --- a/cpp/include/raft/distance/detail/distance_ops/correlation.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/correlation.cuh @@ -16,7 +16,7 @@ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -61,7 +61,7 @@ struct correlation_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize + (2 * (Policy::Mblk + Policy::Nblk) * sizeof(DataT)); } diff --git a/cpp/include/raft/distance/detail/distance_ops/cosine.cuh b/cpp/include/raft/distance/detail/distance_ops/cosine.cuh index 7c37c27b4e..0883136c9f 100644 --- a/cpp/include/raft/distance/detail/distance_ops/cosine.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/cosine.cuh @@ -16,7 +16,7 @@ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -26,7 +26,7 @@ struct cosine_cutlass_op { __device__ cosine_cutlass_op() noexcept {} __device__ AccT operator()(DataT& aNorm, const DataT& bNorm, DataT& accVal) const noexcept { - return static_cast(1.0) - (AccT)(accVal / (aNorm * bNorm)); + return static_cast(1.0) - static_cast(accVal / (aNorm * bNorm)); } __device__ AccT operator()(DataT aData) const noexcept { return aData; } }; @@ -53,7 +53,7 @@ struct cosine_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize + ((Policy::Mblk + Policy::Nblk) * sizeof(DataT)); } @@ -76,7 +76,10 @@ struct cosine_distance_op { } } - cosine_cutlass_op get_cutlass_op() { return cosine_cutlass_op(); } + constexpr cosine_cutlass_op get_cutlass_op() const + { + return cosine_cutlass_op(); + } }; } // namespace raft::distance::detail::ops diff --git a/cpp/include/raft/distance/detail/distance_ops/cutlass.cuh b/cpp/include/raft/distance/detail/distance_ops/cutlass.cuh index d3eb90467b..7a4fe0ce83 100644 --- a/cpp/include/raft/distance/detail/distance_ops/cutlass.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/cutlass.cuh @@ -16,7 +16,8 @@ #pragma once -#include +#include // std::false_type +#include // std::declval namespace raft::distance::detail::ops { @@ -34,7 +35,8 @@ struct has_cutlass_op : std::false_type { // Specialization recognizes types that do support CUTLASS template -struct has_cutlass_op> : std::true_type { +struct has_cutlass_op().get_cutlass_op())>> + : std::true_type { }; } // namespace raft::distance::detail::ops diff --git a/cpp/include/raft/distance/detail/distance_ops/hamming.cuh b/cpp/include/raft/distance/detail/distance_ops/hamming.cuh index 1cfdcfdc73..475b8892e9 100644 --- a/cpp/include/raft/distance/detail/distance_ops/hamming.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/hamming.cuh @@ -16,7 +16,7 @@ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -45,7 +45,7 @@ struct hamming_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/hellinger.cuh b/cpp/include/raft/distance/detail/distance_ops/hellinger.cuh index c4aecc7a6f..0489b45854 100644 --- a/cpp/include/raft/distance/detail/distance_ops/hellinger.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/hellinger.cuh @@ -15,7 +15,7 @@ */ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -42,7 +42,7 @@ struct hellinger_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/jensen_shannon.cuh b/cpp/include/raft/distance/detail/distance_ops/jensen_shannon.cuh index 41eeb9dd83..e46c63734c 100644 --- a/cpp/include/raft/distance/detail/distance_ops/jensen_shannon.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/jensen_shannon.cuh @@ -15,7 +15,8 @@ */ #pragma once -#include +#include // raft::log +#include // DI namespace raft::distance::detail::ops { @@ -44,7 +45,7 @@ struct jensen_shannon_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/kl_divergence.cuh b/cpp/include/raft/distance/detail/distance_ops/kl_divergence.cuh index d046b62c30..d083c5ddcc 100644 --- a/cpp/include/raft/distance/detail/distance_ops/kl_divergence.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/kl_divergence.cuh @@ -15,7 +15,8 @@ */ #pragma once -#include +#include // raft::log +#include // DI namespace raft::distance::detail::ops { @@ -49,7 +50,7 @@ struct kl_divergence_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/l1.cuh b/cpp/include/raft/distance/detail/distance_ops/l1.cuh index 8ec4000827..7e86fd3603 100644 --- a/cpp/include/raft/distance/detail/distance_ops/l1.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/l1.cuh @@ -15,7 +15,7 @@ */ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -41,7 +41,7 @@ struct l1_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/l2_exp.cuh b/cpp/include/raft/distance/detail/distance_ops/l2_exp.cuh index 2a7af53813..95577fd311 100644 --- a/cpp/include/raft/distance/detail/distance_ops/l2_exp.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/l2_exp.cuh @@ -16,7 +16,7 @@ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -54,7 +54,7 @@ struct l2_exp_distance_op { using AccT = AccType; using IdxT = IdxType; - bool sqrt; + const bool sqrt; l2_exp_distance_op(bool sqrt_) noexcept : sqrt(sqrt_) {} @@ -67,7 +67,7 @@ struct l2_exp_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize + ((Policy::Mblk + Policy::Nblk) * sizeof(DataT)); } @@ -102,7 +102,10 @@ struct l2_exp_distance_op { } } - l2_exp_cutlass_op get_cutlass_op() { return l2_exp_cutlass_op(sqrt); } + constexpr l2_exp_cutlass_op get_cutlass_op() const + { + return l2_exp_cutlass_op(sqrt); + } }; } // namespace raft::distance::detail::ops diff --git a/cpp/include/raft/distance/detail/distance_ops/l2_unexp.cuh b/cpp/include/raft/distance/detail/distance_ops/l2_unexp.cuh index f0ea591eaf..62c212ee8f 100644 --- a/cpp/include/raft/distance/detail/distance_ops/l2_unexp.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/l2_unexp.cuh @@ -16,7 +16,7 @@ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -46,7 +46,7 @@ struct l2_unexp_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/l_inf.cuh b/cpp/include/raft/distance/detail/distance_ops/l_inf.cuh index fb21fb1a21..88853a3083 100644 --- a/cpp/include/raft/distance/detail/distance_ops/l_inf.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/l_inf.cuh @@ -16,7 +16,7 @@ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -42,7 +42,7 @@ struct l_inf_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/lp_unexp.cuh b/cpp/include/raft/distance/detail/distance_ops/lp_unexp.cuh index 71dfd51a6e..290f4af1b4 100644 --- a/cpp/include/raft/distance/detail/distance_ops/lp_unexp.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/lp_unexp.cuh @@ -15,7 +15,8 @@ */ #pragma once -#include +#include // raft::pow, raft::abs +#include // DI namespace raft::distance::detail::ops { @@ -45,7 +46,7 @@ struct lp_unexp_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/russel_rao.cuh b/cpp/include/raft/distance/detail/distance_ops/russel_rao.cuh index ea09e4d1db..63dbf350d1 100644 --- a/cpp/include/raft/distance/detail/distance_ops/russel_rao.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/russel_rao.cuh @@ -16,7 +16,7 @@ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -47,7 +47,7 @@ struct russel_rao_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. template - constexpr size_t shared_mem_size() + static constexpr size_t shared_mem_size() { return Policy::SmemSize; } diff --git a/cpp/include/raft/distance/detail/distance_ops/template.cuh b/cpp/include/raft/distance/detail/distance_ops/template.cuh index 6998f3cad4..4320068361 100644 --- a/cpp/include/raft/distance/detail/distance_ops/template.cuh +++ b/cpp/include/raft/distance/detail/distance_ops/template.cuh @@ -16,7 +16,7 @@ #pragma once -#include +#include // DI namespace raft::distance::detail::ops { @@ -42,8 +42,8 @@ struct template_distance_op { // Size of shared memory. This is normally decided by the kernel policy, but // some ops such as correlation_distance_op use more. - template - constexpr size_t shared_mem_size() + template + static constexpr size_t shared_mem_size() { return Policy::SmemSize + TODO; } @@ -59,6 +59,10 @@ struct template_distance_op { { TODO; } + + // If exist, returns a cutlass op that performs the same operation. + // See cosine and l2_exp distance ops for an example. + constexpr l2_exp_cutlass_op get_cutlass_op() const { TODO; } }; } // namespace raft::distance::detail::ops diff --git a/cpp/include/raft/distance/detail/fused_l2_nn.cuh b/cpp/include/raft/distance/detail/fused_l2_nn.cuh index 8fbd7a9c69..be6fed9f10 100644 --- a/cpp/include/raft/distance/detail/fused_l2_nn.cuh +++ b/cpp/include/raft/distance/detail/fused_l2_nn.cuh @@ -16,23 +16,20 @@ #pragma once -#include -#include -#include -#include -#include -#include +#include // size_t +#include // std::numeric_limits +#include // raft::KeyValuePair +#include // raft::identity_op +#include // ops::l2_exp_distance_op +#include // PairwiseDistances +#include // Policy +#include // raft::ceildiv, raft::shfl namespace raft { namespace distance { namespace detail { -#if (ENABLE_MEMCPY_ASYNC == 1) -#include -using namespace nvcuda::experimental; -#endif - template struct KVPMinReduceImpl { typedef raft::KeyValuePair KVP; @@ -124,11 +121,10 @@ DI void updateReducedVal( template __global__ __launch_bounds__(P::Nthreads, 2) void fusedL2NNkernel(OutT* min, const DataT* x, @@ -142,7 +138,7 @@ __global__ __launch_bounds__(P::Nthreads, 2) void fusedL2NNkernel(OutT* min, int* mutex, ReduceOpT redOp, KVPReduceOpT pairRedOp, - CoreLambda core_op, + OpT distance_op, FinalLambda fin_op) { extern __shared__ char smem[]; @@ -163,24 +159,6 @@ __global__ __launch_bounds__(P::Nthreads, 2) void fusedL2NNkernel(OutT* min, IdxT gridStrideY) { KVPReduceOpT pairRed_op(pairRedOp); -#pragma unroll - for (int i = 0; i < P::AccRowsPerTh; ++i) { -#pragma unroll - for (int j = 0; j < P::AccColsPerTh; ++j) { - acc[i][j] = regxn[i] + regyn[j] - (DataT)2.0 * acc[i][j]; - } - } - if (Sqrt) { -#pragma unroll - for (int i = 0; i < P::AccRowsPerTh; ++i) { -#pragma unroll - for (int j = 0; j < P::AccColsPerTh; ++j) { - auto acc_ij = acc[i][j]; - acc[i][j] = acc_ij > DataT{0} ? raft::sqrt(acc_ij) : DataT{0}; - } - } - } - // intra thread reduce const auto acccolid = threadIdx.x % P::AccThCols; const auto accrowid = threadIdx.x / P::AccThCols; @@ -229,18 +207,18 @@ __global__ __launch_bounds__(P::Nthreads, 2) void fusedL2NNkernel(OutT* min, }; IdxT lda = k, ldb = k, ldd = n; - PairwiseDistances + row_major, + write_out> obj(x, y, m, @@ -251,9 +229,9 @@ __global__ __launch_bounds__(P::Nthreads, 2) void fusedL2NNkernel(OutT* min, ldd, xn, yn, - nullptr, + nullptr, // Output pointer smem, - core_op, + distance_op, epilog_lambda, fin_op, rowEpilog_lambda); @@ -289,9 +267,6 @@ void fusedL2NNImpl(OutT* min, constexpr auto maxVal = std::numeric_limits::max(); typedef KeyValuePair KVPair; - // Accumulation operation lambda - auto core_lambda = [] __device__(DataT & acc, DataT & x, DataT & y) { acc += x * y; }; - RAFT_CUDA_TRY(cudaMemsetAsync(workspace, 0, sizeof(int) * m, stream)); if (initOutBuffer) { initKernel @@ -300,59 +275,25 @@ void fusedL2NNImpl(OutT* min, } constexpr size_t shmemSize = P::SmemSize + ((P::Mblk + P::Nblk) * sizeof(DataT)); - if (sqrt) { - auto fusedL2NNSqrt = fusedL2NNkernel; - dim3 grid = launchConfigGenerator

(m, n, shmemSize, fusedL2NNSqrt); - - fusedL2NNSqrt<<>>(min, - x, - y, - xn, - yn, - m, - n, - k, - maxVal, - workspace, - redOp, - pairRedOp, - core_lambda, - raft::identity_op{}); - } else { - auto fusedL2NN = fusedL2NNkernel; - dim3 grid = launchConfigGenerator

(m, n, shmemSize, fusedL2NN); - fusedL2NN<<>>(min, - x, - y, - xn, - yn, - m, - n, - k, - maxVal, - workspace, - redOp, - pairRedOp, - core_lambda, - raft::identity_op{}); - } + using AccT = DataT; + ops::l2_exp_distance_op distance_op{sqrt}; + + raft::identity_op fin_op{}; + + auto kernel = fusedL2NNkernel; + + dim3 grid = launchConfigGenerator

(m, n, shmemSize, kernel); + + kernel<<>>( + min, x, y, xn, yn, m, n, k, maxVal, workspace, redOp, pairRedOp, distance_op, fin_op); RAFT_CUDA_TRY(cudaGetLastError()); } diff --git a/cpp/include/raft/distance/detail/pairwise_distance_base.cuh b/cpp/include/raft/distance/detail/pairwise_distance_base.cuh index 0293f10c29..c6b09be31e 100644 --- a/cpp/include/raft/distance/detail/pairwise_distance_base.cuh +++ b/cpp/include/raft/distance/detail/pairwise_distance_base.cuh @@ -14,14 +14,11 @@ * limitations under the License. */ #pragma once -#include -#include -#include -#include -#include -#include +#include // raft::linalg::Contractions_NT +#include // ceildiv +#include // RAFT_CUDA_TRY -#include +#include // size_t namespace raft { namespace distance { @@ -29,16 +26,12 @@ namespace detail { /** * @brief Device class for L1, L2 and cosine distance metrics. - * @tparam useNorms whether norms are needed * @tparam DataT input data-type (for A and B matrices) * @tparam AccT accumulation data-type * @tparam OutT output data-type (for C and D matrices) * @tparam IdxT index data-type * @tparam Policy struct which tunes the Contraction kernel - * @tparam CoreLambda tells how to accumulate an x and y into - acc. its signature: - template void core_lambda(AccT& acc, - const DataT& x, const DataT& y) + * @tparam OpT A distance operation, e.g., cosine_distance_op. * @tparam EpilogueLambda applies an elementwise function to compute final values. Its signature is: template void epilogue_lambda @@ -56,19 +49,17 @@ namespace detail { * @param[in] yn row norms of input matrix B. Required for expanded L2, cosine * @param[output] pD output matrix * @param[in] smem shared mem buffer for intermediate storage of A, B, xn & yn. - * @param core_op the core accumulation operation lambda + * @param distance_op the distance operation, e.g. cosine_distance_op * @param epilog_op the epilog operation lambda * @param fin_op the final gemm epilogue lambda * @param rowEpilog_op epilog lambda that executes when a full row has been processed */ -template > struct PairwiseDistances : public BaseClass { + // Get accumulation type from distance_op + using AccT = typename OpT::AccT; + private: typedef Policy P; const DataT* xn; @@ -83,7 +77,7 @@ struct PairwiseDistances : public BaseClass { const DataT* const yBase; OutT* dOutput; char* smem; - CoreLambda core_op; + OpT distance_op; EpilogueLambda epilog_op; FinalLambda fin_op; rowEpilogueLambda rowEpilog_op; @@ -109,7 +103,7 @@ struct PairwiseDistances : public BaseClass { const DataT* _yn, OutT* _dOutput, char* _smem, - CoreLambda _core_op, + OpT _distance_op, EpilogueLambda _epilog_op, FinalLambda _fin_op, rowEpilogueLambda _rowEpilog_op) @@ -119,7 +113,7 @@ struct PairwiseDistances : public BaseClass { yBase(_y), dOutput(_dOutput), smem(_smem), - core_op(_core_op), + distance_op(_distance_op), epilog_op(_epilog_op), fin_op(_fin_op), rowEpilog_op(_rowEpilog_op), @@ -159,15 +153,25 @@ struct PairwiseDistances : public BaseClass { this->switch_read_buffer(); // Epilog: - if (useNorms) { + if (distance_op.use_norms) { DataT regxn[P::AccRowsPerTh], regyn[P::AccColsPerTh]; load_norms(tile_idx_m, tile_idx_n, regxn, regyn); // Overlap ldg with epilog computation ldgNextGridStride(tile_idx_m, tile_idx_n); + // Calculate distance_op epilog. + // Use .template to disambiguate (See: + // https://en.cppreference.com/w/cpp/language/dependent_name) + distance_op.template epilog(acc, regxn, regyn, tile_idx_n, tile_idx_m); + // And any possible additional epilogs epilog_op(acc, regxn, regyn, tile_idx_n, tile_idx_m); } else { // Overlap ldg with epilog computation ldgNextGridStride(tile_idx_m, tile_idx_n); + // Calculate distance_op epilog. + // Use .template to disambiguate (See: + // https://en.cppreference.com/w/cpp/language/dependent_name) + distance_op.template epilog(acc, nullptr, nullptr, tile_idx_n, tile_idx_m); + // And any possible additional epilogs epilog_op(acc, nullptr, nullptr, tile_idx_n, tile_idx_m); } if (writeOut) { store_output(tile_idx_m, tile_idx_n); } @@ -201,24 +205,41 @@ struct PairwiseDistances : public BaseClass { } } - DI void accumulate() + DI void accumulate_reg_tile(DataT (®_x)[P::AccRowsPerTh][P::Veclen], + DataT (®_y)[P::AccColsPerTh][P::Veclen]) { #pragma unroll - for (int ki = 0; ki < P::Kblk; ki += P::Veclen) { - this->ldsXY(ki); + for (int v = 0; v < P::Veclen; ++v) { #pragma unroll for (int i = 0; i < P::AccRowsPerTh; ++i) { #pragma unroll for (int j = 0; j < P::AccColsPerTh; ++j) { -#pragma unroll - for (int v = 0; v < P::Veclen; ++v) { - core_op(acc[i][j], this->regx[i][v], this->regy[j][v]); - } + distance_op.core(acc[i][j], reg_x[i][v], reg_y[j][v]); } } } } + DI void accumulate() + { + // We have a separate ldsXY and accumulate_reg_tile outside the loop body, + // so that these separated calls can be interspersed with preceding and + // following instructions, thereby hiding latency. + this->ldsXY(0); + + // If expensive inner loop, do not unroll loop. + constexpr int num_iterations = P::Kblk / P::Veclen - 1; + constexpr int unroll_count = decltype(distance_op)::expensive_inner_loop ? 1 : num_iterations; +#pragma unroll unroll_count + for (int ki = P::Veclen; ki < P::Kblk; ki += P::Veclen) { + accumulate_reg_tile(this->regx, this->regy); + this->ldsXY(ki); + } + + // Accumulate last loaded tile. + accumulate_reg_tile(this->regx, this->regy); + } + DI void load_norms(IdxT tile_idx_m, IdxT tile_idx_n, DataT (®xn)[P::AccRowsPerTh], @@ -274,7 +295,11 @@ struct PairwiseDistances : public BaseClass { template dim3 launchConfigGenerator(IdxT m, IdxT n, std::size_t sMemSize, T func) { - const auto numSMs = raft::getMultiProcessorCount(); + int devId; + RAFT_CUDA_TRY(cudaGetDevice(&devId)); + int numSMs; + RAFT_CUDA_TRY(cudaDeviceGetAttribute(&numSMs, cudaDevAttrMultiProcessorCount, devId)); + int numBlocksPerSm = 0; dim3 grid; diff --git a/cpp/include/raft/distance/detail/pairwise_distance_cutlass_base.cuh b/cpp/include/raft/distance/detail/pairwise_distance_cutlass_base.cuh index c5fdd28117..efcd5d9389 100644 --- a/cpp/include/raft/distance/detail/pairwise_distance_cutlass_base.cuh +++ b/cpp/include/raft/distance/detail/pairwise_distance_cutlass_base.cuh @@ -64,21 +64,20 @@ template -typename std::enable_if::value>::type cutlassDistanceKernel( - const DataT* x, - const DataT* y, - const DataT* xn, - const DataT* yn, - IdxT m, - IdxT n, - IdxT k, - IdxT lda, - IdxT ldb, - IdxT ldd, - OutT* dOutput, - FinalLambda fin_op, - OpT distance_op, - cudaStream_t stream) +std::enable_if_t::value> cutlassDistanceKernel(const DataT* x, + const DataT* y, + const DataT* xn, + const DataT* yn, + IdxT m, + IdxT n, + IdxT k, + IdxT lda, + IdxT ldb, + IdxT ldd, + OutT* dOutput, + FinalLambda fin_op, + OpT distance_op, + cudaStream_t stream) { static_assert(!(std::is_same::value), "OutType bool is not supported use uint8_t instead"); diff --git a/cpp/include/raft/distance/detail/pairwise_matrix/dispatch.cuh b/cpp/include/raft/distance/detail/pairwise_matrix/dispatch.cuh index 8524ce6fdf..e04b56ee8a 100644 --- a/cpp/include/raft/distance/detail/pairwise_matrix/dispatch.cuh +++ b/cpp/include/raft/distance/detail/pairwise_matrix/dispatch.cuh @@ -15,63 +15,74 @@ */ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include +/* This file has two responsibilities: + * + * 1. Dispatch to the correct implementation of a kernel based on the + * architecture of the device on which the kernel will be launched. For + * instance, the cosine distance has a CUTLASS-based implementation that can + * be used on SM80+ and the normal implementation that is used on older + * architectures. + * + * 2. Provide concise function templates that can be instantiated in + * src/distance/distance/specializations/detail/. Previously, + * raft::distance::detail::distance was instantiated. The function + * necessarily required a large set of include files, which slowed down the + * build. The raft::distance::detail::pairwise_matrix_arch_dispatch functions + * do not require as large an include files set, which speeds up the build. + */ + +#include // ops::has_cutlass_op +#include // dispatch_sm60 +#include // pairwise_matrix_params +#include // raft::util::arch::SM_* + +// NOTE: to minimize compile times, we do not include dispatch_sm80.cuh. +// Including dispatch_sm80.cuh can slow down compile times (due to CUTLASS). +// Therefore, it is the including file's responsibility to include the correct +// dispatch_smXX.cuh headers, as is done in raft/distance/detail/distance.cuh +// and the specializations in src/distance/distance/specializations/detail/. namespace raft::distance::detail { +// This forward-declaration ensures that we do not need to include +// dispatch_sm80.cuh if we are not calling it in practice. This makes compiling +// all the non-CUTLASS based distance specializations faster. For CUTLASS-based +// distances, dispatch_sm80.cuh has to be included by the file including this +// file. template -void pairwise_matrix_dispatch(OpT distance_op, - IdxT m, - IdxT n, - IdxT k, - const DataT* x, - const DataT* y, - const DataT* x_norm, - const DataT* y_norm, - OutT* out, - FinOpT fin_op, - cudaStream_t stream, - bool is_row_major) -{ - // Create kernel parameter struct. Flip x and y if column major. - IdxT ldx = is_row_major ? k : m; - IdxT ldy = is_row_major ? k : n; - IdxT ld_out = is_row_major ? n : m; - - pairwise_matrix_params params{ - m, n, k, ldx, ldy, ld_out, x, y, x_norm, y_norm, out, fin_op, is_row_major}; - - if (!params.is_row_major) { params.flip_x_and_y(); } + typename SM_compat_t> +void pairwise_matrix_sm80_dispatch(OpT, + pairwise_matrix_params, + SM_compat_t, + cudaStream_t); +template +void pairwise_matrix_instantiation_point(OpT distance_op, + pairwise_matrix_params params, + cudaStream_t stream) +{ // On CUDA 12: // - always execute normal kernel // // On CUDA 11 and below: // - execute CUTLASS-based kernel on SM_80 and above // - execute normal kernel below SM_80 + namespace arch = raft::util::arch; constexpr bool is_ctk_12 = __CUDACC_VER_MAJOR__ == 12; constexpr bool cutlass_op_unavailable = !ops::has_cutlass_op(); if constexpr (is_ctk_12 || cutlass_op_unavailable) { // Always execute legacy kernels on CUDA 12 - auto any_range = raft::arch::SM_range(raft::arch::SM_min(), raft::arch::SM_future()); + auto any_range = arch::SM_range(arch::SM_min(), arch::SM_future()); pairwise_matrix_sm60_dispatch(distance_op, params, any_range, stream); } else { - auto cutlass_range = raft::arch::SM_range(raft::arch::SM_80(), raft::arch::SM_future()); - auto legacy_range = raft::arch::SM_range(raft::arch::SM_min(), raft::arch::SM_80()); + auto cutlass_range = arch::SM_range(arch::SM_80(), arch::SM_future()); + auto legacy_range = arch::SM_range(arch::SM_min(), arch::SM_80()); // Get pointer to SM60 kernel to determine the runtime architecture of the // current system. Other methods to determine the architecture (that do not @@ -79,7 +90,7 @@ void pairwise_matrix_dispatch(OpT distance_op, // https://github.com/NVIDIA/cub/issues/545 auto sm60_wrapper = pairwise_matrix_sm60_get_wrapper(distance_op, params, legacy_range); void* kernel_ptr = reinterpret_cast(sm60_wrapper.kernel_ptr); - auto runtime_arch = raft::arch::kernel_runtime_arch(kernel_ptr); + auto runtime_arch = arch::kernel_runtime_arch(kernel_ptr); if (cutlass_range.contains(runtime_arch)) { // If device is SM_80 or later, use CUTLASS-based kernel. @@ -92,4 +103,35 @@ void pairwise_matrix_dispatch(OpT distance_op, } } +template +void pairwise_matrix_dispatch(OpT distance_op, + IdxT m, + IdxT n, + IdxT k, + const DataT* x, + const DataT* y, + const DataT* x_norm, + const DataT* y_norm, + OutT* out, + FinOpT fin_op, + cudaStream_t stream, + bool is_row_major) +{ + // Create kernel parameter struct. Flip x and y if column major. + IdxT ldx = is_row_major ? k : m; + IdxT ldy = is_row_major ? k : n; + IdxT ld_out = is_row_major ? n : m; + + pairwise_matrix_params params{ + m, n, k, ldx, ldy, ld_out, x, y, x_norm, y_norm, out, fin_op, is_row_major}; + + if (!params.is_row_major) { params.flip_x_and_y(); } + pairwise_matrix_instantiation_point(distance_op, params, stream); +} + }; // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/detail/pairwise_matrix/dispatch_layout.cuh b/cpp/include/raft/distance/detail/pairwise_matrix/dispatch_layout.cuh index c1e4c08af4..f2b0e59822 100644 --- a/cpp/include/raft/distance/detail/pairwise_matrix/dispatch_layout.cuh +++ b/cpp/include/raft/distance/detail/pairwise_matrix/dispatch_layout.cuh @@ -15,10 +15,11 @@ */ #pragma once -#include "kernel_sm60.cuh" -#include -#include - +#include // std::min +#include // size_t +#include // RAFT_EXPECTS +#include // pairwise_matrix_params +#include // std::integral_constant namespace raft::distance::detail { /** @@ -99,15 +100,15 @@ auto dispatch_layout(bool row_major, int vec_len, F&& f) { if (row_major) { switch (vec_len) { - case 4: return f(std::bool_constant(), vec_len_constant<4>()); - case 2: return f(std::bool_constant(), vec_len_constant<2>()); - default: return f(std::bool_constant(), vec_len_constant<1>()); + case 4: return f(std::true_type(), vec_len_constant<4>()); + case 2: return f(std::true_type(), vec_len_constant<2>()); + default: return f(std::true_type(), vec_len_constant<1>()); } } else { switch (vec_len) { - case 4: return f(std::bool_constant(), vec_len_constant<4>()); - case 2: return f(std::bool_constant(), vec_len_constant<2>()); - default: return f(std::bool_constant(), vec_len_constant<1>()); + case 4: return f(std::false_type(), vec_len_constant<4>()); + case 2: return f(std::false_type(), vec_len_constant<2>()); + default: return f(std::false_type(), vec_len_constant<1>()); } } } diff --git a/cpp/include/raft/distance/detail/pairwise_matrix/dispatch_sm60.cuh b/cpp/include/raft/distance/detail/pairwise_matrix/dispatch_sm60.cuh index 6e284007ea..2080fbe9cd 100644 --- a/cpp/include/raft/distance/detail/pairwise_matrix/dispatch_sm60.cuh +++ b/cpp/include/raft/distance/detail/pairwise_matrix/dispatch_sm60.cuh @@ -15,10 +15,10 @@ */ #pragma once -#include -#include -#include -#include +#include // std::min +#include // dispatch_layout +#include // pairwise_matrix_sm60_wrapper +#include // raft::linalg::Policy4x4 namespace raft::distance::detail { @@ -35,7 +35,11 @@ pairwise_matrix_sm60_wrapper pairwise_matrix_sm6 { int vec_len = determine_vec_len(params); - return dispatch_layout(params.is_row_major, vec_len, [&](auto row_major, auto vec_len_aligned) { + // f takes compile-time constants row_major and vec_len aligned and returns + // the corresponding kernel wrapper. The wrapper contains the launch + // parameters of the kernel: a pointer to the kernel function, grid size, + // block size, and shared memory size. + auto f = [&](auto row_major, auto vec_len_aligned) { // row_major and vec_len are std::integral_constants of type bool and int // respectively. @@ -46,15 +50,19 @@ pairwise_matrix_sm60_wrapper pairwise_matrix_sm6 // Prevent double, vec_len=4 combination (this is not supported) constexpr int vec_len = std::min(vec_len_op, static_cast(16 / sizeof(DataT))); - typedef typename raft::linalg::Policy4x4::Policy RowPolicy; - typedef typename raft::linalg::Policy4x4::ColPolicy ColPolicy; - typedef typename std::conditional::type Policy; + using RowPolicy = typename raft::linalg::Policy4x4::Policy; + using ColPolicy = typename raft::linalg::Policy4x4::ColPolicy; + using Policy = typename std::conditional::type; auto wrapper = make_pairwise_matrix_sm60_wrapper(distance_op, params, sm_compat_range); return wrapper; - }); + }; + + // Dispatch_layout calls f with appropriate compile time constants based on + // the runtime values of params.is_row_major and vec_len. + return dispatch_layout(params.is_row_major, vec_len, f); } template // std::min -#include -#include +#include // std::min +#include // cutlassDistanceKernel +#include // dispatch_layout namespace raft::distance::detail { @@ -34,7 +34,9 @@ void pairwise_matrix_sm80_dispatch(OpT distance_op, { int vec_len = determine_vec_len(params); - dispatch_layout(params.is_row_major, vec_len, [&](auto row_major, auto vec_len_aligned) { + // f takes compile-time constants row_major and vec_len aligned and runs the + // corresponding cutlass launch code. + auto f = [&](auto row_major, auto vec_len_aligned) { // row_major and vec_len are std::integral_constants of type bool and int // respectively. @@ -56,7 +58,11 @@ void pairwise_matrix_sm80_dispatch(OpT distance_op, params.fin_op, distance_op, stream); - }); + }; + + // Dispatch_layout calls f with appropriate compile time constants based on + // the runtime values of params.is_row_major and vec_len. + dispatch_layout(params.is_row_major, vec_len, f); } }; // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/detail/pairwise_matrix/kernel_sm60.cuh b/cpp/include/raft/distance/detail/pairwise_matrix/kernel_sm60.cuh index 6e3ab7b26b..2d0a98862e 100644 --- a/cpp/include/raft/distance/detail/pairwise_matrix/kernel_sm60.cuh +++ b/cpp/include/raft/distance/detail/pairwise_matrix/kernel_sm60.cuh @@ -15,11 +15,11 @@ */ #pragma once -#include -#include -#include -#include -#include +#include // assert +#include // raft::void_op +#include // PairwiseDistances +#include // pairwise_matrix_params +#include // raft::util::arch::SM_compute_arch namespace raft::distance::detail { @@ -36,43 +36,27 @@ __global__ __launch_bounds__(Policy::Nthreads, 2) void pairwise_matrix_kernel( { // Early exit to minimize the size of the kernel when it is not supposed to be compiled. constexpr SM_compat_t sm_compat_range{}; - if constexpr (!sm_compat_range.contains(raft::arch::SM_compute_arch())) { + if constexpr (!sm_compat_range.contains(raft::util::arch::SM_compute_arch())) { assert(false); return; } extern __shared__ char smem[]; - using AccT = typename OpT::AccT; - - // Wrap operator back into lambdas. This is temporary and should be removed. - // See: https://github.com/rapidsai/raft/issues/1323 - auto core_op = [distance_op] __device__(AccT & acc, DataT & x, DataT & y) { - distance_op.core(acc, x, y); - }; - auto epilog_op = [distance_op] __device__(AccT acc[Policy::AccRowsPerTh][Policy::AccColsPerTh], - DataT * regxn, - DataT * regyn, - IdxT gridStrideX, - IdxT gridStrideY) { - // Use .template to disambiguate (See: - // https://en.cppreference.com/w/cpp/language/dependent_name) - distance_op.template epilog(acc, regxn, regyn, gridStrideX, gridStrideY); - }; - + // The epilog is already provided by distance_op. Do not provide additional + // epilogs. + auto epilog_op = raft::void_op(); // No support for row_epilog_op. auto row_epilog_op = raft::void_op(); // Always write output constexpr bool write_out = true; constexpr bool use_norms = distance_op.use_norms; - PairwiseDistances -void pairwise_matrix(OpT distance_op, - pairwise_matrix_params params, - cudaStream_t stream) -{ - dim3 blk(Policy::Nthreads); - // Use .template to disambiguate (See: - // https://en.cppreference.com/w/cpp/language/dependent_name) - size_t smem_size = distance_op.template shared_mem_size(); - // Obtain function pointer to kernel - auto kernel = - pairwise_matrix_kernel; - dim3 grid = launchConfigGenerator(params.m, params.n, smem_size, kernel); - - kernel<<>>(distance_op, params); - RAFT_CUDA_TRY(cudaGetLastError()); -} - // The type of a pointer to the pairwise matrix kernel. The following template // arguments are type-erased: // @@ -181,9 +140,9 @@ pairwise_matrix_sm60_wrapper make_pairwise_matri SM_compat_t sm_compat_range) { dim3 block(Policy::Nthreads); - // Use .template to disambiguate (See: + // Use ::template to disambiguate (See: // https://en.cppreference.com/w/cpp/language/dependent_name) - int smem_size = distance_op.template shared_mem_size(); + int smem_size = OpT::template shared_mem_size(); // Obtain function pointer to kernel auto kernel = pairwise_matrix_kernel; diff --git a/cpp/include/raft/distance/specializations/detail/00_write_template.py b/cpp/include/raft/distance/specializations/detail/00_write_template.py new file mode 100644 index 0000000000..63ae6580b4 --- /dev/null +++ b/cpp/include/raft/distance/specializations/detail/00_write_template.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 + +# This template manages all files in this directory, apart from +# inner_product.cuh and kernels.cuh. + + +# NOTE: this template is not perfectly formatted. Use pre-commit to get +# everything in shape again. +start_template = """/* + * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * + * 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. + */ + +#pragma once + +#include + +namespace raft::distance::detail { + +""" + +extern_template = """ +extern template void pairwise_matrix_instantiation_point( + OpT, + pairwise_matrix_params, + cudaStream_t); +""" + +end_template = """} // namespace raft::distance::detail +""" + +data_type_instances = [ + dict( + DataT="float", + AccT="float", + OutT="float", + IdxT="int", + ), + dict( + DataT="double", + AccT="double", + OutT="double", + IdxT="int", + ), +] + + + + +op_instances = [ + dict( + path_prefix="canberra", + OpT="ops::canberra_distance_op", + ), + dict( + path_prefix="correlation", + OpT="ops::correlation_distance_op", + ), + dict( + path_prefix="cosine", + OpT="ops::cosine_distance_op", + # cosine uses CUTLASS for SM80+ + ), + dict( + path_prefix="hamming_unexpanded", + OpT="ops::hamming_distance_op", + ), + dict( + path_prefix="hellinger_expanded", + OpT="ops::hellinger_distance_op", + ), + # inner product is handled by cublas. + dict( + path_prefix="jensen_shannon", + OpT="ops::jensen_shannon_distance_op", + ), + dict( + path_prefix="kl_divergence", + OpT="ops::kl_divergence_op", + ), + dict( + path_prefix="l1", + OpT="ops::l1_distance_op", + ), + dict( + path_prefix="l2_expanded", + OpT="ops::l2_exp_distance_op", + # L2 expanded uses CUTLASS for SM80+ + ), + dict( + path_prefix="l2_unexpanded", + OpT="ops::l2_unexp_distance_op", + ), + dict( + path_prefix="l_inf", + OpT="ops::l_inf_distance_op", + ), + dict( + path_prefix="lp_unexpanded", + OpT="ops::lp_unexp_distance_op", + ), + dict( + path_prefix="russel_rao", + OpT="ops::russel_rao_distance_op", + ), +] + +def fill_in(s, template): + for k, v in template.items(): + s = s.replace(k, v) + return s + +for op_instance in op_instances: + path = fill_in("path_prefix.cuh", op_instance) + with open(path, "w") as f: + f.write(start_template) + + for data_type_instance in data_type_instances: + op_data_instance = { + k : fill_in(v, data_type_instance) + for k, v in op_instance.items() + } + instance = { + **op_data_instance, + **data_type_instance, + "FinopT": "raft::identity_op", + } + + text = fill_in(extern_template, instance) + + f.write(text) + + f.write(end_template) diff --git a/cpp/include/raft/distance/specializations/detail/canberra.cuh b/cpp/include/raft/distance/specializations/detail/canberra.cuh index badce715a5..276c85e5f6 100644 --- a/cpp/include/raft/distance/specializations/detail/canberra.cuh +++ b/cpp/include/raft/distance/specializations/detail/canberra.cuh @@ -16,37 +16,25 @@ #pragma once -#include #include -namespace raft { -namespace distance { -namespace detail { -extern template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::canberra_distance_op, + int, + float, + float, + raft::identity_op>(ops::canberra_distance_op, + pairwise_matrix_params, + cudaStream_t); + +extern template void pairwise_matrix_instantiation_point< + ops::canberra_distance_op, + int, + double, + double, + raft::identity_op>(ops::canberra_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/correlation.cuh b/cpp/include/raft/distance/specializations/detail/correlation.cuh index 013a0d43a3..f019f678df 100644 --- a/cpp/include/raft/distance/specializations/detail/correlation.cuh +++ b/cpp/include/raft/distance/specializations/detail/correlation.cuh @@ -18,36 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void -distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::correlation_distance_op, + int, + float, + float, + raft::identity_op>(ops::correlation_distance_op, + pairwise_matrix_params, + cudaStream_t); + +extern template void pairwise_matrix_instantiation_point< + ops::correlation_distance_op, + int, + double, + double, + raft::identity_op>(ops::correlation_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/cosine.cuh b/cpp/include/raft/distance/specializations/detail/cosine.cuh index c88bd1b0f6..dcde4ec286 100644 --- a/cpp/include/raft/distance/specializations/detail/cosine.cuh +++ b/cpp/include/raft/distance/specializations/detail/cosine.cuh @@ -18,36 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void -distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point, + int, + float, + float, + raft::identity_op>( + ops::cosine_distance_op, + pairwise_matrix_params, + cudaStream_t); + +extern template void pairwise_matrix_instantiation_point< + ops::cosine_distance_op, + int, + double, + double, + raft::identity_op>(ops::cosine_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/hamming_unexpanded.cuh b/cpp/include/raft/distance/specializations/detail/hamming_unexpanded.cuh index 3c5cad3315..1d6964fbce 100644 --- a/cpp/include/raft/distance/specializations/detail/hamming_unexpanded.cuh +++ b/cpp/include/raft/distance/specializations/detail/hamming_unexpanded.cuh @@ -18,36 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void -distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::hamming_distance_op, + int, + float, + float, + raft::identity_op>(ops::hamming_distance_op, + pairwise_matrix_params, + cudaStream_t); + +extern template void pairwise_matrix_instantiation_point< + ops::hamming_distance_op, + int, + double, + double, + raft::identity_op>(ops::hamming_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/hellinger_expanded.cuh b/cpp/include/raft/distance/specializations/detail/hellinger_expanded.cuh index bf214c046f..f96a06f919 100644 --- a/cpp/include/raft/distance/specializations/detail/hellinger_expanded.cuh +++ b/cpp/include/raft/distance/specializations/detail/hellinger_expanded.cuh @@ -18,37 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void -distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); +extern template void pairwise_matrix_instantiation_point< + ops::hellinger_distance_op, + int, + float, + float, + raft::identity_op>(ops::hellinger_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::hellinger_distance_op, + int, + double, + double, + raft::identity_op>(ops::hellinger_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/jensen_shannon.cuh b/cpp/include/raft/distance/specializations/detail/jensen_shannon.cuh index 145834fb70..0b58646582 100644 --- a/cpp/include/raft/distance/specializations/detail/jensen_shannon.cuh +++ b/cpp/include/raft/distance/specializations/detail/jensen_shannon.cuh @@ -18,37 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void -distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); +extern template void pairwise_matrix_instantiation_point< + ops::jensen_shannon_distance_op, + int, + float, + float, + raft::identity_op>(ops::jensen_shannon_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::jensen_shannon_distance_op, + int, + double, + double, + raft::identity_op>(ops::jensen_shannon_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/kl_divergence.cuh b/cpp/include/raft/distance/specializations/detail/kl_divergence.cuh index f0928916cd..5c164e0fd4 100644 --- a/cpp/include/raft/distance/specializations/detail/kl_divergence.cuh +++ b/cpp/include/raft/distance/specializations/detail/kl_divergence.cuh @@ -18,36 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); +extern template void pairwise_matrix_instantiation_point, + int, + float, + float, + raft::identity_op>( + ops::kl_divergence_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point, + int, + double, + double, + raft::identity_op>( + ops::kl_divergence_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/l1.cuh b/cpp/include/raft/distance/specializations/detail/l1.cuh index 23261a2571..870627d909 100644 --- a/cpp/include/raft/distance/specializations/detail/l1.cuh +++ b/cpp/include/raft/distance/specializations/detail/l1.cuh @@ -18,35 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); +extern template void pairwise_matrix_instantiation_point, + int, + float, + float, + raft::identity_op>( + ops::l1_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point, + int, + double, + double, + raft::identity_op>( + ops::l1_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/l2_expanded.cuh b/cpp/include/raft/distance/specializations/detail/l2_expanded.cuh index f953018b7d..ee3207bcce 100644 --- a/cpp/include/raft/distance/specializations/detail/l2_expanded.cuh +++ b/cpp/include/raft/distance/specializations/detail/l2_expanded.cuh @@ -18,36 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); +extern template void pairwise_matrix_instantiation_point, + int, + float, + float, + raft::identity_op>( + ops::l2_exp_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::l2_exp_distance_op, + int, + double, + double, + raft::identity_op>(ops::l2_exp_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/l2_sqrt_expanded.cuh b/cpp/include/raft/distance/specializations/detail/l2_sqrt_expanded.cuh deleted file mode 100644 index 9f5f6a3706..0000000000 --- a/cpp/include/raft/distance/specializations/detail/l2_sqrt_expanded.cuh +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. - * - * 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. - */ - -#pragma once - -#include - -namespace raft { -namespace distance { -namespace detail { -extern template void -distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); - -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); - -} // namespace detail -} // namespace distance -} // namespace raft diff --git a/cpp/include/raft/distance/specializations/detail/l2_sqrt_unexpanded.cuh b/cpp/include/raft/distance/specializations/detail/l2_sqrt_unexpanded.cuh deleted file mode 100644 index 94531ddc33..0000000000 --- a/cpp/include/raft/distance/specializations/detail/l2_sqrt_unexpanded.cuh +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. - * - * 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. - */ - -#pragma once - -#include - -namespace raft { -namespace distance { -namespace detail { -extern template void -distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); - -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); - -} // namespace detail -} // namespace distance -} // namespace raft diff --git a/cpp/include/raft/distance/specializations/detail/l2_unexpanded.cuh b/cpp/include/raft/distance/specializations/detail/l2_unexpanded.cuh index 224b21fce8..1fbf57632b 100644 --- a/cpp/include/raft/distance/specializations/detail/l2_unexpanded.cuh +++ b/cpp/include/raft/distance/specializations/detail/l2_unexpanded.cuh @@ -18,36 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); +extern template void pairwise_matrix_instantiation_point< + ops::l2_unexp_distance_op, + int, + float, + float, + raft::identity_op>(ops::l2_unexp_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::l2_unexp_distance_op, + int, + double, + double, + raft::identity_op>(ops::l2_unexp_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/l_inf.cuh b/cpp/include/raft/distance/specializations/detail/l_inf.cuh index 9a46d7b488..388d3bf439 100644 --- a/cpp/include/raft/distance/specializations/detail/l_inf.cuh +++ b/cpp/include/raft/distance/specializations/detail/l_inf.cuh @@ -18,35 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); +extern template void pairwise_matrix_instantiation_point, + int, + float, + float, + raft::identity_op>( + ops::l_inf_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::l_inf_distance_op, + int, + double, + double, + raft::identity_op>(ops::l_inf_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/lp_unexpanded.cuh b/cpp/include/raft/distance/specializations/detail/lp_unexpanded.cuh index e05ef02c42..d8e86ce6f2 100644 --- a/cpp/include/raft/distance/specializations/detail/lp_unexpanded.cuh +++ b/cpp/include/raft/distance/specializations/detail/lp_unexpanded.cuh @@ -18,36 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); +extern template void pairwise_matrix_instantiation_point< + ops::lp_unexp_distance_op, + int, + float, + float, + raft::identity_op>(ops::lp_unexp_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::lp_unexp_distance_op, + int, + double, + double, + raft::identity_op>(ops::lp_unexp_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/detail/russel_rao.cuh b/cpp/include/raft/distance/specializations/detail/russel_rao.cuh index afc87997c0..4803fb8ab0 100644 --- a/cpp/include/raft/distance/specializations/detail/russel_rao.cuh +++ b/cpp/include/raft/distance/specializations/detail/russel_rao.cuh @@ -18,37 +18,23 @@ #include -namespace raft { -namespace distance { -namespace detail { -extern template void -distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -extern template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); +extern template void pairwise_matrix_instantiation_point< + ops::russel_rao_distance_op, + int, + float, + float, + raft::identity_op>(ops::russel_rao_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +extern template void pairwise_matrix_instantiation_point< + ops::russel_rao_distance_op, + int, + double, + double, + raft::identity_op>(ops::russel_rao_distance_op, + pairwise_matrix_params, + cudaStream_t); +} // namespace raft::distance::detail diff --git a/cpp/include/raft/distance/specializations/distance.cuh b/cpp/include/raft/distance/specializations/distance.cuh index 8daa398b49..a34f696e9e 100644 --- a/cpp/include/raft/distance/specializations/distance.cuh +++ b/cpp/include/raft/distance/specializations/distance.cuh @@ -27,8 +27,6 @@ #include #include #include -#include -#include #include #include #include diff --git a/cpp/include/raft/spatial/knn/detail/fused_l2_knn.cuh b/cpp/include/raft/spatial/knn/detail/fused_l2_knn.cuh index 4e18a210d4..4a571c1447 100644 --- a/cpp/include/raft/spatial/knn/detail/fused_l2_knn.cuh +++ b/cpp/include/raft/spatial/knn/detail/fused_l2_knn.cuh @@ -22,6 +22,8 @@ #include "processing.cuh" #include #include +#include +#include #include #include @@ -183,13 +185,11 @@ DI void updateSortedWarpQ( } } -template Pair; @@ -222,295 +222,279 @@ __global__ __launch_bounds__(Policy::Nthreads, 2) void fusedL2kNN(const DataT* x using namespace raft::neighbors::detail::faiss_select; typedef WarpSelect, NumWarpQ, NumThreadQ, 32> myWarpSelect; - auto rowEpilog_lambda = [m, n, numOfNN, out_dists, out_inds, mutexes] __device__( - IdxT gridStrideY) { - if (gridDim.x == 1) { return; } - - Pair* shDumpKV = nullptr; - if (useNorms) { - shDumpKV = (Pair*)(&smem[Policy::SmemSize + ((Policy::Mblk + Policy::Nblk) * sizeof(DataT))]); - } else { - shDumpKV = (Pair*)(&smem[Policy::SmemSize]); - } - - const int lid = threadIdx.x % warpSize; - const IdxT starty = gridStrideY + (threadIdx.x / Policy::AccThCols); - - // 0 -> consumer done consuming the buffer. - // -1 -> consumer started consuming the buffer - // -2 -> producer done filling the buffer - // 1 -> prod acquired to fill the buffer - if (blockIdx.x == 0) { - auto cta_processed = 0; - myWarpSelect heapArr1(identity, keyMax, numOfNN); - myWarpSelect heapArr2(identity, keyMax, numOfNN); - myWarpSelect* heapArr[] = {&heapArr1, &heapArr2}; - __syncwarp(); - - loadAllWarpQShmem(heapArr, &shDumpKV[0], m, numOfNN); - - while (cta_processed < gridDim.x - 1) { - if (threadIdx.x == 0) { - while (atomicCAS((int*)&mutexes[gridStrideY / Policy::Mblk], -2, -1) != -2) - ; - } - __threadfence(); - __syncthreads(); + auto rowEpilog_lambda = + [m, n, &distance_op, numOfNN, out_dists, out_inds, mutexes] __device__(IdxT gridStrideY) { + if (gridDim.x == 1) { return; } + + // Use ::template to disambiguate (See: + // https://en.cppreference.com/w/cpp/language/dependent_name) + int smem_offset = OpT::template shared_mem_size(); + Pair* shDumpKV = (Pair*)(&smem[smem_offset]); + + const int lid = threadIdx.x % warpSize; + const IdxT starty = gridStrideY + (threadIdx.x / Policy::AccThCols); + + // 0 -> consumer done consuming the buffer. + // -1 -> consumer started consuming the buffer + // -2 -> producer done filling the buffer + // 1 -> prod acquired to fill the buffer + if (blockIdx.x == 0) { + auto cta_processed = 0; + myWarpSelect heapArr1(identity, keyMax, numOfNN); + myWarpSelect heapArr2(identity, keyMax, numOfNN); + myWarpSelect* heapArr[] = {&heapArr1, &heapArr2}; + __syncwarp(); + + loadAllWarpQShmem(heapArr, &shDumpKV[0], m, numOfNN); + + while (cta_processed < gridDim.x - 1) { + if (threadIdx.x == 0) { + while (atomicCAS((int*)&mutexes[gridStrideY / Policy::Mblk], -2, -1) != -2) + ; + } + __threadfence(); + __syncthreads(); #pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { - const auto rowId = starty + i * Policy::AccThRows; - if (rowId < m) { + for (int i = 0; i < Policy::AccRowsPerTh; ++i) { + const auto rowId = starty + i * Policy::AccThRows; + if (rowId < m) { #pragma unroll - for (int j = 0; j < myWarpSelect::kNumWarpQRegisters; ++j) { - Pair otherKV; - otherKV.value = identity; - otherKV.key = keyMax; - const auto idx = j * warpSize + lid; - if (idx < numOfNN) { - otherKV.value = out_dists[rowId * numOfNN + idx]; - otherKV.key = (uint32_t)out_inds[rowId * numOfNN + idx]; - const auto shMemRowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; - shDumpKV[shMemRowId * numOfNN + idx] = otherKV; + for (int j = 0; j < myWarpSelect::kNumWarpQRegisters; ++j) { + Pair otherKV; + otherKV.value = identity; + otherKV.key = keyMax; + const auto idx = j * warpSize + lid; + if (idx < numOfNN) { + otherKV.value = out_dists[rowId * numOfNN + idx]; + otherKV.key = (uint32_t)out_inds[rowId * numOfNN + idx]; + const auto shMemRowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; + shDumpKV[shMemRowId * numOfNN + idx] = otherKV; + } } } } - } - __threadfence(); - __syncthreads(); + __threadfence(); + __syncthreads(); - if (threadIdx.x == 0) { atomicExch((int*)&mutexes[gridStrideY / Policy::Mblk], 0); } - __threadfence(); + if (threadIdx.x == 0) { atomicExch((int*)&mutexes[gridStrideY / Policy::Mblk], 0); } + __threadfence(); // Perform merging of otherKV with topk's across warp. #pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { - const auto rowId = starty + i * Policy::AccThRows; - if (rowId < m) { + for (int i = 0; i < Policy::AccRowsPerTh; ++i) { + const auto rowId = starty + i * Policy::AccThRows; + if (rowId < m) { #pragma unroll - for (int j = 0; j < myWarpSelect::kNumWarpQRegisters; ++j) { - Pair otherKV; - otherKV.value = identity; - otherKV.key = keyMax; - const auto idx = j * warpSize + lid; - if (idx < numOfNN) { - const auto shMemRowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; - otherKV = shDumpKV[shMemRowId * numOfNN + idx]; + for (int j = 0; j < myWarpSelect::kNumWarpQRegisters; ++j) { + Pair otherKV; + otherKV.value = identity; + otherKV.key = keyMax; + const auto idx = j * warpSize + lid; + if (idx < numOfNN) { + const auto shMemRowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; + otherKV = shDumpKV[shMemRowId * numOfNN + idx]; + } + heapArr[i]->add(otherKV.value, otherKV.key); } - heapArr[i]->add(otherKV.value, otherKV.key); } } + cta_processed++; } - cta_processed++; - } #pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { - const auto rowId = starty + i * Policy::AccThRows; - if (rowId < m) { - bool needSort = (heapArr[i]->numVals > 0); - needSort = __any_sync(0xffffffff, needSort); - if (needSort) { heapArr[i]->reduce(); } + for (int i = 0; i < Policy::AccRowsPerTh; ++i) { + const auto rowId = starty + i * Policy::AccThRows; + if (rowId < m) { + bool needSort = (heapArr[i]->numVals > 0); + needSort = __any_sync(0xffffffff, needSort); + if (needSort) { heapArr[i]->reduce(); } + } } - } - storeWarpQGmem(heapArr, out_dists, out_inds, m, numOfNN, starty); - } else { - if (threadIdx.x == 0) { - while (atomicCAS((int*)&mutexes[gridStrideY / Policy::Mblk], 0, 1) != 0) - ; - } - __threadfence(); - __syncthreads(); + storeWarpQGmem(heapArr, out_dists, out_inds, m, numOfNN, starty); + } else { + if (threadIdx.x == 0) { + while (atomicCAS((int*)&mutexes[gridStrideY / Policy::Mblk], 0, 1) != 0) + ; + } + __threadfence(); + __syncthreads(); #pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { - const auto rowId = starty + i * Policy::AccThRows; - if (rowId < m) { - for (int idx = lid; idx < numOfNN; idx += warpSize) { - const auto shMemRowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; - Pair KVPair = shDumpKV[shMemRowId * numOfNN + idx]; - out_dists[rowId * numOfNN + idx] = KVPair.value; - out_inds[rowId * numOfNN + idx] = (IdxT)KVPair.key; + for (int i = 0; i < Policy::AccRowsPerTh; ++i) { + const auto rowId = starty + i * Policy::AccThRows; + if (rowId < m) { + for (int idx = lid; idx < numOfNN; idx += warpSize) { + const auto shMemRowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; + Pair KVPair = shDumpKV[shMemRowId * numOfNN + idx]; + out_dists[rowId * numOfNN + idx] = KVPair.value; + out_inds[rowId * numOfNN + idx] = (IdxT)KVPair.key; + } } } - } - __threadfence(); - __syncthreads(); - - if (threadIdx.x == 0) { atomicExch((int*)&mutexes[gridStrideY / Policy::Mblk], -2); } - __threadfence(); - } - }; + __threadfence(); + __syncthreads(); - // epilogue operation lambda for final value calculation - auto epilog_lambda = [numOfNN, m, n, ldd, out_dists, out_inds, keyMax, identity] __device__( - AccT acc[Policy::AccRowsPerTh][Policy::AccColsPerTh], - DataT * regxn, - DataT * regyn, - IdxT gridStrideX, - IdxT gridStrideY) { - if (useNorms) { -#pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { -#pragma unroll - for (int j = 0; j < Policy::AccColsPerTh; ++j) { - acc[i][j] = regxn[i] + regyn[j] - (DataT)2.0 * acc[i][j]; - } + if (threadIdx.x == 0) { atomicExch((int*)&mutexes[gridStrideY / Policy::Mblk], -2); } + __threadfence(); } - } + }; - Pair* shDumpKV = nullptr; - if (useNorms) { - constexpr size_t shmemSize = - Policy::SmemSize + ((Policy::Mblk + Policy::Nblk) * sizeof(DataT)); - shDumpKV = (Pair*)(&smem[shmemSize]); - } else { - shDumpKV = (Pair*)(&smem[Policy::SmemSize]); - } + // epilogue operation lambda for final value calculation + auto epilog_lambda = + [&distance_op, numOfNN, m, n, ldd, out_dists, out_inds, keyMax, identity] __device__( + AccT acc[Policy::AccRowsPerTh][Policy::AccColsPerTh], + DataT * regxn, + DataT * regyn, + IdxT gridStrideX, + IdxT gridStrideY) { + // Use ::template to disambiguate (See: + // https://en.cppreference.com/w/cpp/language/dependent_name) + int smem_offset = OpT::template shared_mem_size(); + Pair* shDumpKV = (Pair*)(&smem[smem_offset]); + + constexpr uint32_t mask = 0xffffffffu; + const IdxT starty = gridStrideY + (threadIdx.x / Policy::AccThCols); + const IdxT startx = gridStrideX + (threadIdx.x % Policy::AccThCols); + const int lid = raft::laneId(); - constexpr uint32_t mask = 0xffffffffu; - const IdxT starty = gridStrideY + (threadIdx.x / Policy::AccThCols); - const IdxT startx = gridStrideX + (threadIdx.x % Policy::AccThCols); - const int lid = raft::laneId(); - - myWarpSelect heapArr1(identity, keyMax, numOfNN); - myWarpSelect heapArr2(identity, keyMax, numOfNN); - myWarpSelect* heapArr[] = {&heapArr1, &heapArr2}; - if (usePrevTopKs) { - if (gridStrideX == blockIdx.x * Policy::Nblk) { - loadPrevTopKsGmemWarpQ(heapArr, out_dists, out_inds, m, numOfNN, starty); + myWarpSelect heapArr1(identity, keyMax, numOfNN); + myWarpSelect heapArr2(identity, keyMax, numOfNN); + myWarpSelect* heapArr[] = {&heapArr1, &heapArr2}; + if (usePrevTopKs) { + if (gridStrideX == blockIdx.x * Policy::Nblk) { + loadPrevTopKsGmemWarpQ(heapArr, out_dists, out_inds, m, numOfNN, starty); + } } - } - if (gridStrideX > blockIdx.x * Policy::Nblk) { + if (gridStrideX > blockIdx.x * Policy::Nblk) { #pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { - const auto rowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; - Pair tempKV = shDumpKV[(rowId * numOfNN) + numOfNN - 1]; - heapArr[i]->warpKTop = tempKV.value; - } + for (int i = 0; i < Policy::AccRowsPerTh; ++i) { + const auto rowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; + Pair tempKV = shDumpKV[(rowId * numOfNN) + numOfNN - 1]; + heapArr[i]->warpKTop = tempKV.value; + } - // total vals can atmost be 256, (32*8) - int numValsWarpTopK[Policy::AccRowsPerTh]; - int anyWarpTopKs = 0; + // total vals can atmost be 256, (32*8) + int numValsWarpTopK[Policy::AccRowsPerTh]; + int anyWarpTopKs = 0; #pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { - const auto rowId = starty + i * Policy::AccThRows; - numValsWarpTopK[i] = 0; - if (rowId < m) { + for (int i = 0; i < Policy::AccRowsPerTh; ++i) { + const auto rowId = starty + i * Policy::AccThRows; + numValsWarpTopK[i] = 0; + if (rowId < m) { #pragma unroll - for (int j = 0; j < Policy::AccColsPerTh; ++j) { - const auto colId = startx + j * Policy::AccThCols; - if (colId < ldd) { - if (acc[i][j] < heapArr[i]->warpKTop) { numValsWarpTopK[i]++; } + for (int j = 0; j < Policy::AccColsPerTh; ++j) { + const auto colId = startx + j * Policy::AccThCols; + if (colId < ldd) { + if (acc[i][j] < heapArr[i]->warpKTop) { numValsWarpTopK[i]++; } + } } + anyWarpTopKs += numValsWarpTopK[i]; } - anyWarpTopKs += numValsWarpTopK[i]; } - } - anyWarpTopKs = __syncthreads_or(anyWarpTopKs > 0); - if (anyWarpTopKs) { - Pair* allWarpTopKs = (Pair*)(&smem[0]); - uint32_t needScanSort[Policy::AccRowsPerTh]; + anyWarpTopKs = __syncthreads_or(anyWarpTopKs > 0); + if (anyWarpTopKs) { + Pair* allWarpTopKs = (Pair*)(&smem[0]); + uint32_t needScanSort[Policy::AccRowsPerTh]; #pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { - const auto gmemRowId = starty + i * Policy::AccThRows; - needScanSort[i] = 0; - if (gmemRowId < m) { - int myVals = numValsWarpTopK[i]; - needScanSort[i] = __ballot_sync(mask, myVals > 0); - if (needScanSort[i]) { + for (int i = 0; i < Policy::AccRowsPerTh; ++i) { + const auto gmemRowId = starty + i * Policy::AccThRows; + needScanSort[i] = 0; + if (gmemRowId < m) { + int myVals = numValsWarpTopK[i]; + needScanSort[i] = __ballot_sync(mask, myVals > 0); + if (needScanSort[i]) { #pragma unroll - for (unsigned int k = 1; k <= 16; k *= 2) { - const unsigned int n = __shfl_up_sync(mask, numValsWarpTopK[i], k); - if (lid >= k) { numValsWarpTopK[i] += n; } + for (unsigned int k = 1; k <= 16; k *= 2) { + const unsigned int n = __shfl_up_sync(mask, numValsWarpTopK[i], k); + if (lid >= k) { numValsWarpTopK[i] += n; } + } } + // As each thread will know its total vals to write. + // we only store its starting location. + numValsWarpTopK[i] -= myVals; } - // As each thread will know its total vals to write. - // we only store its starting location. - numValsWarpTopK[i] -= myVals; - } - if (needScanSort[i]) { - const auto rowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; - if (gmemRowId < m) { - if (needScanSort[i] & ((uint32_t)1 << lid)) { + if (needScanSort[i]) { + const auto rowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; + if (gmemRowId < m) { + if (needScanSort[i] & ((uint32_t)1 << lid)) { #pragma unroll - for (int j = 0; j < Policy::AccColsPerTh; ++j) { - const auto colId = startx + j * Policy::AccThCols; - if (colId < ldd) { - if (acc[i][j] < heapArr[i]->warpKTop) { - Pair otherKV = {colId, acc[i][j]}; - allWarpTopKs[rowId * (256) + numValsWarpTopK[i]] = otherKV; - numValsWarpTopK[i]++; + for (int j = 0; j < Policy::AccColsPerTh; ++j) { + const auto colId = startx + j * Policy::AccThCols; + if (colId < ldd) { + if (acc[i][j] < heapArr[i]->warpKTop) { + Pair otherKV = {colId, acc[i][j]}; + allWarpTopKs[rowId * (256) + numValsWarpTopK[i]] = otherKV; + numValsWarpTopK[i]++; + } } } } + __syncwarp(); + const int finalNumVals = raft::shfl(numValsWarpTopK[i], 31); + loadWarpQShmem(heapArr[i], &shDumpKV[0], rowId, numOfNN); + updateSortedWarpQ( + heapArr[i], &allWarpTopKs[0], rowId, finalNumVals); } - __syncwarp(); - const int finalNumVals = raft::shfl(numValsWarpTopK[i], 31); - loadWarpQShmem(heapArr[i], &shDumpKV[0], rowId, numOfNN); - updateSortedWarpQ( - heapArr[i], &allWarpTopKs[0], rowId, finalNumVals); } } - } - __syncthreads(); + __syncthreads(); #pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { - if (needScanSort[i]) { - const auto rowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; - const auto gmemRowId = starty + i * Policy::AccThRows; - if (gmemRowId < m) { - storeWarpQShmem(heapArr[i], shDumpKV, rowId, numOfNN); + for (int i = 0; i < Policy::AccRowsPerTh; ++i) { + if (needScanSort[i]) { + const auto rowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; + const auto gmemRowId = starty + i * Policy::AccThRows; + if (gmemRowId < m) { + storeWarpQShmem(heapArr[i], shDumpKV, rowId, numOfNN); + } } } } - } - } else { + } else { #pragma unroll - for (int i = 0; i < Policy::AccRowsPerTh; ++i) { - const auto gmemRowId = starty + i * Policy::AccThRows; - const auto shMemRowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; - if (gmemRowId < m) { + for (int i = 0; i < Policy::AccRowsPerTh; ++i) { + const auto gmemRowId = starty + i * Policy::AccThRows; + const auto shMemRowId = (threadIdx.x / Policy::AccThCols) + i * Policy::AccThRows; + if (gmemRowId < m) { #pragma unroll - for (int j = 0; j < Policy::AccColsPerTh; ++j) { - const auto colId = startx + j * Policy::AccThCols; - Pair otherKV = {keyMax, identity}; - if (colId < ldd) { - otherKV.value = acc[i][j]; - otherKV.key = colId; + for (int j = 0; j < Policy::AccColsPerTh; ++j) { + const auto colId = startx + j * Policy::AccThCols; + Pair otherKV = {keyMax, identity}; + if (colId < ldd) { + otherKV.value = acc[i][j]; + otherKV.key = colId; + } + heapArr[i]->add(otherKV.value, otherKV.key); } - heapArr[i]->add(otherKV.value, otherKV.key); - } - bool needSort = (heapArr[i]->numVals > 0); - needSort = __any_sync(mask, needSort); - if (needSort) { heapArr[i]->reduce(); } - storeWarpQShmem(heapArr[i], shDumpKV, shMemRowId, numOfNN); + bool needSort = (heapArr[i]->numVals > 0); + needSort = __any_sync(mask, needSort); + if (needSort) { heapArr[i]->reduce(); } + storeWarpQShmem(heapArr[i], shDumpKV, shMemRowId, numOfNN); + } } } - } - if (((gridStrideX + Policy::Nblk * gridDim.x) >= n) && gridDim.x == 1) { - // This is last iteration of grid stride X - loadAllWarpQShmem(heapArr, &shDumpKV[0], m, numOfNN); - storeWarpQGmem(heapArr, out_dists, out_inds, m, numOfNN, starty); - } - }; + if (((gridStrideX + Policy::Nblk * gridDim.x) >= n) && gridDim.x == 1) { + // This is last iteration of grid stride X + loadAllWarpQShmem(heapArr, &shDumpKV[0], m, numOfNN); + storeWarpQGmem(heapArr, out_dists, out_inds, m, numOfNN, starty); + } + }; - raft::distance::detail::PairwiseDistances + write_out> obj(x, y, m, @@ -521,9 +505,9 @@ __global__ __launch_bounds__(Policy::Nthreads, 2) void fusedL2kNN(const DataT* x ldd, _xn, _yn, - nullptr, + nullptr, // output ptr, can be null as write_out == false. smem, - core_op, + distance_op, epilog_lambda, fin_op, rowEpilog_lambda); @@ -562,38 +546,32 @@ void fusedL2UnexpKnnImpl(const DataT* x, dim3 blk(KPolicy::Nthreads); // Accumulation operation lambda - auto core_lambda = [] __device__(AccT & acc, DataT & x, DataT & y) { - const auto diff = x - y; - acc += diff * diff; - }; - typedef cub::KeyValuePair Pair; - if (isRowMajor) { - constexpr auto fusedL2UnexpKnn32RowMajor = fusedL2kNN distance_op{sqrt}; + raft::identity_op fin_op{}; + + if constexpr (isRowMajor) { + constexpr auto fusedL2UnexpKnn32RowMajor = fusedL2kNN; - constexpr auto fusedL2UnexpKnn64RowMajor = fusedL2kNN; + constexpr auto fusedL2UnexpKnn64RowMajor = fusedL2kNN; + isRowMajor>; auto fusedL2UnexpKnnRowMajor = fusedL2UnexpKnn32RowMajor; if (numOfNN <= 32) { @@ -604,8 +582,10 @@ void fusedL2UnexpKnnImpl(const DataT* x, ASSERT(numOfNN <= 64, "fusedL2kNN: num of nearest neighbors must be <= 64"); } - const auto sharedMemSize = KPolicy::SmemSize + (KPolicy::Mblk * numOfNN * sizeof(Pair)); - dim3 grid = raft::distance::detail::launchConfigGenerator( + const auto sharedMemSize = + distance_op.template shared_mem_size() + KPolicy::Mblk * numOfNN * sizeof(Pair); + + dim3 grid = raft::distance::detail::launchConfigGenerator( m, n, sharedMemSize, fusedL2UnexpKnnRowMajor); if (grid.x > 1) { @@ -628,9 +608,8 @@ void fusedL2UnexpKnnImpl(const DataT* x, lda, ldb, ldd, - core_lambda, - raft::identity_op{}, - sqrt, + distance_op, + fin_op, (uint32_t)numOfNN, (int*)workspace, out_dists, @@ -753,36 +732,33 @@ void fusedL2ExpKnnImpl(const DataT* x, ASSERT(workspace != nullptr, "workspace is null"); dim3 blk(KPolicy::Nthreads); - // Accumulation operation lambda - auto core_lambda = [] __device__(AccT & acc, DataT & x, DataT & y) { acc += x * y; }; typedef cub::KeyValuePair Pair; - if (isRowMajor) { - constexpr auto fusedL2ExpKnn32RowMajor = fusedL2kNN distance_op{sqrt}; + raft::identity_op fin_op{}; + + if constexpr (isRowMajor) { + constexpr auto fusedL2ExpKnn32RowMajor = fusedL2kNN; - constexpr auto fusedL2ExpKnn64RowMajor = fusedL2kNN; + constexpr auto fusedL2ExpKnn64RowMajor = fusedL2kNN; + isRowMajor>; auto fusedL2ExpKnnRowMajor = fusedL2ExpKnn32RowMajor; if (numOfNN <= 32) { @@ -793,9 +769,8 @@ void fusedL2ExpKnnImpl(const DataT* x, ASSERT(numOfNN <= 64, "fusedL2kNN: num of nearest neighbors must be <= 64"); } - const auto sharedMemSize = KPolicy::SmemSize + - ((KPolicy::Mblk + KPolicy::Nblk) * sizeof(DataT)) + - (KPolicy::Mblk * numOfNN * sizeof(Pair)); + const auto sharedMemSize = + distance_op.template shared_mem_size() + (KPolicy::Mblk * numOfNN * sizeof(Pair)); dim3 grid = raft::distance::detail::launchConfigGenerator( m, n, sharedMemSize, fusedL2ExpKnnRowMajor); int32_t* mutexes = nullptr; @@ -835,9 +810,8 @@ void fusedL2ExpKnnImpl(const DataT* x, lda, ldb, ldd, - core_lambda, - raft::identity_op{}, - sqrt, + distance_op, + fin_op, (uint32_t)numOfNN, mutexes, out_dists, diff --git a/cpp/include/raft/util/arch.cuh b/cpp/include/raft/util/arch.cuh index 8c48b87269..dc35b10063 100644 --- a/cpp/include/raft/util/arch.cuh +++ b/cpp/include/raft/util/arch.cuh @@ -15,25 +15,27 @@ */ #pragma once -namespace raft::arch { +#include // RAFT_CUDA_TRY -/* raft::arch provides the following facilities: +namespace raft::util::arch { + +/* raft::util::arch provides the following facilities: * - * - raft::arch::SM_XX : hardcoded compile-time constants for various compute - * architectures. The values raft::arch::SM_min and raft::arch::SM_future + * - raft::util::arch::SM_XX : hardcoded compile-time constants for various compute + * architectures. The values raft::util::arch::SM_min and raft::util::arch::SM_future * represent architectures that are always smaller and larger (respectively) * than any architecture that can be encountered in practice. * - * - raft::arch::SM_compute_arch : a compile-time value for the *current* + * - raft::util::arch::SM_compute_arch : a compile-time value for the *current* * compute architecture that a kernel is compiled with. It can only be used * inside kernels with a template argument. * - * - raft::arch::kernel_runtime_arch : a function that computes at *run-time* + * - raft::util::arch::kernel_runtime_arch : a function that computes at *run-time* * which version of a kernel will launch (i.e., it will return the compute * architecture of the version of the kernel that will be launched by the * driver). * - * - raft::arch::SM_range : a compile-time value to represent an open interval + * - raft::util::arch::SM_range : a compile-time value to represent an open interval * of compute architectures. This can be used to check if the current * compile-time architecture is in a specified compatibility range. */ @@ -46,9 +48,6 @@ struct SM_generic { public: __host__ __device__ constexpr int value() const { return n; } }; - -// A dummy kernel that is used to determine the runtime architecture. -__global__ inline void dummy_runtime_kernel() {} } // namespace detail // A list of architectures that RAPIDS explicitly builds for (SM60, ..., SM90) @@ -119,7 +118,7 @@ struct SM_runtime { inline SM_runtime kernel_runtime_arch(void* kernel) { cudaFuncAttributes attributes; - cudaFuncGetAttributes(&attributes, kernel); + RAFT_CUDA_TRY(cudaFuncGetAttributes(&attributes, kernel)); return SM_runtime(10 * attributes.ptxVersion); } @@ -143,4 +142,4 @@ struct SM_range { } }; -} // namespace raft::arch +} // namespace raft::util::arch diff --git a/cpp/include/raft/util/cuda_dev_essentials.cuh b/cpp/include/raft/util/cuda_dev_essentials.cuh new file mode 100644 index 0000000000..5080dc33ee --- /dev/null +++ b/cpp/include/raft/util/cuda_dev_essentials.cuh @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ + +#pragma once + +// This file provides a few essential functions for use in __device__ code. The +// scope is necessarily limited to ensure that compilation times are minimized. +// Please make sure not to include large / expensive files from here. + +namespace raft { + +/** helper macro for device inlined functions */ +#define DI inline __device__ +#define HDI inline __host__ __device__ +#define HD __host__ __device__ + +/** + * @brief Provide a ceiling division operation ie. ceil(a / b) + * @tparam IntType supposed to be only integers for now! + */ +template +constexpr HDI IntType ceildiv(IntType a, IntType b) +{ + return (a + b - 1) / b; +} + +/** + * @brief Provide an alignment function ie. ceil(a / b) * b + * @tparam IntType supposed to be only integers for now! + */ +template +constexpr HDI IntType alignTo(IntType a, IntType b) +{ + return ceildiv(a, b) * b; +} + +/** + * @brief Provide an alignment function ie. (a / b) * b + * @tparam IntType supposed to be only integers for now! + */ +template +constexpr HDI IntType alignDown(IntType a, IntType b) +{ + return (a / b) * b; +} + +/** + * @brief Check if the input is a power of 2 + * @tparam IntType data type (checked only for integers) + */ +template +constexpr HDI bool isPo2(IntType num) +{ + return (num && !(num & (num - 1))); +} + +/** + * @brief Give logarithm of the number to base-2 + * @tparam IntType data type (checked only for integers) + */ +template +constexpr HDI IntType log2(IntType num, IntType ret = IntType(0)) +{ + return num <= IntType(1) ? ret : log2(num >> IntType(1), ++ret); +} + +/** number of threads per warp */ +static const int WarpSize = 32; + +/** get the laneId of the current thread */ +DI int laneId() +{ + int id; + asm("mov.s32 %0, %%laneid;" : "=r"(id)); + return id; +} + +} // namespace raft diff --git a/cpp/include/raft/util/cuda_rt_essentials.hpp b/cpp/include/raft/util/cuda_rt_essentials.hpp new file mode 100644 index 0000000000..e5f3af4e61 --- /dev/null +++ b/cpp/include/raft/util/cuda_rt_essentials.hpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ + +#pragma once + +// This file provides a few essential functions that wrap the CUDA runtime API. +// The scope is necessarily limited to ensure that compilation times are +// minimized. Please make sure not to include large / expensive files from here. + +#include +#include + +namespace raft { + +/** + * @brief Exception thrown when a CUDA error is encountered. + */ +struct cuda_error : public raft::exception { + explicit cuda_error(char const* const message) : raft::exception(message) {} + explicit cuda_error(std::string const& message) : raft::exception(message) {} +}; + +} // namespace raft + +/** + * @brief Error checking macro for CUDA runtime API functions. + * + * Invokes a CUDA runtime API function call, if the call does not return + * cudaSuccess, invokes cudaGetLastError() to clear the error and throws an + * exception detailing the CUDA error that occurred + * + */ +#define RAFT_CUDA_TRY(call) \ + do { \ + cudaError_t const status = call; \ + if (status != cudaSuccess) { \ + cudaGetLastError(); \ + std::string msg{}; \ + SET_ERROR_MSG(msg, \ + "CUDA error encountered at: ", \ + "call='%s', Reason=%s:%s", \ + #call, \ + cudaGetErrorName(status), \ + cudaGetErrorString(status)); \ + throw raft::cuda_error(msg); \ + } \ + } while (0) diff --git a/cpp/include/raft/util/cuda_utils.cuh b/cpp/include/raft/util/cuda_utils.cuh index 5be9dc999a..687a6b4651 100644 --- a/cpp/include/raft/util/cuda_utils.cuh +++ b/cpp/include/raft/util/cuda_utils.cuh @@ -23,113 +23,10 @@ #include #include #include - -#ifndef ENABLE_MEMCPY_ASYNC -// enable memcpy_async interface by default for newer GPUs -#if __CUDA_ARCH__ >= 800 -#define ENABLE_MEMCPY_ASYNC 1 -#endif -#else // ENABLE_MEMCPY_ASYNC -// disable memcpy_async for all older GPUs -#if __CUDA_ARCH__ < 800 -#define ENABLE_MEMCPY_ASYNC 0 -#endif -#endif // ENABLE_MEMCPY_ASYNC +#include namespace raft { -/** helper macro for device inlined functions */ -#define DI inline __device__ -#define HDI inline __host__ __device__ -#define HD __host__ __device__ - -/** - * @brief Provide a ceiling division operation ie. ceil(a / b) - * @tparam IntType supposed to be only integers for now! - */ -template -constexpr HDI IntType ceildiv(IntType a, IntType b) -{ - return (a + b - 1) / b; -} - -/** - * @brief Provide an alignment function ie. ceil(a / b) * b - * @tparam IntType supposed to be only integers for now! - */ -template -constexpr HDI IntType alignTo(IntType a, IntType b) -{ - return ceildiv(a, b) * b; -} - -/** - * @brief Provide an alignment function ie. (a / b) * b - * @tparam IntType supposed to be only integers for now! - */ -template -constexpr HDI IntType alignDown(IntType a, IntType b) -{ - return (a / b) * b; -} - -/** - * @brief Check if the input is a power of 2 - * @tparam IntType data type (checked only for integers) - */ -template -constexpr HDI bool isPo2(IntType num) -{ - return (num && !(num & (num - 1))); -} - -/** - * @brief Give logarithm of the number to base-2 - * @tparam IntType data type (checked only for integers) - */ -template -constexpr HDI IntType log2(IntType num, IntType ret = IntType(0)) -{ - return num <= IntType(1) ? ret : log2(num >> IntType(1), ++ret); -} - -/** Device function to apply the input lambda across threads in the grid */ -template -DI void forEach(int num, L lambda) -{ - int idx = (blockDim.x * blockIdx.x) + threadIdx.x; - const int numThreads = blockDim.x * gridDim.x; -#pragma unroll - for (int itr = 0; itr < ItemsPerThread; ++itr, idx += numThreads) { - if (idx < num) lambda(idx, itr); - } -} - -/** number of threads per warp */ -static const int WarpSize = 32; - -/** get the laneId of the current thread */ -DI int laneId() -{ - int id; - asm("mov.s32 %0, %%laneid;" : "=r"(id)); - return id; -} - -/** - * @brief Swap two values - * @tparam T the datatype of the values - * @param a first input - * @param b second input - */ -template -HDI void swapVals(T& a, T& b) -{ - T tmp = a; - a = b; - b = tmp; -} - /** Device function to have atomic add support for older archs */ template DI void myAtomicAdd(Type* address, Type val) diff --git a/cpp/include/raft/util/cudart_utils.hpp b/cpp/include/raft/util/cudart_utils.hpp index 0feb188ad8..0a7ca23028 100644 --- a/cpp/include/raft/util/cudart_utils.hpp +++ b/cpp/include/raft/util/cudart_utils.hpp @@ -25,6 +25,7 @@ #pragma once #include +#include #include #include #include @@ -40,42 +41,7 @@ #include #include #include - -namespace raft { - -/** - * @brief Exception thrown when a CUDA error is encountered. - */ -struct cuda_error : public raft::exception { - explicit cuda_error(char const* const message) : raft::exception(message) {} - explicit cuda_error(std::string const& message) : raft::exception(message) {} -}; - -} // namespace raft - -/** - * @brief Error checking macro for CUDA runtime API functions. - * - * Invokes a CUDA runtime API function call, if the call does not return - * cudaSuccess, invokes cudaGetLastError() to clear the error and throws an - * exception detailing the CUDA error that occurred - * - */ -#define RAFT_CUDA_TRY(call) \ - do { \ - cudaError_t const status = call; \ - if (status != cudaSuccess) { \ - cudaGetLastError(); \ - std::string msg{}; \ - SET_ERROR_MSG(msg, \ - "CUDA error encountered at: ", \ - "call='%s', Reason=%s:%s", \ - #call, \ - cudaGetErrorName(status), \ - cudaGetErrorString(status)); \ - throw raft::cuda_error(msg); \ - } \ - } while (0) +#include // FIXME: Remove after consumers rename #ifndef CUDA_TRY diff --git a/cpp/include/raft/util/device_loads_stores.cuh b/cpp/include/raft/util/device_loads_stores.cuh index 2b87c44d60..c9bda26b81 100644 --- a/cpp/include/raft/util/device_loads_stores.cuh +++ b/cpp/include/raft/util/device_loads_stores.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, NVIDIA CORPORATION. + * Copyright (c) 2021-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ #pragma once -#include +#include // uintX_t +#include // DI namespace raft { diff --git a/cpp/src/distance/specializations/detail/00_write_template.py b/cpp/src/distance/specializations/detail/00_write_template.py new file mode 100644 index 0000000000..3f2f853569 --- /dev/null +++ b/cpp/src/distance/specializations/detail/00_write_template.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +# NOTE: this template is not perfectly formatted. Use pre-commit to get +# everything in shape again. +template = """/* + * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * + * 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 // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +INCLUDE_SM_HEADERS + +namespace raft::distance::detail { + +template void pairwise_matrix_instantiation_point( + OpT, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail +""" + +data_type_instances = [ + dict( + DataT="float", + AccT="float", + OutT="float", + IdxT="int", + ), + dict( + DataT="double", + AccT="double", + OutT="double", + IdxT="int", + ), +] + +op_instances = [ + dict( + path_prefix="canberra", + OpT="ops::canberra_distance_op", + archs = [60], + ), + dict( + path_prefix="correlation", + OpT="ops::correlation_distance_op", + archs = [60], + ), + dict( + path_prefix="cosine", + OpT="ops::cosine_distance_op", + archs = [60, 80], + ), + dict( + path_prefix="hamming_unexpanded", + OpT="ops::hamming_distance_op", + archs = [60], + ), + dict( + path_prefix="hellinger_expanded", + OpT="ops::hellinger_distance_op", + archs = [60], + ), + # inner product is handled by cublas. + dict( + path_prefix="jensen_shannon", + OpT="ops::jensen_shannon_distance_op", + archs = [60], + ), + dict( + path_prefix="kl_divergence", + OpT="ops::kl_divergence_op", + archs = [60], + ), + dict( + path_prefix="l1", + OpT="ops::l1_distance_op", + archs = [60], + ), + dict( + path_prefix="l2_expanded", + OpT="ops::l2_exp_distance_op", + archs = [60, 80], + ), + dict( + path_prefix="l2_unexpanded", + OpT="ops::l2_unexp_distance_op", + archs = [60], + ), + dict( + path_prefix="l_inf", + OpT="ops::l_inf_distance_op", + archs = [60], + ), + dict( + path_prefix="lp_unexpanded", + OpT="ops::lp_unexp_distance_op", + archs = [60], + ), + dict( + path_prefix="russel_rao", + OpT="ops::russel_rao_distance_op", + archs = [60], + ), +] + +def fill_in(s, template): + for k, v in template.items(): + s = s.replace(k, v) + return s + +def fill_include_sm_headers(op_instance): + include_headers ="\n".join([ + f"#include " + for arch in op_instance["archs"] + ]) + + return { + "path_prefix": op_instance["path_prefix"], + "OpT": op_instance["OpT"], + "INCLUDE_SM_HEADERS": include_headers + } + +for op_instance in op_instances: + op_instance = fill_include_sm_headers(op_instance) + + for data_type_instance in data_type_instances: + op_data_instance = { + k : fill_in(v, data_type_instance) + for k, v in op_instance.items() + } + instance = { + **op_data_instance, + **data_type_instance, + "FinopT": "decltype(raft::identity_op())", + } + + text = fill_in(template, instance) + + path = fill_in("path_prefix_DataT_AccT_OutT_IdxT.cu", instance) + with open(path, "w") as f: + f.write(text) diff --git a/cpp/src/distance/specializations/detail/canberra_double_double_double_int.cu b/cpp/src/distance/specializations/detail/canberra_double_double_double_int.cu index 4e9e608792..037d218178 100644 --- a/cpp/src/distance/specializations/detail/canberra_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/canberra_double_double_double_int.cu @@ -14,24 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - double metric_arg); -} // namespace detail -} // namespace distance -} // namespace raft +namespace raft::distance::detail { + +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::canberra_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/canberra_float_float_float_int.cu b/cpp/src/distance/specializations/detail/canberra_float_float_float_int.cu index 6dfc385e55..0ed8ea7bb0 100644 --- a/cpp/src/distance/specializations/detail/canberra_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/canberra_float_float_float_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::canberra_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/correlation_double_double_double_int.cu b/cpp/src/distance/specializations/detail/correlation_double_double_double_int.cu index 2df77a4b5d..0c11f0621e 100644 --- a/cpp/src/distance/specializations/detail/correlation_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/correlation_double_double_double_int.cu @@ -14,27 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { +namespace raft::distance::detail { -template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::correlation_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/correlation_float_float_float_int.cu b/cpp/src/distance/specializations/detail/correlation_float_float_float_int.cu index 76ed00afa6..396e158554 100644 --- a/cpp/src/distance/specializations/detail/correlation_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/correlation_float_float_float_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::correlation_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/cosine_double_double_double_int.cu b/cpp/src/distance/specializations/detail/cosine_double_double_double_int.cu index 3e0bcb92ed..e9afb6f563 100644 --- a/cpp/src/distance/specializations/detail/cosine_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/cosine_double_double_double_int.cu @@ -14,26 +14,21 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include +#include -namespace raft { -namespace distance { -namespace detail { +namespace raft::distance::detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::cosine_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/cosine_float_float_float_int.cu b/cpp/src/distance/specializations/detail/cosine_float_float_float_int.cu index 23131ce2c7..1033c491d6 100644 --- a/cpp/src/distance/specializations/detail/cosine_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/cosine_float_float_float_int.cu @@ -14,26 +14,21 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include +#include -namespace raft { -namespace distance { -namespace detail { +namespace raft::distance::detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::cosine_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu index b618fd024c..195115914d 100644 --- a/cpp/src/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/hamming_unexpanded_double_double_double_int.cu @@ -14,27 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { +namespace raft::distance::detail { -template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::hamming_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu index 18e7aad9e9..a74c6c404e 100644 --- a/cpp/src/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/hamming_unexpanded_float_float_float_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::hamming_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu index 08ab20cfe5..bac1dd7bd0 100644 --- a/cpp/src/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/hellinger_expanded_double_double_double_int.cu @@ -14,27 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { +namespace raft::distance::detail { -template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::hellinger_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu index 79eed075fb..77c113b1a9 100644 --- a/cpp/src/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/hellinger_expanded_float_float_float_int.cu @@ -14,26 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { +namespace raft::distance::detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::hellinger_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/jensen_shannon_double_double_double_int.cu b/cpp/src/distance/specializations/detail/jensen_shannon_double_double_double_int.cu index ed84ee6dc4..188e52c152 100644 --- a/cpp/src/distance/specializations/detail/jensen_shannon_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/jensen_shannon_double_double_double_int.cu @@ -14,25 +14,21 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void + pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::jensen_shannon_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/jensen_shannon_float_float_float_int.cu b/cpp/src/distance/specializations/detail/jensen_shannon_float_float_float_int.cu index a241af767c..b0afbf7bb2 100644 --- a/cpp/src/distance/specializations/detail/jensen_shannon_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/jensen_shannon_float_float_float_int.cu @@ -14,25 +14,21 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void + pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::jensen_shannon_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/kl_divergence_double_double_double_int.cu b/cpp/src/distance/specializations/detail/kl_divergence_double_double_double_int.cu index c4c944d123..f06ae85414 100644 --- a/cpp/src/distance/specializations/detail/kl_divergence_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/kl_divergence_double_double_double_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::kl_divergence_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/kl_divergence_float_float_float_int.cu b/cpp/src/distance/specializations/detail/kl_divergence_float_float_float_int.cu index aa1db5a837..00d5a5ee5b 100644 --- a/cpp/src/distance/specializations/detail/kl_divergence_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/kl_divergence_float_float_float_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::kl_divergence_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/l1_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l1_double_double_double_int.cu index 391a1c2aa4..5c235316da 100644 --- a/cpp/src/distance/specializations/detail/l1_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/l1_double_double_double_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::l1_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/l1_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l1_float_float_float_int.cu index 7b45e52ca1..fb293ca83d 100644 --- a/cpp/src/distance/specializations/detail/l1_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/l1_float_float_float_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::l1_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/l2_expanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l2_expanded_double_double_double_int.cu index 8c5f746fa2..2c02f0224f 100644 --- a/cpp/src/distance/specializations/detail/l2_expanded_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/l2_expanded_double_double_double_int.cu @@ -14,24 +14,21 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); -} // namespace detail -} // namespace distance -} // namespace raft +namespace raft::distance::detail { + +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::l2_exp_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/l2_expanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l2_expanded_float_float_float_int.cu index c266125f98..85e25a25ca 100644 --- a/cpp/src/distance/specializations/detail/l2_expanded_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/l2_expanded_float_float_float_int.cu @@ -14,25 +14,21 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::l2_exp_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu deleted file mode 100644 index 399b120527..0000000000 --- a/cpp/src/distance/specializations/detail/l2_sqrt_expanded_double_double_double_int.cu +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. - * - * 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 -#include - -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); - -} // namespace detail -} // namespace distance -} // namespace raft diff --git a/cpp/src/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu deleted file mode 100644 index 66de212b8e..0000000000 --- a/cpp/src/distance/specializations/detail/l2_sqrt_expanded_float_float_float_int.cu +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. - * - * 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 -#include - -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); - -} // namespace detail -} // namespace distance -} // namespace raft diff --git a/cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu deleted file mode 100644 index 562d93b2de..0000000000 --- a/cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_double_double_double_int.cu +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. - * - * 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 -#include - -namespace raft { -namespace distance { -namespace detail { - -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); -} // namespace detail -} // namespace distance -} // namespace raft diff --git a/cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu deleted file mode 100644 index 386bbafc5f..0000000000 --- a/cpp/src/distance/specializations/detail/l2_sqrt_unexpanded_float_float_float_int.cu +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. - * - * 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 -#include - -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); - -} // namespace detail -} // namespace distance -} // namespace raft diff --git a/cpp/src/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu index 7733c3af48..5b4d995d14 100644 --- a/cpp/src/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/l2_unexpanded_double_double_double_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::l2_unexp_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu index 4ea18d31de..a63c3f0bb8 100644 --- a/cpp/src/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/l2_unexpanded_float_float_float_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::l2_unexp_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/l_inf_double_double_double_int.cu b/cpp/src/distance/specializations/detail/l_inf_double_double_double_int.cu index 74414f8fd6..831167523f 100644 --- a/cpp/src/distance/specializations/detail/l_inf_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/l_inf_double_double_double_int.cu @@ -14,26 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { +namespace raft::distance::detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::l_inf_distance_op, + pairwise_matrix_params, + cudaStream_t); -} // namespace detail -} // namespace distance -} // namespace raft +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/l_inf_float_float_float_int.cu b/cpp/src/distance/specializations/detail/l_inf_float_float_float_int.cu index e418fc455f..02e667cbe3 100644 --- a/cpp/src/distance/specializations/detail/l_inf_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/l_inf_float_float_float_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::l_inf_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu b/cpp/src/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu index 402cb51b7e..ebd71065ec 100644 --- a/cpp/src/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/lp_unexpanded_double_double_double_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { +namespace raft::distance::detail { -template void distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::lp_unexp_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu b/cpp/src/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu index 7efe2b3349..b94a81fdce 100644 --- a/cpp/src/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/lp_unexpanded_float_float_float_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::lp_unexp_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/russel_rao_double_double_double_int.cu b/cpp/src/distance/specializations/detail/russel_rao_double_double_double_int.cu index b1e6f5e1f4..6f952fcc37 100644 --- a/cpp/src/distance/specializations/detail/russel_rao_double_double_double_int.cu +++ b/cpp/src/distance/specializations/detail/russel_rao_double_double_double_int.cu @@ -14,26 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void -distance( - raft::resources const& handle, - const double* x, - const double* y, - double* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - double metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + double, + double, + decltype(raft::identity_op())>( + ops::russel_rao_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/src/distance/specializations/detail/russel_rao_float_float_float_int.cu b/cpp/src/distance/specializations/detail/russel_rao_float_float_float_int.cu index 1e12bcd705..3223ce33a7 100644 --- a/cpp/src/distance/specializations/detail/russel_rao_float_float_float_int.cu +++ b/cpp/src/distance/specializations/detail/russel_rao_float_float_float_int.cu @@ -14,25 +14,20 @@ * limitations under the License. */ -#include -#include +#include // raft::identity_op +#include // ops::* +#include // pairwise_matrix_instantiation_point +#include -namespace raft { -namespace distance { -namespace detail { -template void distance( - raft::resources const& handle, - const float* x, - const float* y, - float* dist, - int m, - int n, - int k, - void* workspace, - std::size_t worksize, - bool isRowMajor, - float metric_arg); +namespace raft::distance::detail { -} // namespace detail -} // namespace distance -} // namespace raft +template void pairwise_matrix_instantiation_point, + int, + float, + float, + decltype(raft::identity_op())>( + ops::russel_rao_distance_op, + pairwise_matrix_params, + cudaStream_t); + +} // namespace raft::distance::detail diff --git a/cpp/test/distance/distance_base.cuh b/cpp/test/distance/distance_base.cuh index 0e084f2ad8..438e212fbd 100644 --- a/cpp/test/distance/distance_base.cuh +++ b/cpp/test/distance/distance_base.cuh @@ -16,16 +16,24 @@ #include "../test_utils.cuh" #include -#include -#include -#include -#include -#include -#include +#include // common::nvtx::range + +#include // make_device_matrix_view +#include // raft::device_resources +#include // raft::sqrt +#include // raft::distance::DistanceType +#include +#include // rmm::device_uvector + +// When the distance library is precompiled, include only the raft_runtime +// headers. This way, a small change in one of the kernel internals does not +// trigger a rebuild of the test files (it of course still triggers a rebuild of +// the raft specializations) #if defined RAFT_COMPILED -#include +#include +#else +#include #endif -#include namespace raft { namespace distance { @@ -409,6 +417,25 @@ template return os; } +// TODO: Remove when mdspan-based raft::runtime::distance::pairwise_distance is +// implemented. +// +// Context: +// https://github.com/rapidsai/raft/issues/1338 +template +constexpr bool layout_to_row_major(); + +template <> +constexpr bool layout_to_row_major() +{ + return true; +} +template <> +constexpr bool layout_to_row_major() +{ + return false; +} + template void distanceLauncher(raft::device_resources const& handle, DataType* x, @@ -422,12 +449,23 @@ void distanceLauncher(raft::device_resources const& handle, DataType threshold, DataType metric_arg = 2.0f) { +#if defined RAFT_COMPILED + // TODO: Implement and use mdspan-based + // raft::runtime::distance::pairwise_distance here. + // + // Context: + // https://github.com/rapidsai/raft/issues/1338 + bool row_major = layout_to_row_major(); + raft::runtime::distance::pairwise_distance( + handle, x, y, dist, m, n, k, distanceType, row_major, metric_arg); +#else auto x_v = make_device_matrix_view(x, m, k); auto y_v = make_device_matrix_view(y, n, k); auto dist_v = make_device_matrix_view(dist, m, n); raft::distance::distance( handle, x_v, y_v, dist_v, metric_arg); +#endif } template @@ -523,9 +561,25 @@ class BigMatrixDistanceTest : public ::testing::Test { auto testInfo = testing::UnitTest::GetInstance()->current_test_info(); common::nvtx::range fun_scope("test::%s/%s", testInfo->test_suite_name(), testInfo->name()); + void pairwise_distance(raft::device_resources const& handle, + float* x, + float* y, + float* dists, + int m, + int n, + int k, + raft::distance::DistanceType metric, + bool isRowMajor, + float metric_arg); + constexpr bool row_major = true; + constexpr float metric_arg = 0.0f; +#if defined RAFT_COMPILED + raft::runtime::distance::pairwise_distance( + handle, x.data(), x.data(), dist.data(), m, n, k, distanceType, row_major, metric_arg); +#else raft::distance::distance( - handle, x.data(), x.data(), dist.data(), m, n, k, true, 0.0f); - + handle, x.data(), x.data(), dist.data(), m, n, k, row_major, metric_arg); +#endif RAFT_CUDA_TRY(cudaStreamSynchronize(handle.get_stream())); } diff --git a/cpp/test/distance/fused_l2_nn.cu b/cpp/test/distance/fused_l2_nn.cu index 4a74d7f16a..383ad39319 100644 --- a/cpp/test/distance/fused_l2_nn.cu +++ b/cpp/test/distance/fused_l2_nn.cu @@ -182,22 +182,20 @@ class FusedL2NNTest : public ::testing::TestWithParam> { int m = params.m; int n = params.n; int k = params.k; - MinAndDistanceReduceOp redOp; - fusedL2NN, int>( - out, - x.data(), - y.data(), - xn.data(), - yn.data(), - m, - n, - k, - (void*)workspace.data(), - redOp, - raft::distance::KVPMinReduce(), - Sqrt, - true, - stream); + + const bool init_out_buffer = true; + fusedL2NNMinReduce, int>(out, + x.data(), + y.data(), + xn.data(), + yn.data(), + m, + n, + k, + (void*)workspace.data(), + Sqrt, + init_out_buffer, + stream); RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); } }; From 7c73f23ddf5f81ad2cd057c650b7e2e8947c5265 Mon Sep 17 00:00:00 2001 From: Ben Frederickson Date: Thu, 23 Mar 2023 03:29:03 -0700 Subject: [PATCH 09/25] Add nccl to dependencies.yaml (#1361) raft-dask requires nccl to build, add to the dependencies.yaml so that when creating a clean raft conda environment - we can build all of raft out of the box Authors: - Ben Frederickson (https://github.com/benfred) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) - AJ Schmidt (https://github.com/ajschmidt8) URL: https://github.com/rapidsai/raft/pull/1361 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 1 + dependencies.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 39f1fef4d5..7972a8824d 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -33,6 +33,7 @@ dependencies: - libcusolver=11.4.1.48 - libcusparse-dev=11.7.5.86 - libcusparse=11.7.5.86 +- nccl>=2.9.9 - ninja - numpydoc - pydata-sphinx-theme diff --git a/dependencies.yaml b/dependencies.yaml index 9fbf26bcd1..c06ce4a20f 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -53,6 +53,7 @@ dependencies: packages: - c-compiler - cxx-compiler + - nccl>=2.9.9 specific: - output_types: conda matrices: From 419f0c28cd18654064bfd3a6ed2f638e239f46b3 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Thu, 23 Mar 2023 09:28:16 -0400 Subject: [PATCH 10/25] Generate pyproject dependencies with dfg (#1364) This PR uses dependencies.yaml to generate the dependency lists in pyproject.toml Authors: - Vyas Ramasubramani (https://github.com/vyasr) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - AJ Schmidt (https://github.com/ajschmidt8) - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1364 --- .pre-commit-config.yaml | 2 +- .../all_cuda-118_arch-x86_64.yaml | 9 +- conda/recipes/pylibraft/meta.yaml | 1 + dependencies.yaml | 107 +++++++++++++++--- python/pylibraft/pyproject.toml | 24 ++-- python/raft-dask/pyproject.toml | 24 ++-- 6 files changed, 127 insertions(+), 40 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7606914589..630b8788f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -101,7 +101,7 @@ repos: args: ["--toml", "pyproject.toml"] exclude: (?x)^(^CHANGELOG.md$) - repo: https://github.com/rapidsai/dependency-file-generator - rev: v1.4.0 + rev: v1.5.1 hooks: - id: rapids-dependency-file-generator args: ["--clean"] diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 7972a8824d..1afebc98e6 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -18,13 +18,14 @@ dependencies: - cupy - cxx-compiler - cython>=0.29,<0.30 -- dask-cuda=23.04 +- dask-cuda==23.4.* - dask>=2023.1.1 - distributed>=2023.1.1 - doxygen>=1.8.20 - gcc_linux-64=11.* - graphviz - ipython +- joblib>=0.11 - libcublas-dev=11.11.3.6 - libcublas=11.11.3.6 - libcurand-dev=10.3.0.86 @@ -35,12 +36,14 @@ dependencies: - libcusparse=11.7.5.86 - nccl>=2.9.9 - ninja +- numba>=0.49 +- numpy>=1.21 - numpydoc - pydata-sphinx-theme - pytest - pytest-cov - recommonmark -- rmm=23.04 +- rmm==23.4.* - scikit-build>=0.13.1 - scikit-learn - scipy @@ -48,6 +51,6 @@ dependencies: - sphinx-markdown-tables - sysroot_linux-64==2.17 - ucx-proc=*=gpu -- ucx-py=0.31.* +- ucx-py==0.31.* - ucx>=1.13.0 name: all_cuda-118_arch-x86_64 diff --git a/conda/recipes/pylibraft/meta.yaml b/conda/recipes/pylibraft/meta.yaml index a528064348..7730801801 100644 --- a/conda/recipes/pylibraft/meta.yaml +++ b/conda/recipes/pylibraft/meta.yaml @@ -36,6 +36,7 @@ requirements: - cython >=0.29,<0.30 - libraft {{ version }} - libraft-headers {{ version }} + - numpy >=1.21 - python x.x - rmm ={{ minor_version }} - scikit-build >=0.13.1 diff --git a/dependencies.yaml b/dependencies.yaml index c06ce4a20f..dd361a0cdf 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -7,11 +7,14 @@ files: arch: [x86_64] includes: - build + - build_pylibraft - cudatoolkit - develop - docs - - run - - test_python + - run_raft_dask + - run_pylibraft + - test_python_common + - test_pylibraft test_cpp: output: none includes: @@ -21,7 +24,8 @@ files: includes: - cudatoolkit - py_version - - test_python + - test_python_common + - test_pylibraft checks: output: none includes: @@ -33,6 +37,54 @@ files: - cudatoolkit - docs - py_version + py_build_pylibraft: + output: pyproject + pyproject_dir: python/pylibraft + extras: + table: build-system + includes: + - build + - build_pylibraft + - build_wheels + py_run_pylibraft: + output: pyproject + pyproject_dir: python/pylibraft + extras: + table: project + includes: + - run_pylibraft + py_test_pylibraft: + output: pyproject + pyproject_dir: python/pylibraft + extras: + table: project.optional-dependencies + key: test + includes: + - test_python_common + - test_pylibraft + py_build_raft_dask: + output: pyproject + pyproject_dir: python/raft-dask + extras: + table: build-system + includes: + - build + - build_wheels + py_run_raft_dask: + output: pyproject + pyproject_dir: python/raft-dask + extras: + table: project + includes: + - run_raft_dask + py_test_raft_dask: + output: pyproject + pyproject_dir: python/raft-dask + extras: + table: project.optional-dependencies + key: test + includes: + - test_python_common channels: - rapidsai - rapidsai-nightly @@ -42,10 +94,9 @@ channels: dependencies: build: common: - - output_types: [conda, requirements] + - output_types: [conda, requirements, pyproject] packages: - cmake>=3.23.1,!=3.25.0 - - cuda-python >=11.7.1,<12.0 - cython>=0.29,<0.30 - ninja - scikit-build>=0.13.1 @@ -67,6 +118,12 @@ dependencies: packages: - gcc_linux-aarch64=11.* - sysroot_linux-aarch64==2.17 + build_pylibraft: + common: + - output_types: [conda, requirements, pyproject] + packages: + - &cuda_python cuda-python >=11.7.1,<12.0 + - &rmm rmm==23.4.* checks: common: - output_types: [conda, requirements] @@ -151,6 +208,12 @@ dependencies: - recommonmark - sphinx-copybutton - sphinx-markdown-tables + build_wheels: + common: + - output_types: pyproject + packages: + - wheel + - setuptools py_version: specific: - output_types: conda @@ -170,23 +233,41 @@ dependencies: - matrix: packages: - python>=3.8,<3.11 - run: + run_pylibraft: common: - - output_types: [conda] + - output_types: [conda, pyproject] + packages: + - &numpy numpy>=1.21 + - *cuda_python + - *rmm + run_raft_dask: + common: + - output_types: [conda, pyproject] packages: - dask>=2023.1.1 + - dask-cuda==23.4.* - distributed>=2023.1.1 + - joblib>=0.11 + - numba>=0.49 + - *numpy + - ucx-py==0.31.* + - output_types: conda + packages: - ucx>=1.13.0 - - ucx-py=0.31.* - ucx-proc=*=gpu - - rmm=23.04 - - dask-cuda=23.04 - test_python: + - output_types: pyproject + packages: + - pylibraft==23.4.* + test_python_common: common: - - output_types: [conda, requirements] + - output_types: [conda, requirements, pyproject] packages: - - cupy - pytest - pytest-cov + test_pylibraft: + common: + - output_types: [conda, requirements, pyproject] + packages: + - cupy - scikit-learn - scipy diff --git a/python/pylibraft/pyproject.toml b/python/pylibraft/pyproject.toml index 7d92fd0763..fed15bbab0 100644 --- a/python/pylibraft/pyproject.toml +++ b/python/pylibraft/pyproject.toml @@ -15,15 +15,15 @@ [build-system] requires = [ - "wheel", - "setuptools", - "cython>=0.29,<0.30", - "cuda-python>=11.7.1,<12.0", - "scikit-build>=0.13.1", "cmake>=3.23.1,!=3.25.0", + "cuda-python >=11.7.1,<12.0", + "cython>=0.29,<0.30", "ninja", "rmm==23.4.*", -] + "scikit-build>=0.13.1", + "setuptools", + "wheel", +] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. build-backend = "setuptools.build_meta" [project] @@ -37,10 +37,10 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.8" dependencies = [ - "numpy", - "cuda-python>=11.7.1,<12.0", + "cuda-python >=11.7.1,<12.0", + "numpy>=1.21", "rmm==23.4.*", -] +] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ "Intended Audience :: Developers", "Programming Language :: Python", @@ -50,10 +50,12 @@ classifiers = [ [project.optional-dependencies] test = [ + "cupy", "pytest", - "scipy", + "pytest-cov", "scikit-learn", -] + "scipy", +] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.urls] Homepage = "https://github.com/rapidsai/raft" diff --git a/python/raft-dask/pyproject.toml b/python/raft-dask/pyproject.toml index 2fe6522f57..fe490ea117 100644 --- a/python/raft-dask/pyproject.toml +++ b/python/raft-dask/pyproject.toml @@ -15,13 +15,13 @@ [build-system] requires = [ - "wheel", - "setuptools", - "cython>=0.29,<0.30", - "scikit-build>=0.13.1", "cmake>=3.23.1,!=3.25.0", + "cython>=0.29,<0.30", "ninja", -] + "scikit-build>=0.13.1", + "setuptools", + "wheel", +] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project] name = "raft-dask" @@ -34,15 +34,15 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.8" dependencies = [ - "numpy", - "numba>=0.49", - "joblib>=0.11", "dask-cuda==23.4.*", "dask>=2023.1.1", - "ucx-py==0.31.*", "distributed>=2023.1.1", + "joblib>=0.11", + "numba>=0.49", + "numpy>=1.21", "pylibraft==23.4.*", -] + "ucx-py==0.31.*", +] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ "Intended Audience :: Developers", "Programming Language :: Python", @@ -53,8 +53,8 @@ classifiers = [ [project.optional-dependencies] test = [ "pytest", - "dask[distributed,dataframe]", -] + "pytest-cov", +] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.urls] Homepage = "https://github.com/rapidsai/raft" From 31847afbaa55ead3ee99d44fcbe0c41ff8e1f726 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Thu, 23 Mar 2023 18:48:01 -0400 Subject: [PATCH 11/25] Python API for brute-force KNN (#1292) Closes #1289 Authors: - Corey J. Nolet (https://github.com/cjnolet) - Ben Frederickson (https://github.com/benfred) Approvers: - Ben Frederickson (https://github.com/benfred) URL: https://github.com/rapidsai/raft/pull/1292 --- cpp/CMakeLists.txt | 1 + .../raft_runtime/neighbors/brute_force.hpp | 19 +- .../brute_force_knn_int64_t_float.cu | 46 ++--- python/pylibraft/pylibraft/common/mdspan.pyx | 1 - .../pylibraft/neighbors/CMakeLists.txt | 2 +- .../pylibraft/pylibraft/neighbors/__init__.py | 5 +- .../pylibraft/neighbors/brute_force.pyx | 179 ++++++++++++++++++ .../pylibraft/pylibraft/neighbors/common.pyx | 12 +- .../pylibraft/neighbors/cpp/__init__.pxd | 0 .../pylibraft/neighbors/cpp/__init__.py | 14 ++ .../pylibraft/neighbors/cpp/brute_force.pxd | 55 ++++++ .../pylibraft/test/test_brue_force.py | 99 ++++++++++ .../pylibraft/pylibraft/test/test_doctests.py | 3 +- 13 files changed, 395 insertions(+), 41 deletions(-) create mode 100644 python/pylibraft/pylibraft/neighbors/brute_force.pyx create mode 100644 python/pylibraft/pylibraft/neighbors/cpp/__init__.pxd create mode 100644 python/pylibraft/pylibraft/neighbors/cpp/__init__.py create mode 100644 python/pylibraft/pylibraft/neighbors/cpp/brute_force.pxd create mode 100644 python/pylibraft/pylibraft/test/test_brue_force.py diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 034dc059b0..c1704552ec 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -304,6 +304,7 @@ if(RAFT_COMPILE_LIBRARY) # These are somehow missing a kernel definition which is causing a compile error. # src/distance/specializations/detail/kernels/rbf_kernel_double.cu # src/distance/specializations/detail/kernels/rbf_kernel_float.cu + src/neighbors/brute_force_knn_int64_t_float.cu src/distance/specializations/detail/kernels/tanh_kernel_double.cu src/distance/specializations/detail/kernels/tanh_kernel_float.cu src/distance/specializations/detail/kl_divergence_float_float_float_int.cu diff --git a/cpp/include/raft_runtime/neighbors/brute_force.hpp b/cpp/include/raft_runtime/neighbors/brute_force.hpp index 19904f4f78..12da6ff101 100644 --- a/cpp/include/raft_runtime/neighbors/brute_force.hpp +++ b/cpp/include/raft_runtime/neighbors/brute_force.hpp @@ -21,18 +21,17 @@ namespace raft::runtime::neighbors::brute_force { -#define RAFT_INST_BFKNN(IDX_T, DATA_T, MATRIX_IDX_T, INDEX_LAYOUT, SEARCH_LAYOUT) \ - void knn(raft::device_resources const& handle, \ - std::vector> index, \ - raft::device_matrix_view search, \ - raft::device_matrix_view indices, \ - raft::device_matrix_view distances, \ - int k, \ - distance::DistanceType metric = distance::DistanceType::L2Unexpanded, \ - std::optional metric_arg = std::make_optional(2.0f), \ +#define RAFT_INST_BFKNN(IDX_T, DATA_T, MATRIX_IDX_T, INDEX_LAYOUT, SEARCH_LAYOUT) \ + void knn(raft::device_resources const& handle, \ + raft::device_matrix_view index, \ + raft::device_matrix_view search, \ + raft::device_matrix_view indices, \ + raft::device_matrix_view distances, \ + distance::DistanceType metric = distance::DistanceType::L2Unexpanded, \ + std::optional metric_arg = std::make_optional(2.0f), \ std::optional global_id_offset = std::nullopt); -RAFT_INST_BFKNN(int64_t, float, uint32_t, raft::row_major, raft::row_major); +RAFT_INST_BFKNN(int64_t, float, int64_t, raft::row_major, raft::row_major); #undef RAFT_INST_BFKNN diff --git a/cpp/src/neighbors/brute_force_knn_int64_t_float.cu b/cpp/src/neighbors/brute_force_knn_int64_t_float.cu index b0411a59ce..585084fc97 100644 --- a/cpp/src/neighbors/brute_force_knn_int64_t_float.cu +++ b/cpp/src/neighbors/brute_force_knn_int64_t_float.cu @@ -14,8 +14,6 @@ * limitations under the License. */ -#pragma once - #include #include #include @@ -24,30 +22,34 @@ #include +#include + namespace raft::runtime::neighbors::brute_force { -#define RAFT_INST_BFKNN(IDX_T, DATA_T, MATRIX_IDX_T, INDEX_LAYOUT, SEARCH_LAYOUT) \ - void knn(raft::device_resources const& handle, \ - std::vector> index, \ - raft::device_matrix_view search, \ - raft::device_matrix_view indices, \ - raft::device_matrix_view distances, \ - distance::DistanceType metric = distance::DistanceType::L2Unexpanded, \ - std::optional metric_arg = std::make_optional(2.0f), \ - std::optional global_id_offset = std::nullopt) \ - { \ - raft::neighbors::brute_force::knn(handle, \ - index, \ - search, \ - indices, \ - distances, \ - static_cast(indices.extent(1)), \ - metric, \ - metric_arg, \ - global_id_offset); \ +#define RAFT_INST_BFKNN(IDX_T, DATA_T, MATRIX_IDX_T, INDEX_LAYOUT, SEARCH_LAYOUT) \ + void knn(raft::device_resources const& handle, \ + raft::device_matrix_view index, \ + raft::device_matrix_view search, \ + raft::device_matrix_view indices, \ + raft::device_matrix_view distances, \ + distance::DistanceType metric, \ + std::optional metric_arg, \ + std::optional global_id_offset) \ + { \ + std::vector> vec; \ + vec.push_back(index); \ + raft::neighbors::brute_force::knn(handle, \ + vec, \ + search, \ + indices, \ + distances, \ + static_cast(distances.extent(1)), \ + metric, \ + metric_arg, \ + global_id_offset); \ } -RAFT_INST_BFKNN(int64_t, float, uint32_t, raft::row_major, raft::row_major); +RAFT_INST_BFKNN(int64_t, float, int64_t, raft::row_major, raft::row_major); #undef RAFT_INST_BFKNN diff --git a/python/pylibraft/pylibraft/common/mdspan.pyx b/python/pylibraft/pylibraft/common/mdspan.pyx index c7b42ecab7..f35a94bb9c 100644 --- a/python/pylibraft/pylibraft/common/mdspan.pyx +++ b/python/pylibraft/pylibraft/common/mdspan.pyx @@ -159,7 +159,6 @@ cdef device_matrix_view[float, int64_t, row_major] \ return make_device_matrix_view[float, int64_t, row_major]( cai.data, shape[0], shape[1]) - cdef device_matrix_view[uint8_t, int64_t, row_major] \ get_dmv_uint8(cai, check_shape) except *: if cai.dtype != np.uint8: diff --git a/python/pylibraft/pylibraft/neighbors/CMakeLists.txt b/python/pylibraft/pylibraft/neighbors/CMakeLists.txt index 98f0d7f67a..7b9c1591c1 100644 --- a/python/pylibraft/pylibraft/neighbors/CMakeLists.txt +++ b/python/pylibraft/pylibraft/neighbors/CMakeLists.txt @@ -13,7 +13,7 @@ # ============================================================================= # Set the list of Cython files to build -set(cython_sources common.pyx refine.pyx) +set(cython_sources common.pyx refine.pyx brute_force.pyx) set(linked_libraries raft::raft raft::compiled) # Build all of the Cython targets diff --git a/python/pylibraft/pylibraft/neighbors/__init__.py b/python/pylibraft/pylibraft/neighbors/__init__.py index f7510ba2db..a50b6f21a7 100644 --- a/python/pylibraft/pylibraft/neighbors/__init__.py +++ b/python/pylibraft/pylibraft/neighbors/__init__.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # + +from pylibraft.neighbors import brute_force + from .refine import refine -__all__ = ["common", "refine"] +__all__ = ["common", "refine", "brute_force"] diff --git a/python/pylibraft/pylibraft/neighbors/brute_force.pyx b/python/pylibraft/pylibraft/neighbors/brute_force.pyx new file mode 100644 index 0000000000..dbd888756d --- /dev/null +++ b/python/pylibraft/pylibraft/neighbors/brute_force.pyx @@ -0,0 +1,179 @@ +# +# Copyright (c) 2022-2023, NVIDIA CORPORATION. +# +# 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. +# +# cython: profile=False +# distutils: language = c++ +# cython: embedsignature = True +# cython: language_level = 3 + +import numpy as np + +from cython.operator cimport dereference as deref +from libcpp cimport bool, nullptr +from libcpp.vector cimport vector + +from pylibraft.distance.distance_type cimport DistanceType + +from pylibraft.common import ( + DeviceResources, + auto_convert_output, + cai_wrapper, + device_ndarray, +) + +from libc.stdint cimport int64_t, uintptr_t + +from pylibraft.common.cpp.optional cimport optional +from pylibraft.common.handle cimport device_resources +from pylibraft.common.mdspan cimport get_dmv_float, get_dmv_int64 + +from pylibraft.common.handle import auto_sync_handle +from pylibraft.common.input_validation import is_c_contiguous +from pylibraft.common.interruptible import cuda_interruptible + +from pylibraft.distance.distance_type cimport DistanceType + +# TODO: Centralize this + +from pylibraft.distance.pairwise_distance import DISTANCE_TYPES + +from pylibraft.common.cpp.mdspan cimport ( + device_matrix_view, + host_matrix_view, + make_device_matrix_view, + make_host_matrix_view, + row_major, +) +from pylibraft.neighbors.cpp.brute_force cimport knn as c_knn + + +def _get_array_params(array_interface, check_dtype=None): + dtype = np.dtype(array_interface["typestr"]) + if check_dtype is None and dtype != check_dtype: + raise TypeError("dtype %s not supported" % dtype) + shape = array_interface["shape"] + if len(shape) != 2: + raise ValueError("Expected a 2D array, got %d D" % len(shape)) + data = array_interface["data"][0] + return (shape, dtype, data) + + +@auto_sync_handle +@auto_convert_output +def knn(dataset, queries, k=None, indices=None, distances=None, + metric="sqeuclidean", metric_arg=2.0, + global_id_offset=0, handle=None): + """ + Perform a brute-force nearest neighbors search. + + Parameters + ---------- + dataset : array interface compliant matrix, row-major layout, + shape (n_samples, dim). Supported dtype [float] + queries : array interface compliant matrix, row-major layout, + shape (n_queries, dim) Supported dtype [float] + k : int + Number of neighbors to search (k <= 2048). Optional if indices or + distances arrays are given (in which case their second dimension + is k). + indices : Optional array interface compliant matrix shape + (n_queries, k), dtype int64_t. If supplied, neighbor + indices will be written here in-place. (default None) + Supported dtype uint64 + distances : Optional array interface compliant matrix shape + (n_queries, k), dtype float. If supplied, neighbor + indices will be written here in-place. (default None) + + {handle_docstring} + + Returns + ------- + indices: array interface compliant object containing resulting indices + shape (n_queries, k) + + distances: array interface compliant object containing resulting distances + shape (n_queries, k) + + Examples + -------- + + >>> import cupy as cp + + >>> from pylibraft.common import DeviceResources + >>> from pylibraft.neighbors.brute_force import knn + + >>> n_samples = 50000 + >>> n_features = 50 + >>> n_queries = 1000 + + >>> dataset = cp.random.random_sample((n_samples, n_features), + ... dtype=cp.float32) + >>> # Search using the built index + >>> queries = cp.random.random_sample((n_queries, n_features), + ... dtype=cp.float32) + >>> k = 40 + >>> distances, neighbors = knn(dataset, queries, k) + >>> distances = cp.asarray(distances) + >>> neighbors = cp.asarray(neighbors) + """ + + if handle is None: + handle = DeviceResources() + + dataset_cai = cai_wrapper(dataset) + queries_cai = cai_wrapper(queries) + + if k is None: + if indices is not None: + k = cai_wrapper(indices).shape[1] + elif distances is not None: + k = cai_wrapper(distances).shape[1] + else: + raise ValueError("Argument k must be specified if both indices " + "and distances arg is None") + + n_queries = cai_wrapper(queries).shape[0] + + if indices is None: + indices = device_ndarray.empty((n_queries, k), dtype='int64') + + if distances is None: + distances = device_ndarray.empty((n_queries, k), dtype='float32') + + cdef DistanceType c_metric = DISTANCE_TYPES[metric] + + distances_cai = cai_wrapper(distances) + indices_cai = cai_wrapper(indices) + + cdef optional[float] c_metric_arg = metric_arg + cdef optional[int64_t] c_global_offset = global_id_offset + + cdef device_resources* handle_ = \ + handle.getHandle() + + if dataset_cai.dtype == np.float32: + with cuda_interruptible(): + c_knn(deref(handle_), + get_dmv_float(dataset_cai, check_shape=True), + get_dmv_float(queries_cai, check_shape=True), + get_dmv_int64(indices_cai, check_shape=True), + get_dmv_float(distances_cai, check_shape=True), + c_metric, + c_metric_arg, + c_global_offset) + else: + raise TypeError("dtype %s not supported" % dataset_cai.dtype) + + return (distances, indices) diff --git a/python/pylibraft/pylibraft/neighbors/common.pyx b/python/pylibraft/pylibraft/neighbors/common.pyx index a8380b589b..24c1abcf18 100644 --- a/python/pylibraft/pylibraft/neighbors/common.pyx +++ b/python/pylibraft/pylibraft/neighbors/common.pyx @@ -22,13 +22,15 @@ import warnings from pylibraft.distance.distance_type cimport DistanceType +SUPPORTED_DISTANCES = { + "sqeuclidean": DistanceType.L2Expanded, + "euclidean": DistanceType.L2SqrtExpanded, + "inner_product": DistanceType.InnerProduct, + +} + def _get_metric(metric): - SUPPORTED_DISTANCES = { - "sqeuclidean": DistanceType.L2Expanded, - "euclidean": DistanceType.L2SqrtExpanded, - "inner_product": DistanceType.InnerProduct - } if metric not in SUPPORTED_DISTANCES: if metric == "l2_expanded": warnings.warn("Using l2_expanded as a metric name is deprecated," diff --git a/python/pylibraft/pylibraft/neighbors/cpp/__init__.pxd b/python/pylibraft/pylibraft/neighbors/cpp/__init__.pxd new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/pylibraft/pylibraft/neighbors/cpp/__init__.py b/python/pylibraft/pylibraft/neighbors/cpp/__init__.py new file mode 100644 index 0000000000..a7e7b75096 --- /dev/null +++ b/python/pylibraft/pylibraft/neighbors/cpp/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2022-2023, NVIDIA CORPORATION. +# +# 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. +# diff --git a/python/pylibraft/pylibraft/neighbors/cpp/brute_force.pxd b/python/pylibraft/pylibraft/neighbors/cpp/brute_force.pxd new file mode 100644 index 0000000000..de5e0af267 --- /dev/null +++ b/python/pylibraft/pylibraft/neighbors/cpp/brute_force.pxd @@ -0,0 +1,55 @@ +# +# Copyright (c) 2023, NVIDIA CORPORATION. +# +# 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. +# +# cython: profile=False +# distutils: language = c++ +# cython: embedsignature = True +# cython: language_level = 3 + +import numpy as np + +import pylibraft.common.handle + +from cython.operator cimport dereference as deref +from libc.stdint cimport int8_t, int64_t, uint8_t, uint64_t, uintptr_t +from libcpp cimport bool, nullptr +from libcpp.string cimport string +from libcpp.vector cimport vector + +from rmm._lib.memory_resource cimport device_memory_resource + +from pylibraft.common.cpp.mdspan cimport ( + device_matrix_view, + host_matrix_view, + make_device_matrix_view, + make_host_matrix_view, + row_major, +) +from pylibraft.common.cpp.optional cimport optional +from pylibraft.common.handle cimport device_resources +from pylibraft.distance.distance_type cimport DistanceType + + +cdef extern from "raft_runtime/neighbors/brute_force.hpp" \ + namespace "raft::runtime::neighbors::brute_force" nogil: + + cdef void knn(const device_resources & handle, + device_matrix_view[float, int64_t, row_major] index, + device_matrix_view[float, int64_t, row_major] search, + device_matrix_view[int64_t, int64_t, row_major] indices, + device_matrix_view[float, int64_t, row_major] distances, + DistanceType metric, + optional[float] metric_arg, + optional[int64_t] global_id_offset) except + diff --git a/python/pylibraft/pylibraft/test/test_brue_force.py b/python/pylibraft/pylibraft/test/test_brue_force.py new file mode 100644 index 0000000000..f349be892d --- /dev/null +++ b/python/pylibraft/pylibraft/test/test_brue_force.py @@ -0,0 +1,99 @@ +# Copyright (c) 2022-2023, NVIDIA CORPORATION. +# +# 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. +# + +import numpy as np +import pytest +from scipy.spatial.distance import cdist + +from pylibraft.common import DeviceResources, Stream, device_ndarray +from pylibraft.neighbors.brute_force import knn + + +@pytest.mark.parametrize("n_index_rows", [32, 100]) +@pytest.mark.parametrize("n_query_rows", [32, 100]) +@pytest.mark.parametrize("n_cols", [40, 100]) +@pytest.mark.parametrize("k", [1, 5, 32]) +@pytest.mark.parametrize( + "metric", + [ + "euclidean", + "cityblock", + "chebyshev", + "canberra", + "correlation", + "russellrao", + "cosine", + "sqeuclidean", + # "inner_product", + ], +) +@pytest.mark.parametrize("inplace", [True, False]) +@pytest.mark.parametrize("order", ["F", "C"]) +@pytest.mark.parametrize("dtype", [np.float32]) +def test_knn( + n_index_rows, n_query_rows, n_cols, k, inplace, metric, order, dtype +): + index = np.random.random_sample((n_index_rows, n_cols)).astype(dtype) + queries = np.random.random_sample((n_query_rows, n_cols)).astype(dtype) + + # RussellRao expects boolean arrays + if metric == "russellrao": + index[index < 0.5] = 0.0 + index[index >= 0.5] = 1.0 + queries[queries < 0.5] = 0.0 + queries[queries >= 0.5] = 1.0 + + indices = np.zeros((n_query_rows, k), dtype="int64") + distances = np.zeros((n_query_rows, k), dtype=dtype) + + index_device = device_ndarray(index) + + queries_device = device_ndarray(queries) + indices_device = device_ndarray(indices) + distances_device = device_ndarray(distances) + + s2 = Stream() + handle = DeviceResources(stream=s2) + ret_distances, ret_indices = knn( + index_device, + queries_device, + k, + indices=indices_device, + distances=distances_device, + metric=metric, + handle=handle, + ) + handle.sync() + + pw_dists = cdist(queries, index, metric=metric) + + distances_device = ret_distances if not inplace else distances_device + + actual_distances = distances_device.copy_to_host() + + actual_distances[actual_distances <= 1e-5] = 0.0 + argsort = np.argsort(pw_dists, axis=1) + + for i in range(pw_dists.shape[0]): + expected_indices = argsort[i] + gpu_dists = actual_distances[i] + + if metric == "correlation" or metric == "cosine": + gpu_dists = gpu_dists[::-1] + + cpu_ordered = pw_dists[i, expected_indices] + np.testing.assert_allclose( + cpu_ordered[:k], gpu_dists, atol=1e-4, rtol=1e-4 + ) diff --git a/python/pylibraft/pylibraft/test/test_doctests.py b/python/pylibraft/pylibraft/test/test_doctests.py index 3276ca115f..34be6c55f5 100644 --- a/python/pylibraft/pylibraft/test/test_doctests.py +++ b/python/pylibraft/pylibraft/test/test_doctests.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022, NVIDIA CORPORATION. +# Copyright (c) 2022-2023, NVIDIA CORPORATION. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -96,6 +96,7 @@ def _find_doctests_in_obj(obj, finder=None, criteria=None): DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.distance)) DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors)) DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors.ivf_pq)) +DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.neighbors.brute_force)) DOC_STRINGS.extend(_find_doctests_in_obj(pylibraft.random)) From 0df5cee684cb032e81308e355852b437265a6ffd Mon Sep 17 00:00:00 2001 From: Peter Andreas Entschev Date: Thu, 23 Mar 2023 23:55:31 +0100 Subject: [PATCH 12/25] Relax UCX pin to allow 1.14 (#1366) UCX 1.14.0 was recently released and conda-forge package was updated in https://github.com/conda-forge/ucx-split-feedstock/pull/111 with several packaging improvements. Relax the pin to allow installing UCX v1.14.x as well. Authors: - Peter Andreas Entschev (https://github.com/pentschev) - AJ Schmidt (https://github.com/ajschmidt8) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - AJ Schmidt (https://github.com/ajschmidt8) URL: https://github.com/rapidsai/raft/pull/1366 --- conda/recipes/raft-dask/conda_build_config.yaml | 2 +- conda/recipes/raft-dask/meta.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conda/recipes/raft-dask/conda_build_config.yaml b/conda/recipes/raft-dask/conda_build_config.yaml index ef22522116..778b187870 100644 --- a/conda/recipes/raft-dask/conda_build_config.yaml +++ b/conda/recipes/raft-dask/conda_build_config.yaml @@ -11,7 +11,7 @@ sysroot_version: - "2.17" ucx_version: - - "1.13.0" + - ">=1.13.0,<1.15.0" ucx_py_version: - "0.31.*" diff --git a/conda/recipes/raft-dask/meta.yaml b/conda/recipes/raft-dask/meta.yaml index b387f0f47c..59a67fe148 100644 --- a/conda/recipes/raft-dask/meta.yaml +++ b/conda/recipes/raft-dask/meta.yaml @@ -54,7 +54,7 @@ requirements: - pylibraft {{ version }} - python x.x - rmm ={{ minor_version }} - - ucx >={{ ucx_version }} + - ucx {{ ucx_version }} - ucx-proc=*=gpu - ucx-py {{ ucx_py_version }} From 1b18d1fd5e143a547c94dec1fbf793bdb4685400 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Thu, 23 Mar 2023 20:28:57 -0400 Subject: [PATCH 13/25] Adding architecture diagram to README.md (#1370) I noticed some folks on Twitter asking about an architecture diagram for RAFT. I think it's a good idea to provide this in the README. Authors: - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Ben Frederickson (https://github.com/benfred) - Divye Gala (https://github.com/divyegala) URL: https://github.com/rapidsai/raft/pull/1370 --- README.md | 10 +++++++--- img/arch.png | Bin 0 -> 52500 bytes 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 img/arch.png diff --git a/README.md b/README.md index 8519ebcae1..8c6e817bf9 100755 --- a/README.md +++ b/README.md @@ -35,12 +35,16 @@ While not exhaustive, the following general categories help summarize the accele | **Tools & Utilities** | common utilities for developing CUDA applications, multi-node multi-gpu infrastructure | -All of RAFT's C++ APIs can be accessed header-only and optional pre-compiled shared libraries can 1) speed up compile times and 2) enable the APIs to be used without CUDA-enabled compilers. +RAFT is a C++ header-only template library with an optional shared library that +1) can speed up compile times for common template types, and +2) provides host-accessible "runtime" APIs, which don't require a CUDA compiler to use -In addition to the C++ library, RAFT also provides 2 Python libraries: -- `pylibraft` - lightweight low-level Python wrappers around RAFT's host-accessible "runtime" APIs. +In addition being a C++ library, RAFT also provides 2 Python libraries: +- `pylibraft` - lightweight Python wrappers around RAFT's host-accessible "runtime" APIs. - `raft-dask` - multi-node multi-GPU communicator infrastructure for building distributed algorithms on the GPU with Dask. +![RAFT is a C++ header-only template library with optional shared library and lightweight Python wrappers](img/arch.png) + ## Getting started ### RAPIDS Memory Manager (RMM) diff --git a/img/arch.png b/img/arch.png new file mode 100644 index 0000000000000000000000000000000000000000..ea9cad9204d16a1ff4587a08e53e8530c5ba0e33 GIT binary patch literal 52500 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 literal 0 HcmV?d00001 From 8f1fa073257ccf26628098bca5985238ff495a11 Mon Sep 17 00:00:00 2001 From: Yong Wang Date: Sat, 25 Mar 2023 00:21:07 +0800 Subject: [PATCH 14/25] Improve the performance of radix top-k (#1175) The main changes are: - Add a one-block version. It uses single thread block for one row of a batch and is used when `len` is relatively small (<= 16384) - Avoid writing candidates to buffers when the number of candidates is larger than buffer length. - Add a parameter to control whether to use a fused filter in the last pass or use a standalone filter kernel. The later case is preferable when the leading bits of inputs are almost same. - Early stopping: when the target bucket contains `k` values, we can stop the computation earlier - Many implementation details are polished, like the initialization of `counter`, calculation of kernel launch parameters, and the scan step - Tests and benchmarks are updated to include the new implementations. New benchmarks are added to demonstrate the advantage of adaptive version. Authors: - Yong Wang (https://github.com/yong-wang) - Corey J. Nolet (https://github.com/cjnolet) - Tamas Bela Feher (https://github.com/tfeher) Approvers: - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1175 --- cpp/bench/matrix/select_k.cu | 128 +- cpp/include/raft/matrix/detail/select_k.cuh | 2 +- .../raft/matrix/detail/select_radix.cuh | 1149 ++++++++++++----- cpp/include/raft/spatial/knn/knn.cuh | 4 +- .../raft_internal/matrix/select_k.cuh | 43 +- cpp/test/matrix/select_k.cu | 12 +- 6 files changed, 975 insertions(+), 363 deletions(-) diff --git a/cpp/bench/matrix/select_k.cu b/cpp/bench/matrix/select_k.cu index d4873e2640..870119db52 100644 --- a/cpp/bench/matrix/select_k.cu +++ b/cpp/bench/matrix/select_k.cu @@ -35,6 +35,10 @@ #include #include +#include +#include +#include + namespace raft::matrix { using namespace raft::bench; // NOLINT @@ -50,7 +54,23 @@ struct selection : public fixture { { raft::sparse::iota_fill(in_ids_.data(), IdxT(p.batch_size), IdxT(p.len), stream); raft::random::RngState state{42}; - raft::random::uniform(handle, state, in_dists_.data(), in_dists_.size(), KeyT(-1.0), KeyT(1.0)); + + KeyT min_value = -1.0; + KeyT max_value = 1.0; + if (p.use_same_leading_bits) { + if constexpr (std::is_same_v) { + uint32_t min_bits = 0x3F800000; // 1.0 + uint32_t max_bits = 0x3F8000FF; // 1.00003 + memcpy(&min_value, &min_bits, sizeof(KeyT)); + memcpy(&max_value, &max_bits, sizeof(KeyT)); + } else if constexpr (std::is_same_v) { + uint64_t min_bits = 0x3FF0000000000000; // 1.0 + uint64_t max_bits = 0x3FF0000FFFFFFFFF; // 1.000015 + memcpy(&min_value, &min_bits, sizeof(KeyT)); + memcpy(&max_value, &max_bits, sizeof(KeyT)); + } + } + raft::random::uniform(handle, state, in_dists_.data(), in_dists_.size(), min_value, max_value); } void run_benchmark(::benchmark::State& state) override // NOLINT @@ -60,6 +80,7 @@ struct selection : public fixture { try { std::ostringstream label_stream; label_stream << params_.batch_size << "#" << params_.len << "#" << params_.k; + if (params_.use_same_leading_bits) { label_stream << "#same-leading-bits"; } state.SetLabel(label_stream.str()); loop_on_state(state, [this, &handle]() { select::select_k_impl(handle, @@ -85,21 +106,55 @@ struct selection : public fixture { }; const std::vector kInputs{ - {20000, 500, 1, true}, {20000, 500, 2, true}, {20000, 500, 4, true}, - {20000, 500, 8, true}, {20000, 500, 16, true}, {20000, 500, 32, true}, - {20000, 500, 64, true}, {20000, 500, 128, true}, {20000, 500, 256, true}, - - {1000, 10000, 1, true}, {1000, 10000, 2, true}, {1000, 10000, 4, true}, - {1000, 10000, 8, true}, {1000, 10000, 16, true}, {1000, 10000, 32, true}, - {1000, 10000, 64, true}, {1000, 10000, 128, true}, {1000, 10000, 256, true}, - - {100, 100000, 1, true}, {100, 100000, 2, true}, {100, 100000, 4, true}, - {100, 100000, 8, true}, {100, 100000, 16, true}, {100, 100000, 32, true}, - {100, 100000, 64, true}, {100, 100000, 128, true}, {100, 100000, 256, true}, - - {10, 1000000, 1, true}, {10, 1000000, 2, true}, {10, 1000000, 4, true}, - {10, 1000000, 8, true}, {10, 1000000, 16, true}, {10, 1000000, 32, true}, - {10, 1000000, 64, true}, {10, 1000000, 128, true}, {10, 1000000, 256, true}, + {20000, 500, 1, true}, + {20000, 500, 2, true}, + {20000, 500, 4, true}, + {20000, 500, 8, true}, + {20000, 500, 16, true}, + {20000, 500, 32, true}, + {20000, 500, 64, true}, + {20000, 500, 128, true}, + {20000, 500, 256, true}, + + {1000, 10000, 1, true}, + {1000, 10000, 2, true}, + {1000, 10000, 4, true}, + {1000, 10000, 8, true}, + {1000, 10000, 16, true}, + {1000, 10000, 32, true}, + {1000, 10000, 64, true}, + {1000, 10000, 128, true}, + {1000, 10000, 256, true}, + + {100, 100000, 1, true}, + {100, 100000, 2, true}, + {100, 100000, 4, true}, + {100, 100000, 8, true}, + {100, 100000, 16, true}, + {100, 100000, 32, true}, + {100, 100000, 64, true}, + {100, 100000, 128, true}, + {100, 100000, 256, true}, + + {10, 1000000, 1, true}, + {10, 1000000, 2, true}, + {10, 1000000, 4, true}, + {10, 1000000, 8, true}, + {10, 1000000, 16, true}, + {10, 1000000, 32, true}, + {10, 1000000, 64, true}, + {10, 1000000, 128, true}, + {10, 1000000, 256, true}, + + {10, 1000000, 1, true, false, true}, + {10, 1000000, 2, true, false, true}, + {10, 1000000, 4, true, false, true}, + {10, 1000000, 8, true, false, true}, + {10, 1000000, 16, true, false, true}, + {10, 1000000, 32, true, false, true}, + {10, 1000000, 64, true, false, true}, + {10, 1000000, 128, true, false, true}, + {10, 1000000, 256, true, false, true}, }; #define SELECTION_REGISTER(KeyT, IdxT, A) \ @@ -109,24 +164,27 @@ const std::vector kInputs{ RAFT_BENCH_REGISTER(SelectK, #KeyT "/" #IdxT "/" #A, kInputs); \ } -SELECTION_REGISTER(float, uint32_t, kPublicApi); // NOLINT -SELECTION_REGISTER(float, uint32_t, kRadix8bits); // NOLINT -SELECTION_REGISTER(float, uint32_t, kRadix11bits); // NOLINT -SELECTION_REGISTER(float, uint32_t, kWarpAuto); // NOLINT -SELECTION_REGISTER(float, uint32_t, kWarpImmediate); // NOLINT -SELECTION_REGISTER(float, uint32_t, kWarpFiltered); // NOLINT -SELECTION_REGISTER(float, uint32_t, kWarpDistributed); // NOLINT -SELECTION_REGISTER(float, uint32_t, kWarpDistributedShm); // NOLINT - -SELECTION_REGISTER(double, uint32_t, kRadix8bits); // NOLINT -SELECTION_REGISTER(double, uint32_t, kRadix11bits); // NOLINT -SELECTION_REGISTER(double, uint32_t, kWarpAuto); // NOLINT - -SELECTION_REGISTER(double, int64_t, kRadix8bits); // NOLINT -SELECTION_REGISTER(double, int64_t, kRadix11bits); // NOLINT -SELECTION_REGISTER(double, int64_t, kWarpImmediate); // NOLINT -SELECTION_REGISTER(double, int64_t, kWarpFiltered); // NOLINT -SELECTION_REGISTER(double, int64_t, kWarpDistributed); // NOLINT -SELECTION_REGISTER(double, int64_t, kWarpDistributedShm); // NOLINT +SELECTION_REGISTER(float, uint32_t, kPublicApi); // NOLINT +SELECTION_REGISTER(float, uint32_t, kRadix8bits); // NOLINT +SELECTION_REGISTER(float, uint32_t, kRadix11bits); // NOLINT +SELECTION_REGISTER(float, uint32_t, kRadix11bitsExtraPass); // NOLINT +SELECTION_REGISTER(float, uint32_t, kWarpAuto); // NOLINT +SELECTION_REGISTER(float, uint32_t, kWarpImmediate); // NOLINT +SELECTION_REGISTER(float, uint32_t, kWarpFiltered); // NOLINT +SELECTION_REGISTER(float, uint32_t, kWarpDistributed); // NOLINT +SELECTION_REGISTER(float, uint32_t, kWarpDistributedShm); // NOLINT + +SELECTION_REGISTER(double, uint32_t, kRadix8bits); // NOLINT +SELECTION_REGISTER(double, uint32_t, kRadix11bits); // NOLINT +SELECTION_REGISTER(double, uint32_t, kRadix11bitsExtraPass); // NOLINT +SELECTION_REGISTER(double, uint32_t, kWarpAuto); // NOLINT + +SELECTION_REGISTER(double, int64_t, kRadix8bits); // NOLINT +SELECTION_REGISTER(double, int64_t, kRadix11bits); // NOLINT +SELECTION_REGISTER(double, int64_t, kRadix11bitsExtraPass); // NOLINT +SELECTION_REGISTER(double, int64_t, kWarpImmediate); // NOLINT +SELECTION_REGISTER(double, int64_t, kWarpFiltered); // NOLINT +SELECTION_REGISTER(double, int64_t, kWarpDistributed); // NOLINT +SELECTION_REGISTER(double, int64_t, kWarpDistributedShm); // NOLINT } // namespace raft::matrix diff --git a/cpp/include/raft/matrix/detail/select_k.cuh b/cpp/include/raft/matrix/detail/select_k.cuh index ac1ba3dfa3..20c2fb119d 100644 --- a/cpp/include/raft/matrix/detail/select_k.cuh +++ b/cpp/include/raft/matrix/detail/select_k.cuh @@ -84,7 +84,7 @@ void select_k(const T* in_val, in_val, in_idx, batch_size, len, k, out_val, out_idx, select_min, stream, mr); } else { select::radix::select_k= 4 ? 11 : 8), 512>( - in_val, in_idx, batch_size, len, k, out_val, out_idx, select_min, stream, mr); + in_val, in_idx, batch_size, len, k, out_val, out_idx, select_min, true, stream, mr); } } diff --git a/cpp/include/raft/matrix/detail/select_radix.cuh b/cpp/include/raft/matrix/detail/select_radix.cuh index 643a63d9db..7ac40ac0eb 100644 --- a/cpp/include/raft/matrix/detail/select_radix.cuh +++ b/cpp/include/raft/matrix/detail/select_radix.cuh @@ -16,11 +16,11 @@ #pragma once -#include - -#include #include #include +#include +#include +#include #include #include #include @@ -35,8 +35,8 @@ #include namespace raft::matrix::detail::select::radix { +namespace impl { -constexpr int ITEM_PER_THREAD = 32; constexpr int VECTORIZED_READ_SIZE = 16; template @@ -51,13 +51,6 @@ _RAFT_HOST_DEVICE constexpr int calc_num_passes() return ceildiv(sizeof(T) * 8, BitsPerPass); } -// Minimum reasonable block size for the given radix size. -template -_RAFT_HOST_DEVICE constexpr int calc_min_block_size() -{ - return 1 << std::max(BitsPerPass - 4, Pow2::Log2 + 1); -} - /** * Bit 0 is the least significant (rightmost); * this implementation processes input from the most to the least significant bit. @@ -82,23 +75,43 @@ _RAFT_DEVICE constexpr unsigned calc_mask(int pass) } /** - * Use cub to twiddle bits - so that we can correctly compare bits of floating-point values as well + * Use CUB to twiddle bits - so that we can correctly compare bits of floating-point values as well * as of integers. */ template -_RAFT_DEVICE typename cub::Traits::UnsignedBits twiddle_in(T key, bool greater) +_RAFT_DEVICE typename cub::Traits::UnsignedBits twiddle_in(T key, bool select_min) { auto bits = reinterpret_cast::UnsignedBits&>(key); bits = cub::Traits::TwiddleIn(bits); - if (greater) { bits = ~bits; } + if (!select_min) { bits = ~bits; } return bits; } +template +_RAFT_DEVICE T twiddle_out(typename cub::Traits::UnsignedBits bits, bool select_min) +{ + if (!select_min) { bits = ~bits; } + bits = cub::Traits::TwiddleOut(bits); + return reinterpret_cast(bits); +} + template -_RAFT_DEVICE int calc_bucket(T x, int start_bit, unsigned mask, bool greater) +_RAFT_DEVICE int calc_bucket(T x, int start_bit, unsigned mask, bool select_min) +{ + static_assert(BitsPerPass <= sizeof(int) * 8 - 1, + "BitsPerPass is too large that the result type could not be int"); + return (twiddle_in(x, select_min) >> start_bit) & mask; +} + +template +_RAFT_HOST_DEVICE IdxT calc_buf_len(IdxT len) { - static_assert(BitsPerPass <= sizeof(int) * 8 - 1); // so return type can be int - return (twiddle_in(x, greater) >> start_bit) & mask; + // When writing is skipped, only read `in`(type T). + // When writing is not skipped, read `in_buf`(T) and `in_idx_buf`(IdxT), and write `out_buf`(T) + // and `out_idx_buf`(IdxT). + // The ratio between these cases determines whether to skip writing and hence the buffer size. + constexpr float ratio = 2 + sizeof(IdxT) * 2.0 / sizeof(T); + return len / ratio; } /** @@ -111,17 +124,18 @@ _RAFT_DEVICE int calc_bucket(T x, int start_bit, unsigned mask, bool greater) * @tparam IdxT indexing type * @tparam Func void (T x, IdxT idx) * + * @param thread_rank rank of the calling thread among all participating threads + * @param num_threads number of the threads that participate in processing * @param in the input data * @param len the number of elements to read * @param f the lambda taking two arguments (T x, IdxT idx) */ template -_RAFT_DEVICE void vectorized_process(const T* in, IdxT len, Func f) +_RAFT_DEVICE void vectorized_process( + size_t thread_rank, size_t num_threads, const T* in, IdxT len, Func f) { - const IdxT stride = blockDim.x * gridDim.x; - const int tid = blockIdx.x * blockDim.x + threadIdx.x; if constexpr (sizeof(T) >= VECTORIZED_READ_SIZE || VECTORIZED_READ_SIZE % sizeof(T) != 0) { - for (IdxT i = tid; i < len; i += stride) { + for (IdxT i = thread_rank; i < len; i += num_threads) { f(in[i], i); } } else { @@ -134,8 +148,8 @@ _RAFT_DEVICE void vectorized_process(const T* in, IdxT len, Func f) const IdxT skip_cnt_left = std::min((IdxT)(align_bytes::roundUp(in) - in), len); // The main loop: process all aligned data - for (IdxT i = tid * wide_t::Ratio + skip_cnt_left; i + wide_t::Ratio <= len; - i += stride * wide_t::Ratio) { + for (IdxT i = thread_rank * wide_t::Ratio + skip_cnt_left; i + wide_t::Ratio <= len; + i += num_threads * wide_t::Ratio) { wide.load(in, i); #pragma unroll for (int j = 0; j < wide_t::Ratio; ++j) { @@ -145,30 +159,55 @@ _RAFT_DEVICE void vectorized_process(const T* in, IdxT len, Func f) static_assert(WarpSize >= wide_t::Ratio); // Processes the skipped elements on the left - if (tid < skip_cnt_left) { f(in[tid], tid); } + if (thread_rank < skip_cnt_left) { f(in[thread_rank], thread_rank); } // Processes the skipped elements on the right const IdxT skip_cnt_right = align_elems::mod(len - skip_cnt_left); - const IdxT remain_i = len - skip_cnt_right + tid; + const IdxT remain_i = len - skip_cnt_right + thread_rank; if (remain_i < len) { f(in[remain_i], remain_i); } } } template -struct Counter { +struct alignas(128) Counter { + // We are processing the values in multiple passes, from most significant to least significant. In + // each pass, we keep the length of input (`len`) and the `k` of current pass, and update them at + // the end of the pass. IdxT k; IdxT len; + + // `previous_len` is the length of input in previous pass. Note that `previous_len` rather + // than `len` is used for the filtering step because filtering is indeed for previous pass (see + // comments before `radix_kernel`). IdxT previous_len; - int bucket; - IdxT filter_cnt; - unsigned int finished_block_cnt; - IdxT out_cnt; - IdxT out_back_cnt; + // We determine the bits of the k_th value inside the mask processed by the pass. The + // already known bits are stored in `kth_value_bits`. It's used to discriminate a element is a + // result (written to `out`), a candidate for next pass (written to `out_buf`), or not useful + // (discarded). The bits that are not yet processed do not matter for this purpose. + typename cub::Traits::UnsignedBits kth_value_bits; + + // Record how many elements have passed filtering. It's used to determine the position in the + // `out_buf` where an element should be written. + alignas(128) IdxT filter_cnt; + + // For a row inside a batch, we may launch multiple thread blocks. This counter is used to + // determine if the current block is the last running block. If so, this block will execute scan() + // and choose_bucket(). + alignas(128) unsigned int finished_block_cnt; + + // Record how many elements have been written to the front of `out`. Elements less (if + // select_min==true) than the k-th value are written from front to back. + alignas(128) IdxT out_cnt; + + // Record how many elements have been written to the back of `out`. Elements equal to the k-th + // value are written from back to front. We need to keep count of them separately because the + // number of elements that <= the k-th value might exceed k. + alignas(128) IdxT out_back_cnt; }; /** - * Fused filtering of the current phase and building histogram for the next phase - * (see steps 4-1 in `radix_kernel` description). + * Fused filtering of the current pass and building histogram for the next pass + * (see steps 4 & 1 in `radix_kernel` description). */ template _RAFT_DEVICE void filter_and_histogram(const T* in_buf, @@ -177,12 +216,12 @@ _RAFT_DEVICE void filter_and_histogram(const T* in_buf, IdxT* out_idx_buf, T* out, IdxT* out_idx, - IdxT len, + IdxT previous_len, Counter* counter, IdxT* histogram, - bool greater, + bool select_min, int pass, - int k) + bool early_stop) { constexpr int num_buckets = calc_num_buckets(); __shared__ IdxT histogram_smem[num_buckets]; @@ -198,19 +237,20 @@ _RAFT_DEVICE void filter_and_histogram(const T* in_buf, // Passed to vectorized_process, this function executes in all blocks in parallel, // i.e. the work is split along the input (both, in batches and chunks of a single row). // Later, the histograms are merged using atomicAdd. - auto f = [greater, start_bit, mask](T value, IdxT) { - int bucket = calc_bucket(value, start_bit, mask, greater); - atomicAdd(histogram_smem + bucket, IdxT(1)); + auto f = [select_min, start_bit, mask](T value, IdxT) { + int bucket = calc_bucket(value, start_bit, mask, select_min); + atomicAdd(histogram_smem + bucket, static_cast(1)); }; - vectorized_process(in_buf, len, f); + vectorized_process(static_cast(blockIdx.x) * blockDim.x + threadIdx.x, + static_cast(blockDim.x) * gridDim.x, + in_buf, + previous_len, + f); } else { - const IdxT previous_len = counter->previous_len; - const int want_bucket = counter->bucket; - IdxT& filter_cnt = counter->filter_cnt; - IdxT& out_cnt = counter->out_cnt; - const IdxT counter_len = counter->len; + IdxT* p_filter_cnt = &counter->filter_cnt; + IdxT* p_out_cnt = &counter->out_cnt; + const auto kth_value_bits = counter->kth_value_bits; const int previous_start_bit = calc_start_bit(pass - 1); - const unsigned previous_mask = calc_mask(pass - 1); // See the remark above on the distributed execution of `f` using vectorized_process. auto f = [in_idx_buf, @@ -218,38 +258,50 @@ _RAFT_DEVICE void filter_and_histogram(const T* in_buf, out_idx_buf, out, out_idx, - greater, - k, + select_min, start_bit, mask, previous_start_bit, - previous_mask, - want_bucket, - &filter_cnt, - &out_cnt, - counter_len](T value, IdxT i) { - int prev_bucket = - calc_bucket(value, previous_start_bit, previous_mask, greater); - if (prev_bucket == want_bucket) { - IdxT pos = atomicAdd(&filter_cnt, IdxT(1)); - out_buf[pos] = value; - if (out_idx_buf) { out_idx_buf[pos] = in_idx_buf ? in_idx_buf[i] : i; } - int bucket = calc_bucket(value, start_bit, mask, greater); - atomicAdd(histogram_smem + bucket, IdxT(1)); - - if (counter_len == 1) { - out[k - 1] = value; - out_idx[k - 1] = in_idx_buf ? in_idx_buf[i] : i; + kth_value_bits, + p_filter_cnt, + p_out_cnt, + early_stop](T value, IdxT i) { + const auto previous_bits = (twiddle_in(value, select_min) >> previous_start_bit) + << previous_start_bit; + if (previous_bits == kth_value_bits) { + if (early_stop) { + IdxT pos = atomicAdd(p_out_cnt, static_cast(1)); + out[pos] = value; + out_idx[pos] = in_idx_buf ? in_idx_buf[i] : i; + } else { + if (out_buf) { + IdxT pos = atomicAdd(p_filter_cnt, static_cast(1)); + out_buf[pos] = value; + out_idx_buf[pos] = in_idx_buf ? in_idx_buf[i] : i; + } + + int bucket = calc_bucket(value, start_bit, mask, select_min); + atomicAdd(histogram_smem + bucket, static_cast(1)); } - } else if (prev_bucket < want_bucket) { - IdxT pos = atomicAdd(&out_cnt, IdxT(1)); + } + // the condition `(out_buf || early_stop)` is a little tricky: + // If we skip writing to `out_buf` (when `out_buf` is nullptr), we should skip writing to + // `out` too. So we won't write the same value to `out` multiple times in different passes. + // And if we keep skipping the writing, values will be written in `last_filter_kernel()` at + // last. But when `early_stop` is true, we need to write to `out` since it's the last chance. + else if ((out_buf || early_stop) && previous_bits < kth_value_bits) { + IdxT pos = atomicAdd(p_out_cnt, static_cast(1)); out[pos] = value; out_idx[pos] = in_idx_buf ? in_idx_buf[i] : i; } }; - - vectorized_process(in_buf, previous_len, f); + vectorized_process(static_cast(blockIdx.x) * blockDim.x + threadIdx.x, + static_cast(blockDim.x) * gridDim.x, + in_buf, + previous_len, + f); } + if (early_stop) { return; } __syncthreads(); // merge histograms produced by individual blocks @@ -259,69 +311,184 @@ _RAFT_DEVICE void filter_and_histogram(const T* in_buf, } /** - * Replace a part of the histogram with its own prefix sum, starting from the `start` and adding - * `current` to each entry of the result. + * Replace histogram with its own prefix sum * (step 2 in `radix_kernel` description) */ template -_RAFT_DEVICE void scan(volatile IdxT* histogram, - const int start, - const int num_buckets, - const IdxT current) +_RAFT_DEVICE void scan(volatile IdxT* histogram) { - typedef cub::BlockScan BlockScan; - __shared__ typename BlockScan::TempStorage temp_storage; + constexpr int num_buckets = calc_num_buckets(); + if constexpr (num_buckets >= BlockSize) { + static_assert(num_buckets % BlockSize == 0); + constexpr int items_per_thread = num_buckets / BlockSize; + typedef cub::BlockLoad BlockLoad; + typedef cub::BlockStore + BlockStore; + typedef cub::BlockScan BlockScan; - IdxT thread_data = 0; - int index = start + threadIdx.x; - if (index < num_buckets) { thread_data = histogram[index]; } + __shared__ union { + typename BlockLoad::TempStorage load; + typename BlockScan::TempStorage scan; + typename BlockStore::TempStorage store; + } temp_storage; + IdxT thread_data[items_per_thread]; - BlockScan(temp_storage).InclusiveSum(thread_data, thread_data); - __syncthreads(); - if (index < num_buckets) { histogram[index] = thread_data + current; } - __syncthreads(); // This sync is necessary, as the content of histogram needs - // to be read after + BlockLoad(temp_storage.load).Load(histogram, thread_data); + __syncthreads(); + + BlockScan(temp_storage.scan).InclusiveSum(thread_data, thread_data); + __syncthreads(); + + BlockStore(temp_storage.store).Store(histogram, thread_data); + } else { + typedef cub::BlockScan BlockScan; + __shared__ typename BlockScan::TempStorage temp_storage; + + IdxT thread_data = 0; + if (threadIdx.x < num_buckets) { thread_data = histogram[threadIdx.x]; } + + BlockScan(temp_storage).InclusiveSum(thread_data, thread_data); + __syncthreads(); + + if (threadIdx.x < num_buckets) { histogram[threadIdx.x] = thread_data; } + } } /** * Calculate in which bucket the k-th value will fall - * (steps 2-3 in `radix_kernel` description) + * (steps 3 in `radix_kernel` description) */ -template -_RAFT_DEVICE void choose_bucket(Counter* counter, IdxT* histogram, const IdxT k) +template +_RAFT_DEVICE void choose_bucket(Counter* counter, + const IdxT* histogram, + const IdxT k, + const int pass) { constexpr int num_buckets = calc_num_buckets(); - int index = threadIdx.x; - IdxT last_prefix_sum = 0; - int num_pass = 1; - if constexpr (num_buckets >= BlockSize) { - static_assert(num_buckets % BlockSize == 0); - num_pass = num_buckets / BlockSize; + for (int i = threadIdx.x; i < num_buckets; i += blockDim.x) { + IdxT prev = (i == 0) ? 0 : histogram[i - 1]; + IdxT cur = histogram[i]; + + // one and only one thread will satisfy this condition, so counter is written by only one thread + if (prev < k && cur >= k) { + counter->k = k - prev; // how many values still are there to find + counter->len = cur - prev; // number of values in next pass + typename cub::Traits::UnsignedBits bucket = i; + int start_bit = calc_start_bit(pass); + counter->kth_value_bits |= bucket << start_bit; + } } +} - for (int i = 0; i < num_pass && (last_prefix_sum < k); i++) { - // Turn the i-th chunk of the histogram into its prefix sum. - scan(histogram, i * BlockSize, num_buckets, last_prefix_sum); - if (index < num_buckets) { - // Number of values in the previous `index-1` buckets (see the `scan` op above) - IdxT prev = (index == 0) ? 0 : histogram[index - 1]; - // Number of values in `index` buckets - IdxT cur = histogram[index]; - - // one and only one thread will satisfy this condition, so only write once - if (prev < k && cur >= k) { - counter->k = k - prev; // how many values still are there to find - counter->previous_len = counter->len; - counter->len = cur - prev; // number of values in `index` bucket - counter->bucket = index; +// For one-block version, last_filter() could be called when pass < num_passes - 1. +// So `pass` could not be constexpr +template +_RAFT_DEVICE void last_filter(const T* in_buf, + const IdxT* in_idx_buf, + T* out, + IdxT* out_idx, + IdxT current_len, + IdxT k, + Counter* counter, + const bool select_min, + const int pass) +{ + const auto kth_value_bits = counter->kth_value_bits; + const int start_bit = calc_start_bit(pass); + + // changed in choose_bucket(); need to reload + const IdxT needed_num_of_kth = counter->k; + IdxT* p_out_cnt = &counter->out_cnt; + IdxT* p_out_back_cnt = &counter->out_back_cnt; + for (IdxT i = threadIdx.x; i < current_len; i += blockDim.x) { + const T value = in_buf[i]; + const auto bits = (twiddle_in(value, select_min) >> start_bit) << start_bit; + if (bits < kth_value_bits) { + IdxT pos = atomicAdd(p_out_cnt, static_cast(1)); + out[pos] = value; + // For one-block version, `in_idx_buf` could be nullptr at pass 0. + // For non one-block version, if writing has been skipped, `in_idx_buf` could be nullptr if + // `in_buf` is `in` + out_idx[pos] = in_idx_buf ? in_idx_buf[i] : i; + } else if (bits == kth_value_bits) { + IdxT back_pos = atomicAdd(p_out_back_cnt, static_cast(1)); + if (back_pos < needed_num_of_kth) { + IdxT pos = k - 1 - back_pos; + out[pos] = value; + out_idx[pos] = in_idx_buf ? in_idx_buf[i] : i; } } - index += BlockSize; - // this will break the loop when the counter is set (cur >= k), because last_prefix_sum >= cur - last_prefix_sum = histogram[(i + 1) * BlockSize - 1]; } } +template +__global__ void last_filter_kernel(const T* in, + const IdxT* in_idx, + const T* in_buf, + const IdxT* in_idx_buf, + T* out, + IdxT* out_idx, + IdxT len, + IdxT k, + Counter* counters, + const bool select_min) +{ + const size_t batch_id = blockIdx.y; // size_t to avoid multiplication overflow + + Counter* counter = counters + batch_id; + IdxT previous_len = counter->previous_len; + if (previous_len == 0) { return; } + const IdxT buf_len = calc_buf_len(len); + if (previous_len > buf_len || in_buf == in) { + in_buf = in + batch_id * len; + in_idx_buf = in_idx ? (in_idx + batch_id * len) : nullptr; + previous_len = len; + } else { + in_buf += batch_id * buf_len; + in_idx_buf += batch_id * buf_len; + } + out += batch_id * k; + out_idx += batch_id * k; + + constexpr int pass = calc_num_passes() - 1; + constexpr int start_bit = calc_start_bit(pass); + + const auto kth_value_bits = counter->kth_value_bits; + const IdxT needed_num_of_kth = counter->k; + IdxT* p_out_cnt = &counter->out_cnt; + IdxT* p_out_back_cnt = &counter->out_back_cnt; + + auto f = [k, + select_min, + kth_value_bits, + needed_num_of_kth, + p_out_cnt, + p_out_back_cnt, + in_idx_buf, + out, + out_idx](T value, IdxT i) { + const auto bits = (twiddle_in(value, select_min) >> start_bit) << start_bit; + if (bits < kth_value_bits) { + IdxT pos = atomicAdd(p_out_cnt, static_cast(1)); + out[pos] = value; + out_idx[pos] = in_idx_buf ? in_idx_buf[i] : i; + } else if (bits == kth_value_bits) { + IdxT back_pos = atomicAdd(p_out_back_cnt, static_cast(1)); + if (back_pos < needed_num_of_kth) { + IdxT pos = k - 1 - back_pos; + out[pos] = value; + out_idx[pos] = in_idx_buf ? in_idx_buf[i] : i; + } + } + }; + + vectorized_process(static_cast(blockIdx.x) * blockDim.x + threadIdx.x, + static_cast(blockDim.x) * gridDim.x, + in_buf, + previous_len, + f); +} + /** * * It is expected to call this kernel multiple times (passes), in each pass we process a radix, @@ -350,35 +517,79 @@ _RAFT_DEVICE void choose_bucket(Counter* counter, IdxT* histogram, cons * * In the implementation, the filtering step is delayed to the next pass so the filtering and * histogram computation are fused. In this way, inputs are read once rather than twice. + * + * During the filtering step, we won't write candidates (elements in bucket j) to `out_buf` if the + * number of candidates is larger than the length of `out_buf` (this could happen when the leading + * bits of input values are almost the same). And then in the next pass, inputs are read from `in` + * rather than from `in_buf`. The benefit is that we can save the cost of writing candidates and + * their indices. */ -template -__global__ void __launch_bounds__(BlockSize) radix_kernel(const T* in_buf, - const IdxT* in_idx_buf, - T* out_buf, - IdxT* out_idx_buf, - T* out, - IdxT* out_idx, - Counter* counters, - IdxT* histograms, - const IdxT len, - const int k, - const bool greater, - const int pass) +template +__global__ void radix_kernel(const T* in, + const IdxT* in_idx, + const T* in_buf, + const IdxT* in_idx_buf, + T* out_buf, + IdxT* out_idx_buf, + T* out, + IdxT* out_idx, + Counter* counters, + IdxT* histograms, + const IdxT len, + const IdxT k, + const bool select_min, + const int pass) { - __shared__ bool isLastBlockDone; + const size_t batch_id = blockIdx.y; + auto counter = counters + batch_id; + IdxT current_k; + IdxT previous_len; + IdxT current_len; + if (pass == 0) { + current_k = k; + previous_len = len; + // Need to do this so setting counter->previous_len for the next pass is correct. + // This value is meaningless for pass 0, but it's fine because pass 0 won't be the + // last pass in this implementation so pass 0 won't hit the "if (pass == + // num_passes - 1)" branch. + // Maybe it's better to reload counter->previous_len and use it rather than + // current_len in last_filter() + current_len = len; + } else { + current_k = counter->k; + current_len = counter->len; + previous_len = counter->previous_len; + } + if (current_len == 0) { return; } - constexpr int num_buckets = calc_num_buckets(); - constexpr int num_passes = calc_num_passes(); - const int batch_id = blockIdx.y; - in_buf += batch_id * len; - out_buf += batch_id * len; + // When k=len, early_stop will be true at pass 0. It means filter_and_histogram() should handle + // correctly the case that pass=0 and early_stop=true. However, this special case of k=len is + // handled in other way in select_k() so such case is not possible here. + const bool early_stop = (current_len == current_k); + const IdxT buf_len = calc_buf_len(len); + + // "previous_len > buf_len" means previous pass skips writing buffer + if (pass == 0 || pass == 1 || previous_len > buf_len) { + in_buf = in + batch_id * len; + in_idx_buf = in_idx ? (in_idx + batch_id * len) : nullptr; + previous_len = len; + } else { + in_buf += batch_id * buf_len; + in_idx_buf += batch_id * buf_len; + } + // "current_len > buf_len" means current pass will skip writing buffer + if (pass == 0 || current_len > buf_len) { + out_buf = nullptr; + out_idx_buf = nullptr; + } else { + out_buf += batch_id * buf_len; + out_idx_buf += batch_id * buf_len; + } out += batch_id * k; out_idx += batch_id * k; - if (in_idx_buf) { in_idx_buf += batch_id * len; } - if (out_idx_buf) { out_idx_buf += batch_id * len; } - auto counter = counters + batch_id; - auto histogram = histograms + batch_id * num_buckets; + constexpr int num_buckets = calc_num_buckets(); + auto histogram = histograms + batch_id * num_buckets; filter_and_histogram(in_buf, in_idx_buf, @@ -386,126 +597,468 @@ __global__ void __launch_bounds__(BlockSize) radix_kernel(const T* in_buf, out_idx_buf, out, out_idx, - len, + previous_len, counter, histogram, - greater, + select_min, pass, - k); + early_stop); __threadfence(); + bool isLastBlock = false; if (threadIdx.x == 0) { unsigned int finished = atomicInc(&counter->finished_block_cnt, gridDim.x - 1); - isLastBlockDone = (finished == (gridDim.x - 1)); + isLastBlock = (finished == (gridDim.x - 1)); } - // Synchronize to make sure that each thread reads the correct value of - // isLastBlockDone. - __syncthreads(); - if (isLastBlockDone) { - if (counter->len == 1 && threadIdx.x == 0) { - counter->previous_len = 0; - counter->len = 0; - } - // init counter, other members of counter is initialized with 0 by - // cudaMemset() - if (pass == 0 && threadIdx.x == 0) { - counter->k = k; - counter->len = len; - counter->out_back_cnt = 0; + if (__syncthreads_or(isLastBlock)) { + if (early_stop) { + if (threadIdx.x == 0) { + // `last_filter_kernel()` requires setting previous_len + counter->previous_len = 0; + counter->len = 0; + } + return; } + + scan(histogram); __syncthreads(); + choose_bucket(counter, histogram, current_k, pass); + __syncthreads(); + + constexpr int num_passes = calc_num_passes(); + // reset for next pass + if (pass != num_passes - 1) { + for (int i = threadIdx.x; i < num_buckets; i += blockDim.x) { + histogram[i] = 0; + } + } + if (threadIdx.x == 0) { + // `last_filter_kernel()` requires setting previous_len even in the last pass + counter->previous_len = current_len; + // not necessary for the last pass, but put it here anyway + counter->filter_cnt = 0; + } + + if constexpr (fused_last_filter) { + if (pass == num_passes - 1) { + last_filter(out_buf ? out_buf : in_buf, + out_idx_buf ? out_idx_buf : in_idx_buf, + out, + out_idx, + out_buf ? current_len : len, + k, + counter, + select_min, + pass); + } + } + } +} + +template +int calc_chunk_size(int batch_size, IdxT len, int sm_cnt, Kernel kernel) +{ + int active_blocks; + RAFT_CUDA_TRY( + cudaOccupancyMaxActiveBlocksPerMultiprocessor(&active_blocks, kernel, BlockSize, 0)); + + constexpr int items_per_thread = 32; + constexpr int num_waves = 10; + int chunk_size = + std::max(1, num_waves * sm_cnt * active_blocks * BlockSize * items_per_thread / len); + return std::min(chunk_size, batch_size); +} - IdxT ori_k = counter->k; +template +unsigned calc_grid_dim(int batch_size, IdxT len, int sm_cnt) +{ + static_assert(VECTORIZED_READ_SIZE / sizeof(T) >= 1); + + int active_blocks; + RAFT_CUDA_TRY(cudaOccupancyMaxActiveBlocksPerMultiprocessor( + &active_blocks, radix_kernel, BlockSize, 0)); + active_blocks *= sm_cnt; - if (counter->len > 0) { - choose_bucket(counter, histogram, ori_k); + IdxT best_num_blocks = 0; + float best_tail_wave_penalty = 1.0f; + const IdxT max_num_blocks = ceildiv(len, VECTORIZED_READ_SIZE / sizeof(T) * BlockSize); + for (int num_waves = 1;; ++num_waves) { + IdxT num_blocks = std::min( + max_num_blocks, static_cast(std::max(num_waves * active_blocks / batch_size, 1))); + IdxT items_per_thread = ceildiv(len, num_blocks * BlockSize); + items_per_thread = alignTo(items_per_thread, VECTORIZED_READ_SIZE / sizeof(T)); + num_blocks = ceildiv(len, items_per_thread * BlockSize); + float actual_num_waves = static_cast(num_blocks) * batch_size / active_blocks; + float tail_wave_penalty = + (ceilf(actual_num_waves) - actual_num_waves) / ceilf(actual_num_waves); + + // 0.15 is determined experimentally. It also ensures breaking the loop early, + // e.g. when num_waves > 7, tail_wave_penalty will always <0.15 + if (tail_wave_penalty < 0.15) { + best_num_blocks = num_blocks; + break; + } else if (tail_wave_penalty < best_tail_wave_penalty) { + best_num_blocks = num_blocks; + best_tail_wave_penalty = tail_wave_penalty; } - __syncthreads(); - if (pass == num_passes - 1) { - const IdxT previous_len = counter->previous_len; - const int want_bucket = counter->bucket; - int start_bit = calc_start_bit(pass); - unsigned mask = calc_mask(pass); - - // radix topk - IdxT& out_cnt = counter->out_cnt; - for (IdxT i = threadIdx.x; i < previous_len; i += blockDim.x) { - const T value = out_buf[i]; - int bucket = calc_bucket(value, start_bit, mask, greater); - if (bucket < want_bucket) { - IdxT pos = atomicAdd(&out_cnt, IdxT(1)); - out[pos] = value; - out_idx[pos] = out_idx_buf[i]; - } else if (bucket == want_bucket) { - IdxT needed_num_of_kth = counter->k; - IdxT back_pos = atomicAdd(&(counter->out_back_cnt), IdxT(1)); - if (back_pos < needed_num_of_kth) { - IdxT pos = k - 1 - back_pos; - out[pos] = value; - out_idx[pos] = out_idx_buf[i]; - } - } + if (num_blocks == max_num_blocks) { break; } + } + return best_num_blocks; +} + +template +_RAFT_HOST_DEVICE void set_buf_pointers(const T* in, + const IdxT* in_idx, + T* buf1, + IdxT* idx_buf1, + T* buf2, + IdxT* idx_buf2, + int pass, + const T*& in_buf, + const IdxT*& in_idx_buf, + T*& out_buf, + IdxT*& out_idx_buf) +{ + if (pass == 0) { + in_buf = in; + in_idx_buf = nullptr; + out_buf = nullptr; + out_idx_buf = nullptr; + } else if (pass == 1) { + in_buf = in; + in_idx_buf = in_idx; + out_buf = buf1; + out_idx_buf = idx_buf1; + } else if (pass % 2 == 0) { + in_buf = buf1; + in_idx_buf = idx_buf1; + out_buf = buf2; + out_idx_buf = idx_buf2; + } else { + in_buf = buf2; + in_idx_buf = idx_buf2; + out_buf = buf1; + out_idx_buf = idx_buf1; + } +} + +template +void radix_topk(const T* in, + const IdxT* in_idx, + int batch_size, + IdxT len, + IdxT k, + T* out, + IdxT* out_idx, + bool select_min, + bool fused_last_filter, + unsigned grid_dim, + int sm_cnt, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + // TODO: is it possible to relax this restriction? + static_assert(calc_num_passes() > 1); + constexpr int num_buckets = calc_num_buckets(); + + auto kernel = radix_kernel; + const size_t max_chunk_size = + calc_chunk_size(batch_size, len, sm_cnt, kernel); + if (max_chunk_size != static_cast(batch_size)) { + grid_dim = calc_grid_dim(max_chunk_size, len, sm_cnt); + } + const IdxT buf_len = calc_buf_len(len); + + size_t req_aux = max_chunk_size * (sizeof(Counter) + num_buckets * sizeof(IdxT)); + size_t req_buf = max_chunk_size * buf_len * 2 * (sizeof(T) + sizeof(IdxT)); + size_t mem_req = req_aux + req_buf + 256 * 6; // might need extra memory for alignment + + auto pool_guard = raft::get_pool_memory_resource(mr, mem_req); + if (pool_guard) { + RAFT_LOG_DEBUG("radix::select_k: using pool memory resource with initial size %zu bytes", + pool_guard->pool_size()); + } + + rmm::device_uvector> counters(max_chunk_size, stream, mr); + rmm::device_uvector histograms(max_chunk_size * num_buckets, stream, mr); + rmm::device_uvector buf1(max_chunk_size * buf_len, stream, mr); + rmm::device_uvector idx_buf1(max_chunk_size * buf_len, stream, mr); + rmm::device_uvector buf2(max_chunk_size * buf_len, stream, mr); + rmm::device_uvector idx_buf2(max_chunk_size * buf_len, stream, mr); + + for (size_t offset = 0; offset < static_cast(batch_size); offset += max_chunk_size) { + int chunk_size = std::min(max_chunk_size, batch_size - offset); + RAFT_CUDA_TRY( + cudaMemsetAsync(counters.data(), 0, counters.size() * sizeof(Counter), stream)); + RAFT_CUDA_TRY(cudaMemsetAsync(histograms.data(), 0, histograms.size() * sizeof(IdxT), stream)); + + const T* chunk_in = in + offset * len; + const IdxT* chunk_in_idx = in_idx ? (in_idx + offset * len) : nullptr; + T* chunk_out = out + offset * k; + IdxT* chunk_out_idx = out_idx + offset * k; + + const T* in_buf = nullptr; + const IdxT* in_idx_buf = nullptr; + T* out_buf = nullptr; + IdxT* out_idx_buf = nullptr; + + dim3 blocks(grid_dim, chunk_size); + constexpr int num_passes = calc_num_passes(); + + for (int pass = 0; pass < num_passes; ++pass) { + set_buf_pointers(chunk_in, + chunk_in_idx, + buf1.data(), + idx_buf1.data(), + buf2.data(), + idx_buf2.data(), + pass, + in_buf, + in_idx_buf, + out_buf, + out_idx_buf); + + if (fused_last_filter && pass == num_passes - 1) { + kernel = radix_kernel; } - __syncthreads(); - } else { - // reset for next pass - for (int i = threadIdx.x; i < num_buckets; i += blockDim.x) { - histogram[i] = 0; + + kernel<<>>(chunk_in, + chunk_in_idx, + in_buf, + in_idx_buf, + out_buf, + out_idx_buf, + chunk_out, + chunk_out_idx, + counters.data(), + histograms.data(), + len, + k, + select_min, + pass); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + } + + if (!fused_last_filter) { + last_filter_kernel<<>>(chunk_in, + chunk_in_idx, + out_buf, + out_idx_buf, + chunk_out, + chunk_out_idx, + len, + k, + counters.data(), + select_min); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + } + } +} + +// The following a few functions are for the one-block version, which uses single thread block for +// each row of a batch. +template +_RAFT_DEVICE void filter_and_histogram_for_one_block(const T* in_buf, + const IdxT* in_idx_buf, + T* out_buf, + IdxT* out_idx_buf, + T* out, + IdxT* out_idx, + Counter* counter, + IdxT* histogram, + bool select_min, + int pass) +{ + constexpr int num_buckets = calc_num_buckets(); + for (int i = threadIdx.x; i < num_buckets; i += blockDim.x) { + histogram[i] = 0; + } + IdxT* p_filter_cnt = &counter->filter_cnt; + if (threadIdx.x == 0) { *p_filter_cnt = 0; } + __syncthreads(); + + const int start_bit = calc_start_bit(pass); + const unsigned mask = calc_mask(pass); + const IdxT previous_len = counter->previous_len; + + if (pass == 0) { + auto f = [histogram, select_min, start_bit, mask](T value, IdxT) { + int bucket = calc_bucket(value, start_bit, mask, select_min); + atomicAdd(histogram + bucket, static_cast(1)); + }; + vectorized_process(threadIdx.x, blockDim.x, in_buf, previous_len, f); + } else { + // not use vectorized_process here because it increases #registers a lot + IdxT* p_out_cnt = &counter->out_cnt; + const auto kth_value_bits = counter->kth_value_bits; + const int previous_start_bit = calc_start_bit(pass - 1); + + for (IdxT i = threadIdx.x; i < previous_len; i += blockDim.x) { + const T value = in_buf[i]; + const auto previous_bits = (twiddle_in(value, select_min) >> previous_start_bit) + << previous_start_bit; + if (previous_bits == kth_value_bits) { +#if CUDART_VERSION < 12000 + // Avoiding potential compiler bug in CUDA 11 + volatile +#endif + IdxT pos = atomicAdd(p_filter_cnt, static_cast(1)); + out_buf[pos] = value; + out_idx_buf[pos] = in_idx_buf ? in_idx_buf[i] : i; + + int bucket = calc_bucket(value, start_bit, mask, select_min); + atomicAdd(histogram + bucket, static_cast(1)); + } else if (previous_bits < kth_value_bits) { + IdxT pos = atomicAdd(p_out_cnt, static_cast(1)); + out[pos] = value; + out_idx[pos] = in_idx_buf ? in_idx_buf[i] : i; } - if (threadIdx.x == 0) { counter->filter_cnt = 0; } } } } -/** - * Calculate the minimal batch size, such that GPU is still fully occupied. - */ template -inline dim3 get_optimal_grid_size(size_t req_batch_size, size_t len) +__global__ void radix_topk_one_block_kernel(const T* in, + const IdxT* in_idx, + const IdxT len, + const IdxT k, + T* out, + IdxT* out_idx, + const bool select_min, + T* buf1, + IdxT* idx_buf1, + T* buf2, + IdxT* idx_buf2) { - int dev_id, sm_count, occupancy, max_grid_dim_y; - RAFT_CUDA_TRY(cudaGetDevice(&dev_id)); - RAFT_CUDA_TRY(cudaDeviceGetAttribute(&sm_count, cudaDevAttrMultiProcessorCount, dev_id)); - RAFT_CUDA_TRY(cudaDeviceGetAttribute(&max_grid_dim_y, cudaDevAttrMaxGridDimY, dev_id)); - RAFT_CUDA_TRY(cudaOccupancyMaxActiveBlocksPerMultiprocessor( - &occupancy, radix_kernel, BlockSize, 0)); - - // number of block we'd use if the batch size is enough to occupy the gpu in any case - size_t blocks_per_row = ceildiv(len, BlockSize * ITEM_PER_THREAD); - - // fully occupy GPU - size_t opt_batch_size = ceildiv(sm_count * occupancy, blocks_per_row); - // round it up to the closest pow-of-two for better data alignment - opt_batch_size = isPo2(opt_batch_size) ? opt_batch_size : (1 << (log2(opt_batch_size) + 1)); - // Take a max possible pow-of-two grid_dim_y - max_grid_dim_y = isPo2(max_grid_dim_y) ? max_grid_dim_y : (1 << log2(max_grid_dim_y)); - // If the optimal batch size is very small compared to the requested batch size, we know - // the extra required memory is not significant and we can increase the batch size for - // better occupancy when the grid size is not multiple of the SM count. - // Also don't split the batch size when there is not much work overall. - const size_t safe_enlarge_factor = 9; - const size_t min_grid_size = 1024; - while ((opt_batch_size << safe_enlarge_factor) < req_batch_size || - blocks_per_row * opt_batch_size < min_grid_size) { - opt_batch_size <<= 1; + constexpr int num_buckets = calc_num_buckets(); + __shared__ Counter counter; + __shared__ IdxT histogram[num_buckets]; + + if (threadIdx.x == 0) { + counter.k = k; + counter.len = len; + counter.previous_len = len; + counter.kth_value_bits = 0; + counter.out_cnt = 0; + counter.out_back_cnt = 0; } + __syncthreads(); + + const size_t batch_id = blockIdx.x; // size_t to avoid multiplication overflow + in += batch_id * len; + if (in_idx) { in_idx += batch_id * len; } + out += batch_id * k; + out_idx += batch_id * k; + buf1 += batch_id * len; + idx_buf1 += batch_id * len; + buf2 += batch_id * len; + idx_buf2 += batch_id * len; + const T* in_buf = nullptr; + const IdxT* in_idx_buf = nullptr; + T* out_buf = nullptr; + IdxT* out_idx_buf = nullptr; + + constexpr int num_passes = calc_num_passes(); + for (int pass = 0; pass < num_passes; ++pass) { + set_buf_pointers( + in, in_idx, buf1, idx_buf1, buf2, idx_buf2, pass, in_buf, in_idx_buf, out_buf, out_idx_buf); + + IdxT current_len = counter.len; + IdxT current_k = counter.k; + + filter_and_histogram_for_one_block(in_buf, + in_idx_buf, + out_buf, + out_idx_buf, + out, + out_idx, + &counter, + histogram, + select_min, + pass); + __syncthreads(); + + scan(histogram); + __syncthreads(); + + choose_bucket(&counter, histogram, current_k, pass); + if (threadIdx.x == 0) { counter.previous_len = current_len; } + __syncthreads(); - // Do not exceed the max grid size. - opt_batch_size = std::min(opt_batch_size, size_t(max_grid_dim_y)); - // Don't do more work than needed - opt_batch_size = std::min(opt_batch_size, req_batch_size); - // Let more blocks share one row if the required batch size is too small. - while (opt_batch_size * blocks_per_row < size_t(sm_count * occupancy) && - // Ensure we still can read data somewhat efficiently - len * sizeof(T) > 2 * VECTORIZED_READ_SIZE * BlockSize * blocks_per_row) { - blocks_per_row <<= 1; + if (counter.len == counter.k || pass == num_passes - 1) { + last_filter(pass == 0 ? in : out_buf, + pass == 0 ? in_idx : out_idx_buf, + out, + out_idx, + current_len, + k, + &counter, + select_min, + pass); + break; + } } +} - return dim3(blocks_per_row, opt_batch_size); +// radix_topk() might use multiple thread blocks for one row of a batch. In contrast, the following +// one-block version uses single thread block for one row of a batch, so intermediate data, like +// counters and global histograms, can be kept in shared memory and cheap sync operations can be +// used. It's used when len is relatively small or when the number of blocks per row calculated by +// `calc_grid_dim()` is 1. +template +void radix_topk_one_block(const T* in, + const IdxT* in_idx, + int batch_size, + IdxT len, + IdxT k, + T* out, + IdxT* out_idx, + bool select_min, + int sm_cnt, + rmm::cuda_stream_view stream, + rmm::mr::device_memory_resource* mr) +{ + static_assert(calc_num_passes() > 1); + + auto kernel = radix_topk_one_block_kernel; + const size_t max_chunk_size = + calc_chunk_size(batch_size, len, sm_cnt, kernel); + + auto pool_guard = + raft::get_pool_memory_resource(mr, + max_chunk_size * len * 2 * (sizeof(T) + sizeof(IdxT)) + + 256 * 4 // might need extra memory for alignment + ); + if (pool_guard) { + RAFT_LOG_DEBUG("radix::select_k: using pool memory resource with initial size %zu bytes", + pool_guard->pool_size()); + } + + rmm::device_uvector buf1(len * max_chunk_size, stream, mr); + rmm::device_uvector idx_buf1(len * max_chunk_size, stream, mr); + rmm::device_uvector buf2(len * max_chunk_size, stream, mr); + rmm::device_uvector idx_buf2(len * max_chunk_size, stream, mr); + + for (size_t offset = 0; offset < static_cast(batch_size); offset += max_chunk_size) { + int chunk_size = std::min(max_chunk_size, batch_size - offset); + kernel<<>>(in + offset * len, + in_idx ? (in_idx + offset * len) : nullptr, + len, + k, + out + offset * k, + out_idx + offset * k, + select_min, + buf1.data(), + idx_buf1.data(), + buf2.data(), + idx_buf2.data()); + } } +} // namespace impl + /** * Select k smallest or largest key/values from each row in the input data. * @@ -546,6 +1099,12 @@ inline dim3 get_optimal_grid_size(size_t req_batch_size, size_t len) * the payload selected together with `out`. * @param select_min * whether to select k smallest (true) or largest (false) keys. + * @param fused_last_filter + * when it's true, the last filter is fused into the kernel in the last pass and only one thread + * block will do the filtering; when false, a standalone filter kernel with multiple thread + * blocks is called. The later case is preferable when leading bits of input data are almost the + * same. That is, when the value range of input data is narrow. In such case, there could be a + * large number of inputs for the last filter, hence using multiple thread blocks is beneficial. * @param stream * @param mr an optional memory resource to use across the calls (you can provide a large enough * memory pool here to avoid memory allocations within the call). @@ -553,109 +1112,65 @@ inline dim3 get_optimal_grid_size(size_t req_batch_size, size_t len) template void select_k(const T* in, const IdxT* in_idx, - size_t batch_size, - size_t len, - int k, + int batch_size, + IdxT len, + IdxT k, T* out, IdxT* out_idx, bool select_min, + bool fused_last_filter, rmm::cuda_stream_view stream, rmm::mr::device_memory_resource* mr = nullptr) { - // reduce the block size if the input length is too small. - if constexpr (BlockSize > calc_min_block_size()) { - if (BlockSize * ITEM_PER_THREAD > len) { - return select_k( - in, in_idx, batch_size, len, k, out, out_idx, select_min, stream); + if (k == len) { + RAFT_CUDA_TRY( + cudaMemcpyAsync(out, in, sizeof(T) * batch_size * len, cudaMemcpyDeviceToDevice, stream)); + if (in_idx) { + RAFT_CUDA_TRY(cudaMemcpyAsync( + out_idx, in_idx, sizeof(IdxT) * batch_size * len, cudaMemcpyDeviceToDevice, stream)); + } else { + auto out_idx_view = + raft::make_device_vector_view(out_idx, static_cast(len) * batch_size); + raft::device_resources handle(stream); + raft::linalg::map_offset(handle, out_idx_view, raft::mod_const_op(len)); } + return; } - // TODO: is it possible to relax this restriction? - static_assert(calc_num_passes() > 1); - constexpr int num_buckets = calc_num_buckets(); - - dim3 blocks = get_optimal_grid_size(batch_size, len); - size_t max_chunk_size = blocks.y; - - size_t req_aux = max_chunk_size * (sizeof(Counter) + num_buckets * sizeof(IdxT)); - size_t req_buf = max_chunk_size * len * 2 * (sizeof(T) + sizeof(IdxT)); - size_t mem_req = req_aux + req_buf; - size_t mem_free, mem_total; - RAFT_CUDA_TRY(cudaMemGetInfo(&mem_free, &mem_total)); - std::optional managed_memory; - rmm::mr::device_memory_resource* mr_buf = nullptr; - if (mem_req > mem_free) { - // if there's not enough memory for buffers on the device, resort to the managed memory. - mem_req = req_aux; - managed_memory.emplace(); - mr_buf = &managed_memory.value(); - } - - auto pool_guard = raft::get_pool_memory_resource(mr, mem_req); - if (pool_guard) { - RAFT_LOG_DEBUG("radix::select_k: using pool memory resource with initial size %zu bytes", - pool_guard->pool_size()); + // TODO: use device_resources::get_device_properties() instead; should change it when we refactor + // resource management + int sm_cnt; + { + int dev; + RAFT_CUDA_TRY(cudaGetDevice(&dev)); + RAFT_CUDA_TRY(cudaDeviceGetAttribute(&sm_cnt, cudaDevAttrMultiProcessorCount, dev)); } - if (mr_buf == nullptr) { mr_buf = mr; } - - rmm::device_uvector> counters(max_chunk_size, stream, mr); - rmm::device_uvector histograms(max_chunk_size * num_buckets, stream, mr); - rmm::device_uvector buf1(max_chunk_size * len, stream, mr_buf); - rmm::device_uvector idx_buf1(max_chunk_size * len, stream, mr_buf); - rmm::device_uvector buf2(max_chunk_size * len, stream, mr_buf); - rmm::device_uvector idx_buf2(max_chunk_size * len, stream, mr_buf); - for (size_t offset = 0; offset < batch_size; offset += max_chunk_size) { - blocks.y = std::min(max_chunk_size, batch_size - offset); + constexpr int items_per_thread = 32; - RAFT_CUDA_TRY( - cudaMemsetAsync(counters.data(), 0, counters.size() * sizeof(Counter), stream)); - RAFT_CUDA_TRY(cudaMemsetAsync(histograms.data(), 0, histograms.size() * sizeof(IdxT), stream)); - - const T* in_buf = nullptr; - const IdxT* in_idx_buf = nullptr; - T* out_buf = nullptr; - IdxT* out_idx_buf = nullptr; - - constexpr int num_passes = calc_num_passes(); - - for (int pass = 0; pass < num_passes; ++pass) { - if (pass == 0) { - in_buf = in + offset * len; - in_idx_buf = nullptr; - out_buf = nullptr; - out_idx_buf = nullptr; - } else if (pass == 1) { - in_buf = in + offset * len; - in_idx_buf = in_idx ? in_idx + offset * len : nullptr; - out_buf = buf1.data(); - out_idx_buf = idx_buf1.data(); - } else if (pass % 2 == 0) { - in_buf = buf1.data(); - in_idx_buf = idx_buf1.data(); - out_buf = buf2.data(); - out_idx_buf = idx_buf2.data(); - } else { - in_buf = buf2.data(); - in_idx_buf = idx_buf2.data(); - out_buf = buf1.data(); - out_idx_buf = idx_buf1.data(); - } - - radix_kernel - <<>>(in_buf, - in_idx_buf, - out_buf, - out_idx_buf, - out + offset * k, - out_idx + offset * k, - counters.data(), - histograms.data(), - len, - k, - !select_min, - pass); - RAFT_CUDA_TRY(cudaPeekAtLastError()); + if (len <= BlockSize * items_per_thread) { + impl::radix_topk_one_block( + in, in_idx, batch_size, len, k, out, out_idx, select_min, sm_cnt, stream, mr); + } else { + unsigned grid_dim = + impl::calc_grid_dim(batch_size, len, sm_cnt); + if (grid_dim == 1) { + impl::radix_topk_one_block( + in, in_idx, batch_size, len, k, out, out_idx, select_min, sm_cnt, stream, mr); + } else { + impl::radix_topk(in, + in_idx, + batch_size, + len, + k, + out, + out_idx, + select_min, + fused_last_filter, + grid_dim, + sm_cnt, + stream, + mr); } } } diff --git a/cpp/include/raft/spatial/knn/knn.cuh b/cpp/include/raft/spatial/knn/knn.cuh index 692d262043..a7bbfd9500 100644 --- a/cpp/include/raft/spatial/knn/knn.cuh +++ b/cpp/include/raft/spatial/knn/knn.cuh @@ -153,12 +153,12 @@ template case SelectKAlgo::RADIX_8_BITS: matrix::detail::select::radix::select_k( - in_keys, in_values, n_inputs, input_len, k, out_keys, out_values, select_min, stream); + in_keys, in_values, n_inputs, input_len, k, out_keys, out_values, select_min, true, stream); break; case SelectKAlgo::RADIX_11_BITS: matrix::detail::select::radix::select_k( - in_keys, in_values, n_inputs, input_len, k, out_keys, out_values, select_min, stream); + in_keys, in_values, n_inputs, input_len, k, out_keys, out_values, select_min, true, stream); break; case SelectKAlgo::WARP_SORT: diff --git a/cpp/internal/raft_internal/matrix/select_k.cuh b/cpp/internal/raft_internal/matrix/select_k.cuh index ede6382c33..188122c9b4 100644 --- a/cpp/internal/raft_internal/matrix/select_k.cuh +++ b/cpp/internal/raft_internal/matrix/select_k.cuh @@ -33,7 +33,8 @@ struct params { size_t len; int k; bool select_min; - bool use_index_input = true; + bool use_index_input = true; + bool use_same_leading_bits = false; }; inline auto operator<<(std::ostream& os, const params& ss) -> std::ostream& @@ -42,7 +43,8 @@ inline auto operator<<(std::ostream& os, const params& ss) -> std::ostream& os << ", len: " << ss.len; os << ", k: " << ss.k; os << (ss.select_min ? ", asc" : ", dsc"); - os << (ss.use_index_input ? "}" : ", no-input-index}"); + os << (ss.use_index_input ? "" : ", no-input-index"); + os << (ss.use_same_leading_bits ? ", same-leading-bits}" : "}"); return os; } @@ -50,6 +52,7 @@ enum class Algo { kPublicApi, kRadix8bits, kRadix11bits, + kRadix11bitsExtraPass, kWarpAuto, kWarpImmediate, kWarpFiltered, @@ -63,6 +66,7 @@ inline auto operator<<(std::ostream& os, const Algo& algo) -> std::ostream& case Algo::kPublicApi: return os << "kPublicApi"; case Algo::kRadix8bits: return os << "kRadix8bits"; case Algo::kRadix11bits: return os << "kRadix11bits"; + case Algo::kRadix11bitsExtraPass: return os << "kRadix11bitsExtraPass"; case Algo::kWarpAuto: return os << "kWarpAuto"; case Algo::kWarpImmediate: return os << "kWarpImmediate"; case Algo::kWarpFiltered: return os << "kWarpFiltered"; @@ -103,11 +107,38 @@ void select_k_impl(const device_resources& handle, } } case Algo::kRadix8bits: - return detail::select::radix::select_k( - in, in_idx, batch_size, len, k, out, out_idx, select_min, stream); + return detail::select::radix::select_k(in, + in_idx, + batch_size, + len, + k, + out, + out_idx, + select_min, + true, // fused_last_filter + stream); case Algo::kRadix11bits: - return detail::select::radix::select_k( - in, in_idx, batch_size, len, k, out, out_idx, select_min, stream); + return detail::select::radix::select_k(in, + in_idx, + batch_size, + len, + k, + out, + out_idx, + select_min, + true, // fused_last_filter + stream); + case Algo::kRadix11bitsExtraPass: + return detail::select::radix::select_k(in, + in_idx, + batch_size, + len, + k, + out, + out_idx, + select_min, + false, // fused_last_filter + stream); case Algo::kWarpAuto: return detail::select::warpsort::select_k( in, in_idx, batch_size, len, k, out, out_idx, select_min, stream); diff --git a/cpp/test/matrix/select_k.cu b/cpp/test/matrix/select_k.cu index 392464eb27..2a40d70abc 100644 --- a/cpp/test/matrix/select_k.cu +++ b/cpp/test/matrix/select_k.cu @@ -332,6 +332,7 @@ INSTANTIATE_TEST_CASE_P( // NOLINT testing::Values(select::Algo::kPublicApi, select::Algo::kRadix8bits, select::Algo::kRadix11bits, + select::Algo::kRadix11bitsExtraPass, select::Algo::kWarpImmediate, select::Algo::kWarpFiltered, select::Algo::kWarpDistributed))); @@ -426,6 +427,7 @@ INSTANTIATE_TEST_CASE_P( // NOLINT testing::Combine(inputs_random_longlist, testing::Values(select::Algo::kRadix8bits, select::Algo::kRadix11bits, + select::Algo::kRadix11bitsExtraPass, select::Algo::kWarpImmediate, select::Algo::kWarpFiltered, select::Algo::kWarpDistributed, @@ -440,6 +442,7 @@ INSTANTIATE_TEST_CASE_P( // NOLINT testing::Combine(inputs_random_longlist, testing::Values(select::Algo::kRadix8bits, select::Algo::kRadix11bits, + select::Algo::kRadix11bitsExtraPass, select::Algo::kWarpImmediate, select::Algo::kWarpFiltered, select::Algo::kWarpDistributed, @@ -451,7 +454,11 @@ TEST_P(ReferencedRandomDoubleInt, LargeSize) { run(); } // NOLINT INSTANTIATE_TEST_CASE_P( // NOLINT SelectK, ReferencedRandomDoubleInt, - testing::Combine(inputs_random_largesize, testing::Values(select::Algo::kWarpAuto))); + testing::Combine(inputs_random_largesize, + testing::Values(select::Algo::kWarpAuto, + select::Algo::kRadix8bits, + select::Algo::kRadix11bits, + select::Algo::kRadix11bitsExtraPass))); using ReferencedRandomFloatSizeT = SelectK::params_random>; @@ -459,6 +466,7 @@ TEST_P(ReferencedRandomFloatSizeT, LargeK) { run(); } // NOLINT INSTANTIATE_TEST_CASE_P(SelectK, // NOLINT ReferencedRandomFloatSizeT, testing::Combine(inputs_random_largek, - testing::Values(select::Algo::kRadix11bits))); + testing::Values(select::Algo::kRadix11bits, + select::Algo::kRadix11bitsExtraPass))); } // namespace raft::matrix From 9389108b7f7f100c883092a46f09738f23ab8151 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Fri, 24 Mar 2023 13:42:03 -0400 Subject: [PATCH 15/25] RAFT skeleton project template (#1312) This is a copy and modification of a user's project but I think this is going to be generally useful to users as the same types of challenges are going to come up again. In this case, the user wasn't able to build/link because they weren't using `rapids-cmake` to propagate important configuration settings. I think having a skeleton project available that we build in CI and keep up to date will help new users build more applications on RAFT. TODO: - [x] Make building the template optional - [x] Verify this can build in CMake and reuse already built/installed bits - [x] Add to docs / readme and reference in README.md - [x] Add a little example of invoking an API (maybe `pairwise_distances`?) to `main()` Authors: - Corey J. Nolet (https://github.com/cjnolet) - Ben Frederickson (https://github.com/benfred) Approvers: - Micka (https://github.com/lowener) - Dante Gama Dessavre (https://github.com/dantegd) - Divye Gala (https://github.com/divyegala) - AJ Schmidt (https://github.com/ajschmidt8) URL: https://github.com/rapidsai/raft/pull/1312 --- .github/workflows/build.yaml | 1 + README.md | 61 ++++-------------- build.sh | 15 ++++- .../recipes/libraft/build_libraft_template.sh | 5 ++ conda/recipes/libraft/meta.yaml | 36 +++++++++++ cpp/template/CMakeLists.txt | 38 +++++++++++ cpp/template/README.md | 18 ++++++ cpp/template/build.sh | 41 ++++++++++++ .../cmake/thirdparty/fetch_rapids.cmake | 21 ++++++ cpp/template/cmake/thirdparty/get_raft.cmake | 62 ++++++++++++++++++ cpp/template/src/test_distance.cu | 42 ++++++++++++ docs/source/build.md | 64 ++++++++----------- python/pylibraft/setup.cfg | 38 +++++++++++ setup.cfg | 55 ++++++++++++++++ 14 files changed, 409 insertions(+), 88 deletions(-) create mode 100644 conda/recipes/libraft/build_libraft_template.sh create mode 100644 cpp/template/CMakeLists.txt create mode 100644 cpp/template/README.md create mode 100755 cpp/template/build.sh create mode 100644 cpp/template/cmake/thirdparty/fetch_rapids.cmake create mode 100644 cpp/template/cmake/thirdparty/get_raft.cmake create mode 100644 cpp/template/src/test_distance.cu create mode 100644 python/pylibraft/setup.cfg create mode 100644 setup.cfg diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 41b6a639d8..32aab5656b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -52,6 +52,7 @@ jobs: branch: ${{ inputs.branch }} date: ${{ inputs.date }} sha: ${{ inputs.sha }} + skip_upload_pkgs: libraft-template docs-build: if: github.ref_type == 'branch' && github.event_name == 'push' needs: python-build diff --git a/README.md b/README.md index 8c6e817bf9..81973ff82e 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ #

 RAFT: Reusable Accelerated Functions and Tools
- ![Navigating the canyons of accelerated possibilities](img/raft.png) ## Resources @@ -85,9 +84,9 @@ raft::device_resources handle; int n_samples = 5000; int n_features = 50; -auto input = raft::make_device_matrix(handle, n_samples, n_features); -auto labels = raft::make_device_vector(handle, n_samples); -auto output = raft::make_device_matrix(handle, n_samples, n_samples); +auto input = raft::make_device_matrix(handle, n_samples, n_features); +auto labels = raft::make_device_vector(handle, n_samples); +auto output = raft::make_device_matrix(handle, n_samples, n_samples); raft::random::make_blobs(handle, input.view(), labels.view()); @@ -222,52 +221,15 @@ pip install raft-dask-cu11 --extra-index-url=https://pypi.ngc.nvidia.com ### CMake & CPM -RAFT uses the [RAPIDS-CMake](https://github.com/rapidsai/rapids-cmake) library, which makes it simple to include in downstream cmake projects. RAPIDS CMake provides a convenience layer around CPM. - -After [installing](https://github.com/rapidsai/rapids-cmake#installation) rapids-cmake in your project, you can begin using RAFT by placing the code snippet below in a file named `get_raft.cmake` and including it in your cmake build with `include(get_raft.cmake)`. This will make available several targets to add to configure the link libraries for your artifacts. - -```cmake - -set(RAFT_VERSION "22.12") -set(RAFT_FORK "rapidsai") -set(RAFT_PINNED_TAG "branch-${RAFT_VERSION}") - -function(find_and_configure_raft) - set(oneValueArgs VERSION FORK PINNED_TAG COMPILE_LIBRARIES) - cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" - "${multiValueArgs}" ${ARGN} ) - - #----------------------------------------------------- - # Invoke CPM find_package() - #----------------------------------------------------- - - rapids_cpm_find(raft ${PKG_VERSION} - GLOBAL_TARGETS raft::raft - BUILD_EXPORT_SET projname-exports - INSTALL_EXPORT_SET projname-exports - CPM_ARGS - GIT_REPOSITORY https://github.com/${PKG_FORK}/raft.git - GIT_TAG ${PKG_PINNED_TAG} - SOURCE_SUBDIR cpp - OPTIONS - "BUILD_TESTS OFF" - "BUILD_BENCH OFF" - "RAFT_COMPILE_LIBRARIES ${PKG_COMPILE_LIBRARIES}" - ) - -endfunction() - -# Change pinned tag here to test a commit in CI -# To use a different RAFT locally, set the CMake variable -# CPM_raft_SOURCE=/path/to/local/raft -find_and_configure_raft(VERSION ${RAFT_VERSION}.00 - FORK ${RAFT_FORK} - PINNED_TAG ${RAFT_PINNED_TAG} - COMPILE_LIBRARIES NO -) -``` +RAFT uses the [RAPIDS-CMake](https://github.com/rapidsai/rapids-cmake) library, which makes it easy to include in downstream cmake projects. RAPIDS-CMake provides a convenience layer around CPM. Please refer to [these instructions](https://github.com/rapidsai/rapids-cmake#installation) to install and use rapids-cmake in your project. + +#### Example Template Project + +You can find an [example RAFT](cpp/template/README.md) project template in the `cpp/template` directory, which demonstrates how to build a new application with RAFT or incorporate RAFT into an existing cmake project. + +#### CMake Targets -Several CMake targets can be made available by adding components in the table below to the `RAFT_COMPONENTS` list above, separated by spaces. The `raft::raft` target will always be available. RAFT headers require, at a minimum, the CUDA toolkit libraries and RMM dependencies. +Additional CMake targets can be made available by adding components in the table below to the `RAFT_COMPONENTS` list above, separated by spaces. The `raft::raft` target will always be available. RAFT headers require, at a minimum, the CUDA toolkit libraries and RMM dependencies. | Component | Target | Description | Base Dependencies | |-------------|---------------------|-----------------------------------------------------------|---------------------------------------| @@ -321,6 +283,7 @@ The folder structure mirrors other RAPIDS repos, with the following folders: - `internal`: A private header-only component that hosts the code shared between benchmarks and tests. - `scripts`: Helpful scripts for development - `src`: Compiled APIs and template specializations for the shared libraries + - `template`: A skeleton template containing the bare-bones file structure and cmake configuration for writing applications with RAFT. - `test`: Googletests source code - `docs`: Source code and scripts for building library documentation (Uses breath, doxygen, & pydocs) - `python`: Source code for Python libraries. diff --git a/build.sh b/build.sh index b5a72f4205..9468d2cab0 100755 --- a/build.sh +++ b/build.sh @@ -18,7 +18,7 @@ ARGS=$* # script, and that this script resides in the repo dir! REPODIR=$(cd $(dirname $0); pwd) -VALIDARGS="clean libraft pylibraft raft-dask docs tests bench clean --uninstall -v -g -n --compile-lib --allgpuarch --no-nvtx --show_depr_warn -h" +VALIDARGS="clean libraft pylibraft raft-dask docs tests bench template clean --uninstall -v -g -n --compile-lib --allgpuarch --no-nvtx --show_depr_warn -h" HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=] [--limit-tests=] [--limit-bench=] where is: clean - remove all existing build artifacts and configuration (start over) @@ -29,6 +29,7 @@ HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool= is: -v - verbose build mode @@ -354,13 +355,12 @@ if (( ${NUMARGS} == 0 )) || hasArg libraft || hasArg docs || hasArg tests || has -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ -DCMAKE_CUDA_ARCHITECTURES=${RAFT_CMAKE_CUDA_ARCHITECTURES} \ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ - -DRAFT_COMPILE_LIBRARIES=${COMPILE_LIBRARIES} \ + -DRAFT_COMPILE_LIBRARY=${COMPILE_LIBRARY} \ -DRAFT_NVTX=${NVTX} \ -DDISABLE_DEPRECATION_WARNINGS=${DISABLE_DEPRECATION_WARNINGS} \ -DBUILD_TESTS=${BUILD_TESTS} \ -DBUILD_BENCH=${BUILD_BENCH} \ -DCMAKE_MESSAGE_LOG_LEVEL=${CMAKE_LOG_LEVEL} \ - -DRAFT_COMPILE_LIBRARY=${COMPILE_LIBRARY} \ ${CACHE_ARGS} \ ${EXTRA_CMAKE_ARGS} @@ -410,3 +410,12 @@ if hasArg docs; then cd ${SPHINX_BUILD_DIR} sphinx-build -b html source _html fi + +################################################################################ +# Initiate build for example RAFT application template (if needed) + +if hasArg template; then + pushd cpp/template + ./build.sh + popd +fi diff --git a/conda/recipes/libraft/build_libraft_template.sh b/conda/recipes/libraft/build_libraft_template.sh new file mode 100644 index 0000000000..9759402884 --- /dev/null +++ b/conda/recipes/libraft/build_libraft_template.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Copyright (c) 2022-2023, NVIDIA CORPORATION. + +# Just building template so we verify it uses libraft.so and fail if it doesn't build +./build.sh template \ No newline at end of file diff --git a/conda/recipes/libraft/meta.yaml b/conda/recipes/libraft/meta.yaml index 2a724672ab..f911166a9a 100644 --- a/conda/recipes/libraft/meta.yaml +++ b/conda/recipes/libraft/meta.yaml @@ -150,3 +150,39 @@ outputs: home: https://rapids.ai/ license: Apache-2.0 summary: libraft tests + - name: libraft-template + version: {{ version }} + script: build_libraft_template.sh + build: + script_env: *script_env + number: {{ GIT_DESCRIBE_NUMBER }} + string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} + ignore_run_exports_from: + - {{ compiler('cuda') }} + requirements: + build: + - {{ compiler('c') }} + - {{ compiler('cuda') }} {{ cuda_version }} + - {{ compiler('cxx') }} + - cmake {{ cmake_version }} + - ninja + - sysroot_{{ target_platform }} {{ sysroot_version }} + host: + - {{ pin_subpackage('libraft', exact=True) }} + - {{ pin_subpackage('libraft-headers', exact=True) }} + - cuda-profiler-api {{ cuda_profiler_api_host_version }} + - libcublas {{ libcublas_host_version }} + - libcublas-dev {{ libcublas_host_version }} + - libcurand {{ libcurand_host_version }} + - libcurand-dev {{ libcurand_host_version }} + - libcusolver {{ libcusolver_host_version }} + - libcusolver-dev {{ libcusolver_host_version }} + - libcusparse {{ libcusparse_host_version }} + - libcusparse-dev {{ libcusparse_host_version }} + run: + - {{ pin_subpackage('libraft', exact=True) }} + - {{ pin_subpackage('libraft-headers', exact=True) }} + about: + home: https://rapids.ai/ + license: Apache-2.0 + summary: libraft template diff --git a/cpp/template/CMakeLists.txt b/cpp/template/CMakeLists.txt new file mode 100644 index 0000000000..501a5c9503 --- /dev/null +++ b/cpp/template/CMakeLists.txt @@ -0,0 +1,38 @@ +# ============================================================================= +# Copyright (c) 2023, NVIDIA CORPORATION. +# +# 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. + +cmake_minimum_required(VERSION 3.23.1 FATAL_ERROR) + +# ------------- configure rapids-cmake --------------# + +include(cmake/thirdparty/fetch_rapids.cmake) +include(rapids-cmake) +include(rapids-cpm) +include(rapids-cuda) +include(rapids-export) +include(rapids-find) + +# ------------- configure project --------------# + +rapids_cuda_init_architectures(test_raft) + +project(test_raft LANGUAGES CXX CUDA) + +# ------------- configure raft -----------------# + +rapids_cpm_init() +include(cmake/thirdparty/get_raft.cmake) + +# -------------- compile tasks ----------------- # +add_executable(TEST_RAFT src/test_distance.cu) +target_link_libraries(TEST_RAFT PRIVATE raft::raft raft::compiled) diff --git a/cpp/template/README.md b/cpp/template/README.md new file mode 100644 index 0000000000..348dff270a --- /dev/null +++ b/cpp/template/README.md @@ -0,0 +1,18 @@ +# Example RAFT Project Template + +This template project provides a drop-in sample to either start building a new application with, or using RAFT in an existing CMake project. + +First, please refer to our [installation docs](https://docs.rapids.ai/api/raft/stable/build.html#cuda-gpu-requirements) for the minimum requirements to use RAFT. + +Once the minimum requirements are satisfied, this example template application can be built with the provided `build.sh` script. This is a bash script that calls the appropriate CMake commands, so you can look into it to see the typical CMake based build workflow. + +This directory (`RAFT_SOURCE/cpp/template`) can be copied directly in order to build a new application with RAFT. + +RAFT can be integrated into an existing CMake project by copying the contents in the `configure rapids-cmake` and `configure raft` sections of the provided `CMakeLists.txt` into your project, along with `cmake/thirdparty/get_raft.cmake`. + +Make sure to link against the appropriate Cmake targets. Use `raft::raft`to add make the headers available and `raft::compiled` when utilizing the shared library. + +```cmake +target_link_libraries(your_app_target PRIVATE raft::raft raft::compiled) +``` + diff --git a/cpp/template/build.sh b/cpp/template/build.sh new file mode 100755 index 0000000000..3ac00fc9af --- /dev/null +++ b/cpp/template/build.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Copyright (c) 2023, NVIDIA CORPORATION. + +# raft empty project template build script + +# Abort script on first error +set -e + +PARALLEL_LEVEL=${PARALLEL_LEVEL:=`nproc`} + +BUILD_TYPE=Release +BUILD_DIR=build/ + +RAFT_REPO_REL="" +EXTRA_CMAKE_ARGS="" +set -e + + +if [[ ${RAFT_REPO_REL} != "" ]]; then + RAFT_REPO_PATH="`readlink -f \"${RAFT_REPO_REL}\"`" + EXTRA_CMAKE_ARGS="${EXTRA_CMAKE_ARGS} -DCPM_raft_SOURCE=${RAFT_REPO_PATH}" +fi + +if [ "$1" == "clean" ]; then + rm -rf build + exit 0 +fi + +mkdir -p $BUILD_DIR +cd $BUILD_DIR + +cmake \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -DRAFT_NVTX=OFF \ + -DCMAKE_CUDA_ARCHITECTURES="NATIVE" \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + ${EXTRA_CMAKE_ARGS} \ + ../ + +cmake --build . -j${PARALLEL_LEVEL} diff --git a/cpp/template/cmake/thirdparty/fetch_rapids.cmake b/cpp/template/cmake/thirdparty/fetch_rapids.cmake new file mode 100644 index 0000000000..40ba83be9e --- /dev/null +++ b/cpp/template/cmake/thirdparty/fetch_rapids.cmake @@ -0,0 +1,21 @@ +# ============================================================================= +# Copyright (c) 2023, NVIDIA CORPORATION. +# +# 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. + +# Use this variable to update RAPIDS and RAFT versions +set(RAPIDS_VERSION "23.04") + +if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/RAFT_RAPIDS.cmake) + file(DOWNLOAD https://raw.githubusercontent.com/rapidsai/rapids-cmake/branch-${RAPIDS_VERSION}/RAPIDS.cmake + ${CMAKE_CURRENT_BINARY_DIR}/RAFT_RAPIDS.cmake) +endif() +include(${CMAKE_CURRENT_BINARY_DIR}/RAFT_RAPIDS.cmake) diff --git a/cpp/template/cmake/thirdparty/get_raft.cmake b/cpp/template/cmake/thirdparty/get_raft.cmake new file mode 100644 index 0000000000..5463942adf --- /dev/null +++ b/cpp/template/cmake/thirdparty/get_raft.cmake @@ -0,0 +1,62 @@ +# ============================================================================= +# Copyright (c) 2023, NVIDIA CORPORATION. +# +# 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. + +# Use RAPIDS_VERSION from cmake/thirdparty/fetch_rapids.cmake +set(RAFT_VERSION "${RAPIDS_VERSION}") +set(RAFT_FORK "rapidsai") +set(RAFT_PINNED_TAG "branch-${RAPIDS_VERSION}") + +function(find_and_configure_raft) + set(oneValueArgs VERSION FORK PINNED_TAG COMPILE_LIBRARY ENABLE_NVTX ENABLE_MNMG_DEPENDENCIES) + cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" + "${multiValueArgs}" ${ARGN} ) + + set(RAFT_COMPONENTS "") + if(PKG_COMPILE_LIBRARY) + string(APPEND RAFT_COMPONENTS " compiled") + endif() + + if(PKG_ENABLE_MNMG_DEPENDENCIES) + string(APPEND RAFT_COMPONENTS " distributed") + endif() + + #----------------------------------------------------- + # Invoke CPM find_package() + #----------------------------------------------------- + rapids_cpm_find(raft ${PKG_VERSION} + GLOBAL_TARGETS raft::raft + BUILD_EXPORT_SET raft-template-exports + INSTALL_EXPORT_SET raft-template-exports + COMPONENTS ${RAFT_COMPONENTS} + CPM_ARGS + GIT_REPOSITORY https://github.com/${PKG_FORK}/raft.git + GIT_TAG ${PKG_PINNED_TAG} + SOURCE_SUBDIR cpp + OPTIONS + "BUILD_TESTS OFF" + "BUILD_BENCH OFF" + "RAFT_NVTX ${ENABLE_NVTX}" + "RAFT_COMPILE_LIBRARY ${PKG_COMPILE_LIBRARY}" + ) +endfunction() + +# Change pinned tag here to test a commit in CI +# To use a different RAFT locally, set the CMake variable +# CPM_raft_SOURCE=/path/to/local/raft +find_and_configure_raft(VERSION ${RAFT_VERSION}.00 + FORK ${RAFT_FORK} + PINNED_TAG ${RAFT_PINNED_TAG} + COMPILE_LIBRARY ON + ENABLE_MNMG_DEPENDENCIES OFF + ENABLE_NVTX OFF +) diff --git a/cpp/template/src/test_distance.cu b/cpp/template/src/test_distance.cu new file mode 100644 index 0000000000..b86dde70e5 --- /dev/null +++ b/cpp/template/src/test_distance.cu @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * + * 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 +#include +#include +#include +#include + +#ifdef RAFT_COMPILED +#include +#endif + +int main() +{ + raft::device_resources handle; + + int n_samples = 5000; + int n_features = 50; + + auto input = raft::make_device_matrix(handle, n_samples, n_features); + auto labels = raft::make_device_vector(handle, n_samples); + auto output = raft::make_device_matrix(handle, n_samples, n_samples); + + raft::random::make_blobs(handle, input.view(), labels.view()); + + auto metric = raft::distance::DistanceType::L2SqrtExpanded; + raft::distance::pairwise_distance(handle, input.view(), input.view(), output.view(), metric); +} diff --git a/docs/source/build.md b/docs/source/build.md index bbb454736a..e8e6ac8a14 100644 --- a/docs/source/build.md +++ b/docs/source/build.md @@ -75,7 +75,7 @@ Once installed, `libraft` headers (and dependencies which were downloaded and in ``` -### C++ Shared Libraries (optional) +### C++ Shared Library (optional) A shared library can be built for speeding up compile times. The shared library also contains a runtime API that allows you to invoke RAFT APIs directly from C++ source files (without `nvcc`). The shared library can also significantly improve re-compile times both while developing RAFT and using its APIs to develop applications. Pass the `--compile-lib` flag to `build.sh` to build the library: ```bash @@ -109,7 +109,7 @@ Compile the tests using the `tests` target in `build.sh`. Test compile times can be improved significantly by using the optional shared libraries. If installed, they will be used automatically when building the tests but `--compile-libs` can be used to add additional compilation units and compile them with the tests. ```bash -./build.sh libraft tests --compile-libs +./build.sh libraft tests --compile-lib ``` The tests are broken apart by algorithm category, so you will find several binaries in `cpp/build/` named `*_TEST`. @@ -151,19 +151,17 @@ make -j install RAFT's cmake has the following configurable flags available:. -| Flag | Possible Values | Default Value | Behavior | -| --- | --- | --- | --- | -| BUILD_TESTS | ON, OFF | ON | Compile Googletests | -| BUILD_BENCH | ON, OFF | OFF | Compile benchmarks | -| raft_FIND_COMPONENTS | nn distance | | Configures the optional components as a space-separated list | -| RAFT_COMPILE_LIBRARIES | ON, OFF | ON if either BUILD_TESTS or BUILD_BENCH is ON; otherwise OFF | Compiles all `libraft` shared libraries (these are required for Googletests) | -| RAFT_COMPILE_NN_LIBRARY | ON, OFF | OFF | Compiles the `libraft-nn` shared library | -| RAFT_COMPILE_DIST_LIBRARY | ON, OFF | OFF | Compiles the `libraft-distance` shared library | -| DETECT_CONDA_ENV | ON, OFF | ON | Enable detection of conda environment for dependencies | -| RAFT_NVTX | ON, OFF | OFF | Enable NVTX Markers | -| CUDA_ENABLE_KERNELINFO | ON, OFF | OFF | Enables `kernelinfo` in nvcc. This is useful for `compute-sanitizer` | -| CUDA_ENABLE_LINEINFO | ON, OFF | OFF | Enable the -lineinfo option for nvcc | -| CUDA_STATIC_RUNTIME | ON, OFF | OFF | Statically link the CUDA runtime | +| Flag | Possible Values | Default Value | Behavior | +|---------------------------|----------------------| --- | --- | +| BUILD_TESTS | ON, OFF | ON | Compile Googletests | +| BUILD_BENCH | ON, OFF | OFF | Compile benchmarks | +| raft_FIND_COMPONENTS | compiled distributed | | Configures the optional components as a space-separated list | +| RAFT_COMPILE_LIBRARY | ON, OFF | ON if either BUILD_TESTS or BUILD_BENCH is ON; otherwise OFF | Compiles all `libraft` shared libraries (these are required for Googletests) | +| DETECT_CONDA_ENV | ON, OFF | ON | Enable detection of conda environment for dependencies | +| RAFT_NVTX | ON, OFF | OFF | Enable NVTX Markers | +| CUDA_ENABLE_KERNELINFO | ON, OFF | OFF | Enables `kernelinfo` in nvcc. This is useful for `compute-sanitizer` | +| CUDA_ENABLE_LINEINFO | ON, OFF | OFF | Enable the -lineinfo option for nvcc | +| CUDA_STATIC_RUNTIME | ON, OFF | OFF | Statically link the CUDA runtime | Currently, shared libraries are provided for the `libraft-nn` and `libraft-distance` components. @@ -248,9 +246,9 @@ PROPERTIES CXX_STANDARD 17 ``` -### C++ header-only integration +### C++ header-only integration (without cmake) -When the needed [build dependencies](#build-dependencies) are already satisfied, RAFT can be trivially integrated into downstream projects by cloning the repository and adding `cpp/include` from RAFT to the include path: +While not a highly suggested method for building against RAFT, when all of the needed [build dependencies](#build-dependencies) are already satisfied, RAFT can be integrated into downstream projects by cloning the repository and adding `cpp/include` from RAFT to the include path: ```cmake set(RAFT_GIT_DIR ${CMAKE_CURRENT_BINARY_DIR}/raft CACHE STRING "Path to RAFT repo") ExternalProject_Add(raft @@ -262,8 +260,12 @@ ExternalProject_Add(raft INSTALL_COMMAND "") set(RAFT_INCLUDE_DIR ${RAFT_GIT_DIR}/raft/cpp/include CACHE STRING "RAFT include variable") ``` +### C++ header-only integration (with cmake) -If RAFT has already been installed, such as by using the `build.sh` script, use `find_package(raft)` and the `raft::raft` target. + +When using cmake, you can install RAFT headers into your environment with `./build.sh libraft`. + +If the RAFT headers have already been installed into your environment with cmake or through conda, such as by using the `build.sh` script, use `find_package(raft)` and the `raft::raft` target. ### Using C++ pre-compiled shared libraries @@ -271,17 +273,19 @@ Use `find_package(raft COMPONENTS compiled distributed)` to enable the shared li The pre-compiled libraries contain template specializations for commonly used types, such as single- and double-precision floating-point. In order to use the symbols in the pre-compiled libraries, the compiler needs to be told not to instantiate templates that are already contained in the shared libraries. By convention, these header files are named `specializations.cuh` and located in the base directory for the packages that contain specializations. -The following example tells the compiler to ignore the pre-compiled templates for the `raft::distance` API so any symbols already compiled into the `libraft` shared library will be used instead: +The following example tells the compiler to ignore the pre-compiled templates for the `raft::distance` API so any symbols already compiled into the `libraft` shared library will be used instead. RAFT's cmake creates a variable `RAFT_COMPILED` which can be used to ignore the pre-compiled template specializations only when the shared library has been enabled through cmake (such as by specifying the `compiled` component in `find_package`): ```c++ +#ifdef RAFT_COMPILED #include #include +#endif ``` ### Building RAFT C++ from source in cmake RAFT uses the [RAPIDS-CMake](https://github.com/rapidsai/rapids-cmake) library so it can be more easily included into downstream projects. RAPIDS cmake provides a convenience layer around the [CMake Package Manager (CPM)](https://github.com/cpm-cmake/CPM.cmake). -The following example is similar to invoking `find_package(raft)` but uses `rapids_cpm_find`, which provides a richer and more flexible configuration landscape by using CPM to fetch any dependencies not already available to the build. The `raft::raft` link target will be made available and it's recommended that it be used as a `PRIVATE` link dependency in downstream projects. The `COMPILE_LIBRARIES` option enables the building the shared libraries. +The following example is similar to invoking `find_package(raft)` but uses `rapids_cpm_find`, which provides a richer and more flexible configuration landscape by using CPM to fetch any dependencies not already available to the build. The `raft::raft` link target will be made available and it's recommended that it be used as a `PRIVATE` link dependency in downstream projects. The `COMPILE_LIBRARY` option enables the building the shared libraries. The following `cmake` snippet enables a flexible configuration of RAFT: @@ -292,19 +296,10 @@ set(RAFT_FORK "rapidsai") set(RAFT_PINNED_TAG "branch-${RAFT_VERSION}") function(find_and_configure_raft) - set(oneValueArgs VERSION FORK PINNED_TAG COMPILE_LIBRARY CLONE_ON_PIN) + set(oneValueArgs VERSION FORK PINNED_TAG COMPILE_LIBRARY) cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) - - #----------------------------------------------------- - # Clone RAFT locally if PINNED_TAG has been changed - #----------------------------------------------------- - if(PKG_CLONE_ON_PIN AND NOT PKG_PINNED_TAG STREQUAL "branch-${RAFT_VERSION}") - message("Pinned tag found: ${PKG_PINNED_TAG}. Cloning raft locally.") - set(CPM_DOWNLOAD_raft ON) - set(CMAKE_IGNORE_PATH "${CMAKE_INSTALL_PREFIX}/include/raft;${CMAKE_IGNORE_PATH}) - endif() - + #----------------------------------------------------- # Invoke CPM find_package() #----------------------------------------------------- @@ -332,15 +327,12 @@ endfunction() find_and_configure_raft(VERSION ${RAFT_VERSION}.00 FORK ${RAFT_FORK} PINNED_TAG ${RAFT_PINNED_TAG} - - # When PINNED_TAG above doesn't match cuml, - # force local raft clone in build directory - # even if it's already installed. - CLONE_ON_PIN ON COMPILE_LIBRARY NO ) ``` +You can find a fully-functioning [example template project](../../cpp/template/README.md) in the `cpp/template` directory, which provides everything you need to build a new application with RAFT or incorporate RAFT Into your existing libraries. + ## Uninstall Once built and installed, RAFT can be safely uninstalled using `build.sh` by specifying any or all of the installed components. Please note that since `pylibraft` depends on `libraft`, uninstalling `pylibraft` will also uninstall `libraft`: diff --git a/python/pylibraft/setup.cfg b/python/pylibraft/setup.cfg new file mode 100644 index 0000000000..7d1a0c9065 --- /dev/null +++ b/python/pylibraft/setup.cfg @@ -0,0 +1,38 @@ +# Copyright (c) 2022-2023, NVIDIA CORPORATION. + +[isort] +line_length=79 +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +combine_as_imports=True +order_by_type=True +known_dask= + dask + distributed + dask_cuda +known_rapids= + nvtext + cudf + cuml + cugraph + dask_cudf + rmm +known_first_party= + raft + pylibraft +default_section=THIRDPARTY +sections=FUTURE,STDLIB,THIRDPARTY,DASK,RAPIDS,FIRSTPARTY,LOCALFOLDER +skip= + thirdparty + .eggs + .git + .hg + .mypy_cache + .tox + .venv + _build + buck-out + build + dist + __init__.py diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..e64641d05b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,55 @@ +# Copyright (c) 2022-2023, NVIDIA CORPORATION. + +[flake8] +filename = *.py, *.pyx, *.pxd, *.pxi +exclude = __init__.py, *.egg, build, docs, .git +force-check = True +ignore = + # line break before binary operator + W503, + # whitespace before : + E203 +per-file-ignores = + # Rules ignored only in Cython: + # E211: whitespace before '(' (used in multi-line imports) + # E225: Missing whitespace around operators (breaks cython casting syntax like ) + # E226: Missing whitespace around arithmetic operators (breaks cython pointer syntax like int*) + # E227: Missing whitespace around bitwise or shift operator (Can also break casting syntax) + # E275: Missing whitespace after keyword (Doesn't work with Cython except?) + # E402: invalid syntax (works for Python, not Cython) + # E999: invalid syntax (works for Python, not Cython) + # W504: line break after binary operator (breaks lines that end with a pointer) + *.pyx: E211, E225, E226, E227, E275, E402, E999, W504 + *.pxd: E211, E225, E226, E227, E275, E402, E999, W504 + *.pxi: E211, E225, E226, E227, E275, E402, E999, W504 + +[pydocstyle] +# Due to https://github.com/PyCQA/pydocstyle/issues/363, we must exclude rather +# than include using match-dir. Note that as discussed in +# https://stackoverflow.com/questions/65478393/how-to-filter-directories-using-the-match-dir-flag-for-pydocstyle, +# unlike the match option above this match-dir will have no effect when +# pydocstyle is invoked from pre-commit. Therefore this exclusion list must +# also be maintained in the pre-commit config file. +match-dir = ^(?!(ci|cpp|conda|docs|java|notebooks)).*$ +# Allow missing docstrings for docutils +ignore-decorators = .*(docutils|doc_apply|copy_docstring).* +select = + D201, D204, D206, D207, D208, D209, D210, D211, D214, D215, D300, D301, D302, D403, D405, D406, D407, D408, D409, D410, D411, D412, D414, D418 + # Would like to enable the following rules in the future: + # D200, D202, D205, D400 + +[mypy] +ignore_missing_imports = True +# If we don't specify this, then mypy will check excluded files if +# they are imported by a checked file. +follow_imports = skip + +[codespell] +# note: pre-commit passes explicit lists of files here, which this skip file list doesn't override - +# this is only to allow you to run codespell interactively +skip = ./.git,./.github,./cpp/build,.*egg-info.*,./.mypy_cache,.*_skbuild +# ignore short words, and typename parameters like OffsetT +ignore-regex = \b(.{1,4}|[A-Z]\w*T)\b +ignore-words-list = inout,unparseable,numer +builtin = clear +quiet-level = 3 From f4c7f1f94e135c511de67459ffac3d9d6e9f9794 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Fri, 24 Mar 2023 17:40:46 -0400 Subject: [PATCH 16/25] Adding some functions back in that seem to be a copy/paste error (#1373) Authors: - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Ben Frederickson (https://github.com/benfred) URL: https://github.com/rapidsai/raft/pull/1373 --- cpp/include/raft/util/cuda_dev_essentials.cuh | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cpp/include/raft/util/cuda_dev_essentials.cuh b/cpp/include/raft/util/cuda_dev_essentials.cuh index 5080dc33ee..bb9ebbba59 100644 --- a/cpp/include/raft/util/cuda_dev_essentials.cuh +++ b/cpp/include/raft/util/cuda_dev_essentials.cuh @@ -88,4 +88,30 @@ DI int laneId() return id; } +/** Device function to apply the input lambda across threads in the grid */ +template +DI void forEach(int num, L lambda) +{ + int idx = (blockDim.x * blockIdx.x) + threadIdx.x; + const int numThreads = blockDim.x * gridDim.x; +#pragma unroll + for (int itr = 0; itr < ItemsPerThread; ++itr, idx += numThreads) { + if (idx < num) lambda(idx, itr); + } +} + +/** + * @brief Swap two values + * @tparam T the datatype of the values + * @param a first input + * @param b second input + */ +template +HDI void swapVals(T& a, T& b) +{ + T tmp = a; + a = b; + b = tmp; +} + } // namespace raft From 76c828dd4da4dc922626ba2a440a46dea6ab03b9 Mon Sep 17 00:00:00 2001 From: Allard Hendriksen Date: Sat, 25 Mar 2023 20:09:35 +0100 Subject: [PATCH 17/25] Add extern template for ivfflat_interleaved_scan (#1360) This should cut compilation time for refine_d_int64_t_float.cu.o et al from ~900 seconds to 29 seconds. The refine specialization contain >100 instances of the ivfflat_interleaved_scan kernel, even though these should be seperately compiled by the ivfflat_search specializations. The call to ivf_flat_interleaved_scan is [here](https://github.com/rapidsai/raft/blob/56ac43ad93a319a61073dce1b3b937f6f13ade63/cpp/include/raft/neighbors/detail/refine.cuh#L121). Depends on (so please merge after) PR #1307. Authors: - Allard Hendriksen (https://github.com/ahendriksen) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1360 --- .../raft/neighbors/detail/ivf_flat_search.cuh | 8 ++++ cpp/include/raft/neighbors/detail/refine.cuh | 8 ++++ .../neighbors/specializations/ivf_flat.cuh | 25 ++++++++++++- .../ivfflat_search_float_int64_t.cu | 37 ++++++++++++++++--- .../ivfflat_search_int8_t_int64_t.cu | 28 +++++++++++--- .../ivfflat_search_uint8_t_int64_t.cu | 28 +++++++++++--- 6 files changed, 115 insertions(+), 19 deletions(-) diff --git a/cpp/include/raft/neighbors/detail/ivf_flat_search.cuh b/cpp/include/raft/neighbors/detail/ivf_flat_search.cuh index f657070df4..e6533eaf51 100644 --- a/cpp/include/raft/neighbors/detail/ivf_flat_search.cuh +++ b/cpp/include/raft/neighbors/detail/ivf_flat_search.cuh @@ -1065,6 +1065,14 @@ void ivfflat_interleaved_scan(const index& index, uint32_t& grid_dim_x, rmm::cuda_stream_view stream) { + // greppable-id-specializations-ivf-flat-search: The ivfflat_interleaved_scan + // function is used in both raft::neighbors::ivf_flat::search and + // raft::neighbors::detail::refine_device. To prevent a duplicate + // instantiation of this function (which defines ~270 kernels) in the refine + // specializations, an extern template definition is provided. Please check + // related function calls after editing this function definition. Search for + // `greppable-id-specializations-ivf-flat-search` to find them. + const int capacity = bound_by_power_of_two(k); select_interleaved_scan_kernel::run(capacity, index.veclen(), diff --git a/cpp/include/raft/neighbors/detail/refine.cuh b/cpp/include/raft/neighbors/detail/refine.cuh index f244d5875c..aedfc42698 100644 --- a/cpp/include/raft/neighbors/detail/refine.cuh +++ b/cpp/include/raft/neighbors/detail/refine.cuh @@ -117,6 +117,14 @@ void refine_device(raft::device_resources const& handle, n_queries, n_candidates); + // greppable-id-specializations-ivf-flat-search: The ivfflat_interleaved_scan + // function is used in both raft::neighbors::ivf_flat::search and + // raft::neighbors::detail::refine_device. To prevent a duplicate + // instantiation of this function (which defines ~270 kernels) in the refine + // specializations, an extern template definition is provided. Please check + // and adjust the extern template definition and the instantiation when the + // below function call is edited. Search for + // `greppable-id-specializations-ivf-flat-search` to find them. uint32_t grid_dim_x = 1; raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan< data_t, diff --git a/cpp/include/raft/neighbors/specializations/ivf_flat.cuh b/cpp/include/raft/neighbors/specializations/ivf_flat.cuh index 013c7359e5..161f3462c9 100644 --- a/cpp/include/raft/neighbors/specializations/ivf_flat.cuh +++ b/cpp/include/raft/neighbors/specializations/ivf_flat.cuh @@ -20,6 +20,13 @@ namespace raft::neighbors::ivf_flat { +// greppable-id-specializations-ivf-flat-search: The ivfflat_interleaved_scan +// function is used in both raft::neighbors::ivf_flat::search and +// raft::neighbors::detail::refine_device. To prevent a duplicate instantiation +// of this function (which defines ~270 kernels) in the refine specializations, +// an extern template definition is provided here. Please check related function +// calls after editing template definition below. Search for +// `greppable-id-specializations-ivf-flat-search` to find them. #define RAFT_INST(T, IdxT) \ extern template auto build(raft::device_resources const& handle, \ const index_params& params, \ @@ -44,7 +51,23 @@ namespace raft::neighbors::ivf_flat { const raft::neighbors::ivf_flat::index&, \ raft::device_matrix_view, \ raft::device_matrix_view, \ - raft::device_matrix_view); + raft::device_matrix_view); \ + \ + extern template void raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan< \ + T, \ + typename raft::spatial::knn::detail::utils::config::value_t, \ + IdxT>(const index& index, \ + const T* queries, \ + const uint32_t* coarse_query_results, \ + const uint32_t n_queries, \ + const raft::distance::DistanceType metric, \ + const uint32_t n_probes, \ + const uint32_t k, \ + const bool select_min, \ + IdxT* neighbors, \ + float* distances, \ + uint32_t& grid_dim_x, \ + rmm::cuda_stream_view stream); RAFT_INST(float, int64_t); RAFT_INST(int8_t, int64_t); diff --git a/cpp/src/neighbors/specializations/ivfflat_search_float_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_search_float_int64_t.cu index 6de65546c8..dce7083139 100644 --- a/cpp/src/neighbors/specializations/ivfflat_search_float_int64_t.cu +++ b/cpp/src/neighbors/specializations/ivfflat_search_float_int64_t.cu @@ -18,12 +18,37 @@ namespace raft::neighbors::ivf_flat { -#define RAFT_MAKE_INSTANCE(T, IdxT) \ - template void search(raft::device_resources const&, \ - raft::neighbors::ivf_flat::search_params const&, \ - const raft::neighbors::ivf_flat::index&, \ - raft::device_matrix_view, \ - raft::device_matrix_view, \ +// greppable-id-specializations-ivf-flat-search: The ivfflat_interleaved_scan +// function is used in both raft::neighbors::ivf_flat::search and +// raft::neighbors::detail::refine_device. To prevent a duplicate instantiation +// of this function (which defines ~270 kernels) in the refine specializations, +// an extern template definition is provided. To make sure +// ivfflat_interleaved_scan is actually compiled here, we explicitly instantiate +// it below. Please check related function calls after editing template +// definition below. Search for `greppable-id-specializations-ivf-flat-search` +// to find them. +#define RAFT_MAKE_INSTANCE(T, IdxT) \ + template void raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan< \ + T, \ + typename raft::spatial::knn::detail::utils::config::value_t, \ + IdxT>(const index& index, \ + const T* queries, \ + const uint32_t* coarse_query_results, \ + const uint32_t n_queries, \ + const raft::distance::DistanceType metric, \ + const uint32_t n_probes, \ + const uint32_t k, \ + const bool select_min, \ + IdxT* neighbors, \ + float* distances, \ + uint32_t& grid_dim_x, \ + rmm::cuda_stream_view stream); \ + \ + template void search(raft::device_resources const&, \ + raft::neighbors::ivf_flat::search_params const&, \ + const raft::neighbors::ivf_flat::index&, \ + raft::device_matrix_view, \ + raft::device_matrix_view, \ raft::device_matrix_view); RAFT_MAKE_INSTANCE(float, int64_t); diff --git a/cpp/src/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu index 8eda240ccd..b03d878bae 100644 --- a/cpp/src/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu +++ b/cpp/src/neighbors/specializations/ivfflat_search_int8_t_int64_t.cu @@ -18,12 +18,28 @@ namespace raft::neighbors::ivf_flat { -#define RAFT_MAKE_INSTANCE(T, IdxT) \ - template void search(raft::device_resources const&, \ - raft::neighbors::ivf_flat::search_params const&, \ - const raft::neighbors::ivf_flat::index&, \ - raft::device_matrix_view, \ - raft::device_matrix_view, \ +#define RAFT_MAKE_INSTANCE(T, IdxT) \ + template void raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan< \ + T, \ + typename raft::spatial::knn::detail::utils::config::value_t, \ + IdxT>(const index& index, \ + const T* queries, \ + const uint32_t* coarse_query_results, \ + const uint32_t n_queries, \ + const raft::distance::DistanceType metric, \ + const uint32_t n_probes, \ + const uint32_t k, \ + const bool select_min, \ + IdxT* neighbors, \ + float* distances, \ + uint32_t& grid_dim_x, \ + rmm::cuda_stream_view stream); \ + \ + template void search(raft::device_resources const&, \ + raft::neighbors::ivf_flat::search_params const&, \ + const raft::neighbors::ivf_flat::index&, \ + raft::device_matrix_view, \ + raft::device_matrix_view, \ raft::device_matrix_view); RAFT_MAKE_INSTANCE(int8_t, int64_t); diff --git a/cpp/src/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu b/cpp/src/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu index 8ff6533628..2d42bae0d1 100644 --- a/cpp/src/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu +++ b/cpp/src/neighbors/specializations/ivfflat_search_uint8_t_int64_t.cu @@ -18,12 +18,28 @@ namespace raft::neighbors::ivf_flat { -#define RAFT_MAKE_INSTANCE(T, IdxT) \ - template void search(raft::device_resources const&, \ - raft::neighbors::ivf_flat::search_params const&, \ - const raft::neighbors::ivf_flat::index&, \ - raft::device_matrix_view, \ - raft::device_matrix_view, \ +#define RAFT_MAKE_INSTANCE(T, IdxT) \ + template void raft::neighbors::ivf_flat::detail::ivfflat_interleaved_scan< \ + T, \ + typename raft::spatial::knn::detail::utils::config::value_t, \ + IdxT>(const index& index, \ + const T* queries, \ + const uint32_t* coarse_query_results, \ + const uint32_t n_queries, \ + const raft::distance::DistanceType metric, \ + const uint32_t n_probes, \ + const uint32_t k, \ + const bool select_min, \ + IdxT* neighbors, \ + float* distances, \ + uint32_t& grid_dim_x, \ + rmm::cuda_stream_view stream); \ + \ + template void search(raft::device_resources const&, \ + raft::neighbors::ivf_flat::search_params const&, \ + const raft::neighbors::ivf_flat::index&, \ + raft::device_matrix_view, \ + raft::device_matrix_view, \ raft::device_matrix_view); RAFT_MAKE_INSTANCE(uint8_t, int64_t); From 0d3bd3da5a2eb77a5ca2f7f9b9ed367030811c06 Mon Sep 17 00:00:00 2001 From: Ben Frederickson Date: Tue, 28 Mar 2023 01:00:57 -0700 Subject: [PATCH 18/25] add a distance epilogue function to the bfknn call (#1371) Add the ability for a user to specify an epilogue function to run after the distance in the brute_force::knn call. This lets us remove faiss from cuml, by updating the hdbscan reachability code (https://github.com/rapidsai/cuml/pull/5293) Authors: - Ben Frederickson (https://github.com/benfred) - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1371 --- cpp/include/raft/neighbors/brute_force.cuh | 32 ++++++------ .../raft/neighbors/detail/knn_brute_force.cuh | 51 ++++++++++++++----- .../neighbors/specializations/brute_force.cuh | 3 +- .../raft/spatial/knn/detail/ball_cover.cuh | 1 - .../brute_force_knn_int64_t_float.cu | 11 +--- .../brute_force_knn_impl_long_float_int.cu | 3 +- .../brute_force_knn_impl_long_float_uint.cu | 3 +- .../brute_force_knn_impl_uint_float_int.cu | 3 +- .../brute_force_knn_impl_uint_float_uint.cu | 3 +- cpp/test/neighbors/ball_cover.cu | 1 - cpp/test/neighbors/knn.cu | 2 +- 11 files changed, 69 insertions(+), 44 deletions(-) diff --git a/cpp/include/raft/neighbors/brute_force.cuh b/cpp/include/raft/neighbors/brute_force.cuh index 4891cc5f8d..dac1a29c7f 100644 --- a/cpp/include/raft/neighbors/brute_force.cuh +++ b/cpp/include/raft/neighbors/brute_force.cuh @@ -122,9 +122,8 @@ inline void knn_merge_parts( * * raft::raft::device_resources handle; * ... - * int k = 10; * auto metric = raft::distance::DistanceType::L2SqrtExpanded; - * brute_force::knn(handle, index, search, indices, distances, k, metric); + * brute_force::knn(handle, index, search, indices, distances, metric); * @endcode * * @param[in] handle: the cuml handle to use @@ -132,28 +131,31 @@ inline void knn_merge_parts( * @param[in] search: matrix (size n*d) to be used for searching the index * @param[out] indices: matrix (size n*k) to store output knn indices * @param[out] distances: matrix (size n*k) to store the output knn distance - * @param[in] k: the number of nearest neighbors to return * @param[in] metric: distance metric to use. Euclidean (L2) is used by default * @param[in] metric_arg: the value of `p` for Minkowski (l-p) distances. This * is ignored if the metric_type is not Minkowski. * @param[in] global_id_offset: optional starting global id mapping for the local partition * (assumes the index contains contiguous ids in the global id space) + * @param[in] distance_epilogue: optional epilogue function to run after computing distances. This + function takes a triple of the (value, rowid, colid) for each + element in the pairwise distances and returns a transformed value + back. */ template + typename search_layout, + typename epilogue_op = raft::identity_op> void knn(raft::device_resources const& handle, std::vector> index, raft::device_matrix_view search, raft::device_matrix_view indices, raft::device_matrix_view distances, - value_int k, distance::DistanceType metric = distance::DistanceType::L2Unexpanded, std::optional metric_arg = std::make_optional(2.0f), - std::optional global_id_offset = std::nullopt) + std::optional global_id_offset = std::nullopt, + epilogue_op distance_epilogue = raft::identity_op()) { RAFT_EXPECTS(index[0].extent(1) == search.extent(1), "Number of dimensions for both index and search matrices must be equal"); @@ -161,15 +163,14 @@ void knn(raft::device_resources const& handle, RAFT_EXPECTS(indices.extent(0) == distances.extent(0) && distances.extent(0) == search.extent(0), "Number of rows in output indices and distances matrices must equal number of rows " "in search matrix."); - RAFT_EXPECTS( - indices.extent(1) == distances.extent(1) && distances.extent(1) == static_cast(k), - "Number of columns in output indices and distances matrices must be equal to k"); + RAFT_EXPECTS(indices.extent(1) == distances.extent(1) && distances.extent(1), + "Number of columns in output indices and distances matrices must the same"); bool rowMajorIndex = std::is_same_v; bool rowMajorQuery = std::is_same_v; std::vector inputs; - std::vector sizes; + std::vector sizes; for (std::size_t i = 0; i < index.size(); ++i) { inputs.push_back(const_cast(index[i].data_handle())); sizes.push_back(index[i].extent(0)); @@ -183,18 +184,19 @@ void knn(raft::device_resources const& handle, raft::neighbors::detail::brute_force_knn_impl(handle, inputs, sizes, - static_cast(index[0].extent(1)), + index[0].extent(1), // TODO: This is unfortunate. Need to fix. const_cast(search.data_handle()), - static_cast(search.extent(0)), + search.extent(0), indices.data_handle(), distances.data_handle(), - k, + indices.extent(1), rowMajorIndex, rowMajorQuery, trans_arg, metric, - metric_arg.value_or(2.0f)); + metric_arg.value_or(2.0f), + distance_epilogue); } /** diff --git a/cpp/include/raft/neighbors/detail/knn_brute_force.cuh b/cpp/include/raft/neighbors/detail/knn_brute_force.cuh index 875fc3b37c..a776ce2586 100644 --- a/cpp/include/raft/neighbors/detail/knn_brute_force.cuh +++ b/cpp/include/raft/neighbors/detail/knn_brute_force.cuh @@ -47,7 +47,9 @@ using namespace raft::spatial::knn; * Calculates brute force knn, using a fixed memory budget * by tiling over both the rows and columns of pairwise_distances */ -template +template void tiled_brute_force_knn(const raft::device_resources& handle, const ElementType* search, // size (m ,d) const ElementType* index, // size (n ,d) @@ -58,9 +60,10 @@ void tiled_brute_force_knn(const raft::device_resources& handle, ElementType* distances, // size (m, k) IndexType* indices, // size (m, k) raft::distance::DistanceType metric, - float metric_arg = 0.0, - size_t max_row_tile_size = 0, - size_t max_col_tile_size = 0) + float metric_arg = 2.0, + size_t max_row_tile_size = 0, + size_t max_col_tile_size = 0, + DistanceEpilogue distance_epilogue = raft::identity_op()) { // Figure out the number of rows/cols to tile for size_t tile_rows = 0; @@ -152,25 +155,41 @@ void tiled_brute_force_knn(const raft::device_resources& handle, metric_arg); if (metric == raft::distance::DistanceType::L2Expanded || metric == raft::distance::DistanceType::L2SqrtExpanded) { - auto row_norms = search_norms.data() + i; - auto col_norms = index_norms.data() + j; + auto row_norms = search_norms.data(); + auto col_norms = index_norms.data(); auto dist = temp_distances.data(); raft::linalg::map_offset( handle, raft::make_device_vector_view(dist, current_query_size * current_centroid_size), - [=] __device__(IndexType i) { - IndexType row = i / current_centroid_size, col = i % current_centroid_size; + [=] __device__(IndexType idx) { + IndexType row = i + (idx / current_centroid_size); + IndexType col = j + (idx % current_centroid_size); - auto val = row_norms[row] + col_norms[col] - 2.0 * dist[i]; + auto val = row_norms[row] + col_norms[col] - 2.0 * dist[idx]; // due to numerical instability (especially around self-distance) // the distances here could be slightly negative, which will // cause NaN values in the subsequent sqrt. Clamp to 0 val = val * (val >= 0.0001); if (metric == raft::distance::DistanceType::L2SqrtExpanded) { val = sqrt(val); } + val = distance_epilogue(val, row, col); return val; }); + } else { + // if we're not l2 distance, and we have a distance epilogue - run it now + if constexpr (!std::is_same_v) { + auto distances_ptr = temp_distances.data(); + raft::linalg::map_offset( + handle, + raft::make_device_vector_view(temp_distances.data(), + current_query_size * current_centroid_size), + [=] __device__(size_t idx) { + IndexType row = i + (idx / current_centroid_size); + IndexType col = j + (idx % current_centroid_size); + return distance_epilogue(distances_ptr[idx], row, col); + }); + } } select_k(temp_distances.data(), @@ -250,7 +269,10 @@ void tiled_brute_force_knn(const raft::device_resources& handle, * @param[in] metric corresponds to the raft::distance::DistanceType enum (default is L2Expanded) * @param[in] metricArg metric argument to use. Corresponds to the p arg for lp norm */ -template +template void brute_force_knn_impl( raft::device_resources const& handle, std::vector& input, @@ -265,7 +287,8 @@ void brute_force_knn_impl( bool rowMajorQuery = true, std::vector* translations = nullptr, raft::distance::DistanceType metric = raft::distance::DistanceType::L2Expanded, - float metricArg = 0) + float metricArg = 0, + DistanceEpilogue distance_epilogue = raft::identity_op()) { auto userStream = handle.get_stream(); @@ -355,6 +378,7 @@ void brute_force_knn_impl( auto stream = handle.get_next_usable_stream(i); if (k <= 64 && rowMajorQuery == rowMajorIndex && rowMajorQuery == true && + std::is_same_v && (metric == raft::distance::DistanceType::L2Unexpanded || metric == raft::distance::DistanceType::L2SqrtUnexpanded || metric == raft::distance::DistanceType::L2Expanded || @@ -424,7 +448,10 @@ void brute_force_knn_impl( out_d_ptr, out_i_ptr, tiled_metric, - metricArg); + metricArg, + 0, + 0, + distance_epilogue); break; } } diff --git a/cpp/include/raft/neighbors/specializations/brute_force.cuh b/cpp/include/raft/neighbors/specializations/brute_force.cuh index d418d40185..1337beb68a 100644 --- a/cpp/include/raft/neighbors/specializations/brute_force.cuh +++ b/cpp/include/raft/neighbors/specializations/brute_force.cuh @@ -36,7 +36,8 @@ namespace raft::neighbors::detail { bool rowMajorQuery, \ std::vector* translations, \ raft::distance::DistanceType metric, \ - float metricArg); + float metricArg, \ + raft::identity_op); RAFT_INST(long, float, int); RAFT_INST(long, float, unsigned int); RAFT_INST(uint32_t, float, int); diff --git a/cpp/include/raft/spatial/knn/detail/ball_cover.cuh b/cpp/include/raft/spatial/knn/detail/ball_cover.cuh index 99d688e232..c8fc6eefda 100644 --- a/cpp/include/raft/spatial/knn/detail/ball_cover.cuh +++ b/cpp/include/raft/spatial/knn/detail/ball_cover.cuh @@ -185,7 +185,6 @@ void k_closest_landmarks(raft::device_resources const& handle, make_device_matrix_view(query_pts, n_query_pts, inputs[0].extent(1)), make_device_matrix_view(R_knn_inds, n_query_pts, k), make_device_matrix_view(R_knn_dists, n_query_pts, k), - k, index.get_metric()); } diff --git a/cpp/src/neighbors/brute_force_knn_int64_t_float.cu b/cpp/src/neighbors/brute_force_knn_int64_t_float.cu index 585084fc97..88545b3607 100644 --- a/cpp/src/neighbors/brute_force_knn_int64_t_float.cu +++ b/cpp/src/neighbors/brute_force_knn_int64_t_float.cu @@ -38,15 +38,8 @@ namespace raft::runtime::neighbors::brute_force { { \ std::vector> vec; \ vec.push_back(index); \ - raft::neighbors::brute_force::knn(handle, \ - vec, \ - search, \ - indices, \ - distances, \ - static_cast(distances.extent(1)), \ - metric, \ - metric_arg, \ - global_id_offset); \ + raft::neighbors::brute_force::knn( \ + handle, vec, search, indices, distances, metric, metric_arg, global_id_offset); \ } RAFT_INST_BFKNN(int64_t, float, int64_t, raft::row_major, raft::row_major); diff --git a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_int.cu b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_int.cu index 07810aa576..04aa42c9f1 100644 --- a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_int.cu +++ b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_int.cu @@ -32,7 +32,8 @@ namespace raft::neighbors::detail { bool rowMajorQuery, \ std::vector* translations, \ raft::distance::DistanceType metric, \ - float metricArg); + float metricArg, \ + raft::identity_op); RAFT_INST(long, float, int); #undef RAFT_INST } // namespace raft::neighbors::detail diff --git a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_uint.cu b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_uint.cu index 0cb873b40a..a8b9d4299a 100644 --- a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_uint.cu +++ b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_long_float_uint.cu @@ -32,7 +32,8 @@ namespace raft::neighbors::detail { bool rowMajorQuery, \ std::vector* translations, \ raft::distance::DistanceType metric, \ - float metricArg); + float metricArg, \ + raft::identity_op); RAFT_INST(long, float, unsigned int); #undef RAFT_INST } // namespace raft::neighbors::detail diff --git a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_int.cu b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_int.cu index f8a69b896f..c97e6e936a 100644 --- a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_int.cu +++ b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_int.cu @@ -32,7 +32,8 @@ namespace raft::neighbors::detail { bool rowMajorQuery, \ std::vector* translations, \ raft::distance::DistanceType metric, \ - float metricArg); + float metricArg, \ + raft::identity_op); RAFT_INST(uint32_t, float, int); #undef RAFT_INST } // namespace raft::neighbors::detail diff --git a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_uint.cu b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_uint.cu index 3c23d1f3e0..87451c385a 100644 --- a/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_uint.cu +++ b/cpp/src/neighbors/specializations/detail/brute_force_knn_impl_uint_float_uint.cu @@ -32,7 +32,8 @@ namespace raft::neighbors::detail { bool rowMajorQuery, \ std::vector* translations, \ raft::distance::DistanceType metric, \ - float metricArg); + float metricArg, \ + raft::identity_op); RAFT_INST(uint32_t, float, unsigned int); #undef RAFT_INST } // namespace raft::neighbors::detail diff --git a/cpp/test/neighbors/ball_cover.cu b/cpp/test/neighbors/ball_cover.cu index 9b51d585de..46ef3a9150 100644 --- a/cpp/test/neighbors/ball_cover.cu +++ b/cpp/test/neighbors/ball_cover.cu @@ -121,7 +121,6 @@ void compute_bfknn(const raft::device_resources& handle, make_device_matrix_view(X2, n_query_rows, d), make_device_matrix_view(inds, n_query_rows, k), make_device_matrix_view(dists, n_query_rows, k), - k, metric); } diff --git a/cpp/test/neighbors/knn.cu b/cpp/test/neighbors/knn.cu index 4bb977432c..bcd4b9cb0b 100644 --- a/cpp/test/neighbors/knn.cu +++ b/cpp/test/neighbors/knn.cu @@ -96,7 +96,7 @@ class KNNTest : public ::testing::TestWithParam { raft::make_device_matrix_view(distances_.data(), rows_, k_); auto metric = raft::distance::DistanceType::L2Unexpanded; - knn(handle, index, search, indices, distances, k_, metric, std::make_optional(0)); + knn(handle, index, search, indices, distances, metric, std::make_optional(0)); build_actual_output<<>>( actual_labels_.data(), rows_, k_, search_labels_.data(), indices_.data()); From 22ebc72e8c0c2738a30a0bbf9b0efc52736eddd6 Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Tue, 28 Mar 2023 15:54:57 -0400 Subject: [PATCH 19/25] Add end-to-end CUDA ann-benchmarks to raft (#1304) New `bench/ann` artifact for comparing (C++ APIs for) GPU-acclerated algorithms end-to-end. Working on this w/ @tfeher but had to squash the original commits into a single commit. Things left to do: - [x] Separate `benchmarks` executables for each different algorithm - [x] Separate build targets for `ggnn` and `hnswlib` - [x] Revise `bench/ann` docs - [x] Break `factory.cuh` abd `benchmark.cu` / `benchmark.cpp` into individual files for each different algorithm to make it easier to plug in new algorithms. - [x] Separate into its own conda package closes #1211 Authors: - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Ray Douglass (https://github.com/raydouglass) - Ben Frederickson (https://github.com/benfred) - Tamas Bela Feher (https://github.com/tfeher) URL: https://github.com/rapidsai/raft/pull/1304 --- .pre-commit-config.yaml | 2 +- build.sh | 68 +- ci/checks/copyright.py | 3 +- .../bench_ann_cuda-118_arch-x86_64.yaml | 37 + .../recipes/libraft/build_libraft_nn_bench.sh | 5 + conda/recipes/libraft/build_libraft_tests.sh | 2 +- conda/recipes/libraft/conda_build_config.yaml | 12 + conda/recipes/libraft/meta.yaml | 44 + cpp/CMakeLists.txt | 30 +- cpp/bench/ann/CMakeLists.txt | 160 ++ cpp/bench/ann/README.md | 3 + cpp/bench/ann/conf/bigann-100M.json | 174 +++ cpp/bench/ann/conf/deep-100M.json | 223 +++ cpp/bench/ann/conf/deep-1B.json | 38 + cpp/bench/ann/conf/glove-100-inner.json | 797 ++++++++++ cpp/bench/ann/conf/sift-128-euclidean.json | 1321 +++++++++++++++++ cpp/bench/ann/scripts/eval.pl | 430 ++++++ cpp/bench/ann/scripts/fbin_to_f16bin.py | 46 + cpp/bench/ann/scripts/hdf5_to_fbin.py | 85 ++ cpp/bench/ann/scripts/split_groundtruth.pl | 45 + cpp/bench/ann/src/common/ann_types.hpp | 88 ++ cpp/bench/ann/src/common/benchmark.hpp | 591 ++++++++ cpp/bench/ann/src/common/benchmark_util.hpp | 33 + cpp/bench/ann/src/common/conf.cpp | 151 ++ cpp/bench/ann/src/common/conf.h | 75 + cpp/bench/ann/src/common/dataset.h | 381 +++++ cpp/bench/ann/src/common/util.cpp | 68 + cpp/bench/ann/src/common/util.h | 79 + cpp/bench/ann/src/faiss/faiss_benchmark.cu | 150 ++ cpp/bench/ann/src/faiss/faiss_wrapper.h | 317 ++++ cpp/bench/ann/src/ggnn/ggnn_benchmark.cu | 125 ++ cpp/bench/ann/src/ggnn/ggnn_wrapper.cuh | 308 ++++ .../ann/src/hnswlib/hnswlib_benchmark.cpp | 120 ++ cpp/bench/ann/src/hnswlib/hnswlib_wrapper.h | 327 ++++ cpp/bench/ann/src/raft/raft_ann_bench_utils.h | 44 + cpp/bench/ann/src/raft/raft_benchmark.cu | 223 +++ cpp/bench/ann/src/raft/raft_ivf_flat.cu | 26 + .../ann/src/raft/raft_ivf_flat_wrapper.h | 142 ++ cpp/bench/ann/src/raft/raft_ivf_pq.cu | 26 + cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h | 214 +++ cpp/bench/ann/src/raft/raft_wrapper.h | 152 ++ cpp/bench/prims/CMakeLists.txt | 141 ++ cpp/bench/{ => prims}/cluster/kmeans.cu | 0 .../{ => prims}/cluster/kmeans_balanced.cu | 0 cpp/bench/{ => prims}/common/benchmark.hpp | 0 .../{ => prims}/distance/distance_common.cuh | 0 .../{ => prims}/distance/distance_cosine.cu | 2 +- .../{ => prims}/distance/distance_exp_l2.cu | 2 +- cpp/bench/{ => prims}/distance/distance_l1.cu | 2 +- .../{ => prims}/distance/distance_unexp_l2.cu | 2 +- cpp/bench/{ => prims}/distance/fused_l2_nn.cu | 0 cpp/bench/{ => prims}/distance/kernels.cu | 0 cpp/bench/{ => prims}/distance/masked_nn.cu | 0 .../distance/tune_pairwise/bench.cu | 0 .../distance/tune_pairwise/kernel.cu | 0 .../distance/tune_pairwise/kernel.cuh | 0 cpp/bench/{ => prims}/linalg/add.cu | 2 +- .../{ => prims}/linalg/map_then_reduce.cu | 2 +- .../{ => prims}/linalg/matrix_vector_op.cu | 2 +- cpp/bench/{ => prims}/linalg/norm.cu | 2 +- cpp/bench/{ => prims}/linalg/normalize.cu | 2 +- cpp/bench/{ => prims}/linalg/reduce.cu | 2 +- .../{ => prims}/linalg/reduce_cols_by_key.cu | 2 +- .../{ => prims}/linalg/reduce_rows_by_key.cu | 2 +- cpp/bench/{ => prims}/main.cpp | 2 +- cpp/bench/{ => prims}/matrix/argmin.cu | 0 cpp/bench/{ => prims}/matrix/gather.cu | 0 cpp/bench/{ => prims}/matrix/select_k.cu | 0 cpp/bench/{ => prims}/neighbors/knn.cuh | 0 .../knn/brute_force_float_int64_t.cu | 2 +- .../knn/brute_force_float_uint32_t.cu | 2 +- .../neighbors/knn/ivf_flat_float_int64_t.cu | 2 +- .../neighbors/knn/ivf_flat_int8_t_int64_t.cu | 2 +- .../neighbors/knn/ivf_flat_uint8_t_int64_t.cu | 0 .../neighbors/knn/ivf_pq_float_int64_t.cu | 0 .../neighbors/knn/ivf_pq_int8_t_int64_t.cu | 0 .../neighbors/knn/ivf_pq_uint8_t_int64_t.cu | 0 cpp/bench/{ => prims}/neighbors/refine.cuh | 0 .../neighbors/refine_float_int64_t.cu | 0 .../neighbors/refine_uint8_t_int64_t.cu | 0 cpp/bench/{ => prims}/random/make_blobs.cu | 2 +- cpp/bench/{ => prims}/random/permute.cu | 0 cpp/bench/{ => prims}/random/rng.cu | 2 +- cpp/bench/{ => prims}/sparse/convert_csr.cu | 0 cpp/cmake/modules/FindAVX.cmake | 110 ++ cpp/cmake/patches/ggnn.patch | 206 +++ cpp/cmake/patches/nlohmann_json.patch | 38 + cpp/cmake/thirdparty/get_faiss.cmake | 87 ++ cpp/cmake/thirdparty/get_ggnn.cmake | 44 + cpp/cmake/thirdparty/get_glog.cmake | 49 + cpp/cmake/thirdparty/get_hnswlib.cmake | 49 + cpp/cmake/thirdparty/get_nlohmann_json.cmake | 39 + .../raft/cluster/detail/kmeans_deprecated.cuh | 18 +- cpp/include/raft/linalg/detail/lanczos.cuh | 4 +- .../raft/solver/detail/lap_functions.cuh | 32 +- cpp/include/raft/solver/linear_assignment.cuh | 4 +- .../raft/sparse/solver/detail/lanczos.cuh | 4 +- .../raft/spectral/detail/matrix_wrappers.hpp | 2 +- cpp/include/raft/util/cudart_utils.hpp | 45 +- dependencies.yaml | 21 + docs/source/cuda_ann_benchmarks.md | 322 ++++ docs/source/index.rst | 1 + thirdparty/LICENSES/LICENSE.pytorch | 77 + 103 files changed, 8371 insertions(+), 125 deletions(-) create mode 100644 conda/environments/bench_ann_cuda-118_arch-x86_64.yaml create mode 100644 conda/recipes/libraft/build_libraft_nn_bench.sh create mode 100644 cpp/bench/ann/CMakeLists.txt create mode 100644 cpp/bench/ann/README.md create mode 100644 cpp/bench/ann/conf/bigann-100M.json create mode 100644 cpp/bench/ann/conf/deep-100M.json create mode 100644 cpp/bench/ann/conf/deep-1B.json create mode 100644 cpp/bench/ann/conf/glove-100-inner.json create mode 100644 cpp/bench/ann/conf/sift-128-euclidean.json create mode 100755 cpp/bench/ann/scripts/eval.pl create mode 100755 cpp/bench/ann/scripts/fbin_to_f16bin.py create mode 100755 cpp/bench/ann/scripts/hdf5_to_fbin.py create mode 100755 cpp/bench/ann/scripts/split_groundtruth.pl create mode 100644 cpp/bench/ann/src/common/ann_types.hpp create mode 100644 cpp/bench/ann/src/common/benchmark.hpp create mode 100644 cpp/bench/ann/src/common/benchmark_util.hpp create mode 100644 cpp/bench/ann/src/common/conf.cpp create mode 100644 cpp/bench/ann/src/common/conf.h create mode 100644 cpp/bench/ann/src/common/dataset.h create mode 100644 cpp/bench/ann/src/common/util.cpp create mode 100644 cpp/bench/ann/src/common/util.h create mode 100644 cpp/bench/ann/src/faiss/faiss_benchmark.cu create mode 100644 cpp/bench/ann/src/faiss/faiss_wrapper.h create mode 100644 cpp/bench/ann/src/ggnn/ggnn_benchmark.cu create mode 100644 cpp/bench/ann/src/ggnn/ggnn_wrapper.cuh create mode 100644 cpp/bench/ann/src/hnswlib/hnswlib_benchmark.cpp create mode 100644 cpp/bench/ann/src/hnswlib/hnswlib_wrapper.h create mode 100644 cpp/bench/ann/src/raft/raft_ann_bench_utils.h create mode 100644 cpp/bench/ann/src/raft/raft_benchmark.cu create mode 100644 cpp/bench/ann/src/raft/raft_ivf_flat.cu create mode 100644 cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h create mode 100644 cpp/bench/ann/src/raft/raft_ivf_pq.cu create mode 100644 cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h create mode 100644 cpp/bench/ann/src/raft/raft_wrapper.h create mode 100644 cpp/bench/prims/CMakeLists.txt rename cpp/bench/{ => prims}/cluster/kmeans.cu (100%) rename cpp/bench/{ => prims}/cluster/kmeans_balanced.cu (100%) rename cpp/bench/{ => prims}/common/benchmark.hpp (100%) rename cpp/bench/{ => prims}/distance/distance_common.cuh (100%) rename cpp/bench/{ => prims}/distance/distance_cosine.cu (94%) rename cpp/bench/{ => prims}/distance/distance_exp_l2.cu (94%) rename cpp/bench/{ => prims}/distance/distance_l1.cu (93%) rename cpp/bench/{ => prims}/distance/distance_unexp_l2.cu (94%) rename cpp/bench/{ => prims}/distance/fused_l2_nn.cu (100%) rename cpp/bench/{ => prims}/distance/kernels.cu (100%) rename cpp/bench/{ => prims}/distance/masked_nn.cu (100%) rename cpp/bench/{ => prims}/distance/tune_pairwise/bench.cu (100%) rename cpp/bench/{ => prims}/distance/tune_pairwise/kernel.cu (100%) rename cpp/bench/{ => prims}/distance/tune_pairwise/kernel.cuh (100%) rename cpp/bench/{ => prims}/linalg/add.cu (96%) rename cpp/bench/{ => prims}/linalg/map_then_reduce.cu (97%) rename cpp/bench/{ => prims}/linalg/matrix_vector_op.cu (99%) rename cpp/bench/{ => prims}/linalg/norm.cu (98%) rename cpp/bench/{ => prims}/linalg/normalize.cu (98%) rename cpp/bench/{ => prims}/linalg/reduce.cu (97%) rename cpp/bench/{ => prims}/linalg/reduce_cols_by_key.cu (98%) rename cpp/bench/{ => prims}/linalg/reduce_rows_by_key.cu (98%) rename cpp/bench/{ => prims}/main.cpp (92%) rename cpp/bench/{ => prims}/matrix/argmin.cu (100%) rename cpp/bench/{ => prims}/matrix/gather.cu (100%) rename cpp/bench/{ => prims}/matrix/select_k.cu (100%) rename cpp/bench/{ => prims}/neighbors/knn.cuh (100%) rename cpp/bench/{ => prims}/neighbors/knn/brute_force_float_int64_t.cu (93%) rename cpp/bench/{ => prims}/neighbors/knn/brute_force_float_uint32_t.cu (93%) rename cpp/bench/{ => prims}/neighbors/knn/ivf_flat_float_int64_t.cu (93%) rename cpp/bench/{ => prims}/neighbors/knn/ivf_flat_int8_t_int64_t.cu (93%) rename cpp/bench/{ => prims}/neighbors/knn/ivf_flat_uint8_t_int64_t.cu (100%) rename cpp/bench/{ => prims}/neighbors/knn/ivf_pq_float_int64_t.cu (100%) rename cpp/bench/{ => prims}/neighbors/knn/ivf_pq_int8_t_int64_t.cu (100%) rename cpp/bench/{ => prims}/neighbors/knn/ivf_pq_uint8_t_int64_t.cu (100%) rename cpp/bench/{ => prims}/neighbors/refine.cuh (100%) rename cpp/bench/{ => prims}/neighbors/refine_float_int64_t.cu (100%) rename cpp/bench/{ => prims}/neighbors/refine_uint8_t_int64_t.cu (100%) rename cpp/bench/{ => prims}/random/make_blobs.cu (98%) rename cpp/bench/{ => prims}/random/permute.cu (100%) rename cpp/bench/{ => prims}/random/rng.cu (98%) rename cpp/bench/{ => prims}/sparse/convert_csr.cu (100%) create mode 100644 cpp/cmake/modules/FindAVX.cmake create mode 100644 cpp/cmake/patches/ggnn.patch create mode 100644 cpp/cmake/patches/nlohmann_json.patch create mode 100644 cpp/cmake/thirdparty/get_faiss.cmake create mode 100644 cpp/cmake/thirdparty/get_ggnn.cmake create mode 100644 cpp/cmake/thirdparty/get_glog.cmake create mode 100644 cpp/cmake/thirdparty/get_hnswlib.cmake create mode 100644 cpp/cmake/thirdparty/get_nlohmann_json.cmake create mode 100644 docs/source/cuda_ann_benchmarks.md create mode 100644 thirdparty/LICENSES/LICENSE.pytorch diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 630b8788f8..d6e4ecb676 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,7 +62,7 @@ repos: entry: ./cpp/scripts/run-cmake-format.sh cmake-format language: python types: [cmake] - exclude: .*/thirdparty/.* + exclude: .*/thirdparty/.*|.*FindAVX.cmake.* # Note that pre-commit autoupdate does not update the versions # of dependencies, so we'll have to update this manually. additional_dependencies: diff --git a/build.sh b/build.sh index 9468d2cab0..3758dc26c4 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ # Copyright (c) 2020-2023, NVIDIA CORPORATION. -# raft build script +# raft build scripts # This script is used to build the component(s) in this repo from # source, and can be called with various options to customize the @@ -15,11 +15,11 @@ NUMARGS=$# ARGS=$* # NOTE: ensure all dir changes are relative to the location of this -# script, and that this script resides in the repo dir! +# scripts, and that this script resides in the repo dir! REPODIR=$(cd $(dirname $0); pwd) -VALIDARGS="clean libraft pylibraft raft-dask docs tests bench template clean --uninstall -v -g -n --compile-lib --allgpuarch --no-nvtx --show_depr_warn -h" -HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=] [--limit-tests=] [--limit-bench=] +VALIDARGS="clean libraft pylibraft raft-dask docs tests template bench-prims bench-ann clean --uninstall -v -g -n --compile-lib --allgpuarch --no-nvtx --show_depr_warn -h" +HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=] [--limit-tests=] [--limit-bench-prims=] [--limit-bench-ann=] where is: clean - remove all existing build artifacts and configuration (start over) libraft - build the raft C++ code only. Also builds the C-wrapper library @@ -28,7 +28,8 @@ HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool= is: @@ -39,7 +40,8 @@ HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool==3.23.1,!=3.25.0 +- cuda-profiler-api=11.8.86 +- cudatoolkit=11.8 +- cxx-compiler +- cython>=0.29,<0.30 +- faiss-proc=*=cuda +- gcc_linux-64=11.* +- glog>=0.6.0 +- h5py>=3.8.0 +- hnswlib=0.7.0 +- libcublas-dev=11.11.3.6 +- libcublas=11.11.3.6 +- libcurand-dev=10.3.0.86 +- libcurand=10.3.0.86 +- libcusolver-dev=11.4.1.48 +- libcusolver=11.4.1.48 +- libcusparse-dev=11.7.5.86 +- libcusparse=11.7.5.86 +- libfaiss>=1.7.1 +- nccl>=2.9.9 +- ninja +- nlohmann_json>=3.11.2 +- scikit-build>=0.13.1 +- sysroot_linux-64==2.17 +name: bench_ann_cuda-118_arch-x86_64 diff --git a/conda/recipes/libraft/build_libraft_nn_bench.sh b/conda/recipes/libraft/build_libraft_nn_bench.sh new file mode 100644 index 0000000000..dc6250f0f4 --- /dev/null +++ b/conda/recipes/libraft/build_libraft_nn_bench.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Copyright (c) 2023, NVIDIA CORPORATION. + +./build.sh tests bench-ann --allgpuarch --no-nvtx +cmake --install cpp/build --component ann_bench diff --git a/conda/recipes/libraft/build_libraft_tests.sh b/conda/recipes/libraft/build_libraft_tests.sh index aa2c1b3e89..cc28f93fb8 100644 --- a/conda/recipes/libraft/build_libraft_tests.sh +++ b/conda/recipes/libraft/build_libraft_tests.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash # Copyright (c) 2022-2023, NVIDIA CORPORATION. -./build.sh tests bench --allgpuarch --no-nvtx +./build.sh tests bench-prims --allgpuarch --no-nvtx cmake --install cpp/build --component testing diff --git a/conda/recipes/libraft/conda_build_config.yaml b/conda/recipes/libraft/conda_build_config.yaml index e1079f4db8..2a66f213a7 100644 --- a/conda/recipes/libraft/conda_build_config.yaml +++ b/conda/recipes/libraft/conda_build_config.yaml @@ -19,6 +19,18 @@ nccl_version: gtest_version: - "=1.10.0" +glog_version: + - ">=0.6.0" + +faiss_version: + - ">=1.7.1" + +h5py_version: + - ">=3.8.0" + +nlohmann_json_version: + - ">=3.11.2" + # The CTK libraries below are missing from the conda-forge::cudatoolkit # package. The "*_host_*" version specifiers correspond to `11.8` packages and the # "*_run_*" version specifiers correspond to `11.x` packages. diff --git a/conda/recipes/libraft/meta.yaml b/conda/recipes/libraft/meta.yaml index f911166a9a..7859807777 100644 --- a/conda/recipes/libraft/meta.yaml +++ b/conda/recipes/libraft/meta.yaml @@ -186,3 +186,47 @@ outputs: home: https://rapids.ai/ license: Apache-2.0 summary: libraft template + - name: libraft-ann-bench + version: {{ version }} + script: build_libraft_nn_bench.sh + build: + script_env: *script_env + number: {{ GIT_DESCRIBE_NUMBER }} + string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }} + ignore_run_exports_from: + - {{ compiler('cuda') }} + requirements: + build: + - {{ compiler('c') }} + - {{ compiler('cuda') }} {{ cuda_version }} + - {{ compiler('cxx') }} + - cmake {{ cmake_version }} + - ninja + - sysroot_{{ target_platform }} {{ sysroot_version }} + host: + - {{ pin_subpackage('libraft', exact=True) }} + - {{ pin_subpackage('libraft-headers', exact=True) }} + - cuda-profiler-api {{ cuda_profiler_api_host_version }} + - libcublas {{ libcublas_host_version }} + - libcublas-dev {{ libcublas_host_version }} + - libcurand {{ libcurand_host_version }} + - libcurand-dev {{ libcurand_host_version }} + - libcusolver {{ libcusolver_host_version }} + - libcusolver-dev {{ libcusolver_host_version }} + - libcusparse {{ libcusparse_host_version }} + - libcusparse-dev {{ libcusparse_host_version }} + - glog {{ glog_version }} + - nlohmann_json {{ nlohmann_json_version }} + - libfaiss>=1.7.1 + - faiss-proc=*=cuda + run: + - {{ pin_subpackage('libraft', exact=True) }} + - {{ pin_subpackage('libraft-headers', exact=True) }} + - glog {{ glog_version }} + - faiss-proc=*=cuda + - libfaiss {{ faiss_version }} + - h5py {{ h5py_version }} + about: + home: https://rapids.ai/ + license: Apache-2.0 + summary: libraft ann bench diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index c1704552ec..7bb458c44a 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -46,7 +46,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(BUILD_SHARED_LIBS "Build raft shared libraries" ON) option(BUILD_TESTS "Build raft unit-tests" ON) -option(BUILD_BENCH "Build raft C++ benchmark tests" OFF) +option(BUILD_PRIMS_BENCH "Build raft C++ benchmark tests" OFF) +option(BUILD_ANN_BENCH "Build raft ann benchmarks" OFF) option(CUDA_ENABLE_KERNELINFO "Enable kernel resource usage info" OFF) option(CUDA_ENABLE_LINEINFO "Enable the -lineinfo option for nvcc (useful for cuda-memcheck / profiler)" OFF @@ -58,14 +59,20 @@ option(DISABLE_OPENMP "Disable OpenMP" OFF) option(RAFT_NVTX "Enable nvtx markers" OFF) set(RAFT_COMPILE_LIBRARY_DEFAULT OFF) -if(BUILD_TESTS OR BUILD_BENCH) +if(BUILD_TESTS + OR BUILD_PRIMS_BENCH + OR BUILD_ANN_BENCH +) set(RAFT_COMPILE_LIBRARY_DEFAULT ON) endif() option(RAFT_COMPILE_LIBRARY "Enable building raft shared library instantiations" ${RAFT_COMPILE_LIBRARY_DEFAULT} ) -if(BUILD_TESTS OR BUILD_BENCH) +if(BUILD_TESTS + OR BUILD_PRIMS_BENCH + OR BUILD_ANN_BENCH +) # Needed because GoogleBenchmark changes the state of FindThreads.cmake, causing subsequent runs # to have different values for the `Threads::Threads` target. Setting this flag ensures # `Threads::Threads` is the same value in first run and subsequent runs. @@ -78,7 +85,7 @@ include(CMakeDependentOption) message(VERBOSE "RAFT: Building optional components: ${raft_FIND_COMPONENTS}") message(VERBOSE "RAFT: Build RAFT unit-tests: ${BUILD_TESTS}") -message(VERBOSE "RAFT: Building raft C++ benchmarks: ${BUILD_BENCH}") +message(VERBOSE "RAFT: Building raft C++ benchmarks: ${BUILD_PRIMS_BENCH}") message(VERBOSE "RAFT: Enable detection of conda environment for dependencies: ${DETECT_CONDA_ENV}") message(VERBOSE "RAFT: Disable depreaction warnings " ${DISABLE_DEPRECATION_WARNINGS}) message(VERBOSE "RAFT: Disable OpenMP: ${DISABLE_OPENMP}") @@ -159,7 +166,7 @@ if(BUILD_TESTS) include(cmake/thirdparty/get_gtest.cmake) endif() -if(BUILD_BENCH) +if(BUILD_PRIMS_BENCH) include(${rapids-cmake-dir}/cpm/gbench.cmake) rapids_cpm_gbench() endif() @@ -647,7 +654,7 @@ raft_export( # ################################################################################################## # * shared test/bench headers ------------------------------------------------ -if(BUILD_TESTS OR BUILD_BENCH) +if(BUILD_TESTS OR BUILD_PRIMS_BENCH) include(internal/CMakeLists.txt) endif() @@ -661,6 +668,13 @@ endif() # ################################################################################################## # * build benchmark executable ----------------------------------------------- -if(BUILD_BENCH) - include(bench/CMakeLists.txt) +if(BUILD_PRIMS_BENCH) + include(bench/prims/CMakeLists.txt) +endif() + +# ################################################################################################## +# * build ann benchmark executable ----------------------------------------------- + +if(BUILD_ANN_BENCH) + include(bench/ann/CMakeLists.txt) endif() diff --git a/cpp/bench/ann/CMakeLists.txt b/cpp/bench/ann/CMakeLists.txt new file mode 100644 index 0000000000..6267be518e --- /dev/null +++ b/cpp/bench/ann/CMakeLists.txt @@ -0,0 +1,160 @@ +# ============================================================================= +# Copyright (c) 2023, NVIDIA CORPORATION. +# +# 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. +# ============================================================================= + +# ################################################################################################## +# * compiler function ----------------------------------------------------------------------------- + +option(RAFT_ANN_BENCH_USE_FAISS_BFKNN "Include faiss' brute-force knn algorithm in benchmark" ON) +option(RAFT_ANN_BENCH_USE_FAISS_IVF_FLAT "Include faiss' ivf flat algorithm in benchmark" ON) +option(RAFT_ANN_BENCH_USE_FAISS_IVF_PQ "Include faiss' ivf pq algorithm in benchmark" ON) +option(RAFT_ANN_BENCH_USE_RAFT_BFKNN "Include raft's brute-force knn algorithm in benchmark" ON) +option(RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT "Include raft's ivf flat algorithm in benchmark" ON) +option(RAFT_ANN_BENCH_USE_RAFT_IVF_PQ "Include raft's ivf pq algorithm in benchmark" ON) +option(RAFT_ANN_BENCH_USE_HNSWLIB "Include hnsw algorithm in benchmark" ON) +option(RAFT_ANN_BENCH_USE_GGNN "Include ggnn algorithm in benchmark" ON) + +find_package(Threads REQUIRED) + +set(RAFT_ANN_BENCH_USE_FAISS OFF) +if(RAFT_ANN_BENCH_USE_FAISS_BFKNN + OR RAFT_ANN_BENCH_USE_FAISS_IVFPQ + OR RAFT_ANN_BENCH_USE_FAISS_IFFLAT +) + set(RAFT_ANN_BENCH_USE_FAISS ON) +endif() + +set(RAFT_ANN_BENCH_USE_RAFT OFF) +if(RAFT_ANN_BENCH_USE_RAFT_BFKNN + OR RAFT_ANN_BENCH_USE_RAFT_IVFPQ + OR RAFT_ANN_BENCH_USE_RAFT_IVFFLAT +) + set(RAFT_ANN_BENCH_USE_RAFT ON) +endif() + +if(RAFT_ANN_BENCH_USE_HNSWLIB) + include(cmake/thirdparty/get_hnswlib.cmake) +endif() + +option(RAFT_ANN_BENCH_USE_MULTIGPU "Use multi-gpus (where possible) in benchmarks" OFF) + +include(cmake/thirdparty/get_nlohmann_json.cmake) + +if(RAFT_ANN_BENCH_USE_GGNN) + include(cmake/thirdparty/get_ggnn.cmake) +endif() + +if(RAFT_ANN_BENCH_USE_FAISS) + include(cmake/thirdparty/get_faiss.cmake) +endif() + +function(ConfigureAnnBench) + + set(oneValueArgs NAME) + set(multiValueArgs PATH LINKS CXXFLAGS INCLUDES) + + cmake_parse_arguments( + ConfigureAnnBench "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} + ) + + set(BENCH_NAME ${ConfigureAnnBench_NAME}_ANN_BENCH) + + add_executable( + ${BENCH_NAME} ${ConfigureAnnBench_PATH} bench/ann/src/common/conf.cpp + bench/ann/src/common/util.cpp + ) + target_link_libraries( + ${BENCH_NAME} + PRIVATE raft::raft + nlohmann_json::nlohmann_json + $<$:NCCL::NCCL> + ${ConfigureAnnBench_LINKS} + Threads::Threads + $ + $ + ) + + set_target_properties( + ${BENCH_NAME} + PROPERTIES # set target compile options + INSTALL_RPATH "\$ORIGIN/../../../lib" + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CUDA_STANDARD 17 + CUDA_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + INTERFACE_POSITION_INDEPENDENT_CODE ON + ) + + set(${ConfigureAnnBench_CXXFLAGS} ${RAFT_CXX_FLAGS} ${ConfigureAnnBench_CXXFLAGS}) + + target_compile_options( + ${BENCH_NAME} PRIVATE "$<$:${ConfigureAnnBench_CXXFLAGS}>" + "$<$:${RAFT_CUDA_FLAGS}>" + ) + + if(RAFT_ANN_BENCH_USE_${ConfigureAnnBench_NAME}) + target_compile_definitions( + ${BENCH_NAME} + PUBLIC + RAFT_ANN_BENCH_USE_${ConfigureAnnBench_NAME}=RAFT_ANN_BENCH_USE_${ConfigureAnnBench_NAME} + ) + endif() + + target_include_directories( + ${BENCH_NAME} + PUBLIC "$" + PRIVATE ${ConfigureAnnBench_INCLUDES} + ) + + install( + TARGETS ${BENCH_NAME} + COMPONENT ann_bench + DESTINATION bin/ann + EXCLUDE_FROM_ALL + ) +endfunction() + +if(RAFT_ANN_BENCH_USE_HNSWLIB) + ConfigureAnnBench( + NAME HNSWLIB PATH bench/ann/src/hnswlib/hnswlib_benchmark.cpp INCLUDES + ${CMAKE_CURRENT_BINARY_DIR}/_deps/hnswlib-src/hnswlib CXXFLAGS "${HNSW_CXX_FLAGS}" + ) +endif() + +if(RAFT_ANN_BENCH_USE_RAFT) + ConfigureAnnBench( + NAME + RAFT_IVF_PQ + PATH + bench/ann/src/raft/raft_benchmark.cu + $<$:bench/ann/src/raft/raft_ivf_pq.cu> + $<$:bench/ann/src/raft/raft_ivf_flat.cu> + LINKS + raft::compiled + ) +endif() + +if(RAFT_ANN_BENCH_USE_FAISS) + ConfigureAnnBench( + NAME FAISS_IVF_FLAT PATH bench/ann/src/faiss/faiss_benchmark.cu LINKS faiss::faiss + ) +endif() + +if(RAFT_ANN_BENCH_USE_GGNN) + include(cmake/thirdparty/get_glog.cmake) + ConfigureAnnBench( + NAME GGNN PATH bench/ann/src/ggnn/ggnn_benchmark.cu INCLUDES + ${CMAKE_CURRENT_BINARY_DIR}/_deps/ggnn-src/include LINKS glog::glog + ) +endif() diff --git a/cpp/bench/ann/README.md b/cpp/bench/ann/README.md new file mode 100644 index 0000000000..1a8af2e448 --- /dev/null +++ b/cpp/bench/ann/README.md @@ -0,0 +1,3 @@ +# RAFT CUDA ANN Benchmarks + +Please see the [ANN Benchmarks](https://docs.rapids.ai/api/raft/stable/cuda_ann_benchmarks.html) section of the RAFT documentation for instructions on building and using the ANN benchmarks. \ No newline at end of file diff --git a/cpp/bench/ann/conf/bigann-100M.json b/cpp/bench/ann/conf/bigann-100M.json new file mode 100644 index 0000000000..5f16f3378d --- /dev/null +++ b/cpp/bench/ann/conf/bigann-100M.json @@ -0,0 +1,174 @@ +{ + "dataset" : { + "name" : "bigann-100M", + "base_file" : "data/bigann-1B/base.1B.u8bin", + "subset_size" : 100000000, + "query_file" : "data/bigann-1B/query.public.10K.u8bin", + "distance" : "euclidean" + }, + + "search_basic_param" : { + "batch_size" : 10000, + "k" : 10, + "run_count" : 2 + }, + + "index" : [ + { + "name": "raft_ivf_pq.dimpq64-cluster5K-float-float", + "algo": "raft_ivf_pq", + "build_param": { + "niter": 25, + "nlist": 5000, + "pq_dim": 64, + "ratio": 10 + }, + "file": "index/bigann-100M/raft_ivf_pq/dimpq64-cluster5K", + "search_params": [ + { + "numProbes": 20, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "numProbes": 30, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "numProbes": 40, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "numProbes": 50, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "numProbes": 100, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "numProbes": 200, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "numProbes": 500, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "numProbes": 1000, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + } + ], + "search_result_file": "result/bigann-100M/raft_ivf_pq/dimpq64-cluster5K-float-float" + }, + { + "name" : "hnswlib.M12", + "algo" : "hnswlib", + "build_param": {"M":12, "efConstruction":500, "numThreads":32}, + "file" : "index/bigann-100M/hnswlib/M12", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/bigann-100M/hnswlib/M12" + }, + { + "name" : "hnswlib.M16", + "algo" : "hnswlib", + "build_param": {"M":16, "efConstruction":500, "numThreads":32}, + "file" : "index/bigann-100M/hnswlib/M16", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/bigann-100M/hnswlib/M16" + }, + { + "name" : "hnswlib.M24", + "algo" : "hnswlib", + "build_param": {"M":24, "efConstruction":500, "numThreads":32}, + "file" : "index/bigann-100M/hnswlib/M24", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/bigann-100M/hnswlib/M24" + }, + { + "name" : "hnswlib.M36", + "algo" : "hnswlib", + "build_param": {"M":36, "efConstruction":500, "numThreads":32}, + "file" : "index/bigann-100M/hnswlib/M36", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/bigann-100M/hnswlib/M36" + }, + + + { + "name" : "ivf_flat.nlist100K", + "algo" : "ivf_flat", + "build_param": { + "nlist" : 100000, + "niter" : 25, + "ratio" : 5 + }, + "file" : "index/bigann-100M/ivf_flat/nlist100K", + "search_params" : [ + {"max_batch":10000, "max_k":10, "nprobe":20}, + {"max_batch":10000, "max_k":10, "nprobe":30}, + {"max_batch":10000, "max_k":10, "nprobe":40}, + {"max_batch":10000, "max_k":10, "nprobe":50}, + {"max_batch":10000, "max_k":10, "nprobe":100}, + {"max_batch":10000, "max_k":10, "nprobe":200}, + {"max_batch":10000, "max_k":10, "nprobe":500}, + {"max_batch":10000, "max_k":10, "nprobe":1000} + ], + "search_result_file" : "result/bigann-100M/ivf_flat/nlist100K" + }, + + + + ] +} diff --git a/cpp/bench/ann/conf/deep-100M.json b/cpp/bench/ann/conf/deep-100M.json new file mode 100644 index 0000000000..b3a945d50e --- /dev/null +++ b/cpp/bench/ann/conf/deep-100M.json @@ -0,0 +1,223 @@ +{ + "dataset" : { + "name" : "deep-100M", + "base_file" : "data/deep-1B/base.1B.fbin", + "subset_size" : 100000000, + "query_file" : "data/deep-1B/query.public.10K.fbin", + "distance" : "euclidean" + }, + + "search_basic_param" : { + "batch_size" : 10000, + "k" : 10, + "run_count" : 2 + }, + + "index" : [ + { + "name" : "hnswlib.M12", + "algo" : "hnswlib", + "build_param": {"M":12, "efConstruction":500, "numThreads":32}, + "file" : "index/deep-100M/hnswlib/M12", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/deep-100M/hnswlib/M12" + }, + { + "name" : "hnswlib.M16", + "algo" : "hnswlib", + "build_param": {"M":16, "efConstruction":500, "numThreads":32}, + "file" : "index/deep-100M/hnswlib/M16", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/deep-100M/hnswlib/M16" + }, + { + "name" : "hnswlib.M24", + "algo" : "hnswlib", + "build_param": {"M":24, "efConstruction":500, "numThreads":32}, + "file" : "index/deep-100M/hnswlib/M24", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/deep-100M/hnswlib/M24" + }, + { + "name" : "hnswlib.M36", + "algo" : "hnswlib", + "build_param": {"M":36, "efConstruction":500, "numThreads":32}, + "file" : "index/deep-100M/hnswlib/M36", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/deep-100M/hnswlib/M36" + }, + { + "name" : "faiss_ivf_flat.nlist50K", + "algo" : "faiss_gpu_ivf_flat", + "build_param": {"nlist":50000}, + "file" : "index/deep-100M/faiss_ivf_flat/nlist50K", + "search_params" : [ + {"nprobe":20}, + {"nprobe":30}, + {"nprobe":40}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/deep-100M/faiss_ivf_flat/nlist50K" + }, + { + "name" : "faiss_ivf_flat.nlist100K", + "algo" : "faiss_gpu_ivf_flat", + "build_param": {"nlist":100000}, + "file" : "index/deep-100M/faiss_ivf_flat/nlist100K", + "search_params" : [ + {"nprobe":20}, + {"nprobe":30}, + {"nprobe":40}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/deep-100M/faiss_ivf_flat/nlist100K" + }, + { + "name" : "faiss_ivf_flat.nlist200K", + "algo" : "faiss_gpu_ivf_flat", + "build_param": {"nlist":200000}, + "file" : "index/deep-100M/faiss_ivf_flat/nlist200K", + "search_params" : [ + {"nprobe":20}, + {"nprobe":30}, + {"nprobe":40}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/deep-100M/faiss_ivf_flat/nlist200K" + }, + + + { + "name" : "faiss_ivf_pq.M48-nlist16K", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":16384, "M":48}, + "file" : "index/deep-100M/faiss_ivf_pq/M48-nlist16K", + "search_params" : [ + {"nprobe":10}, + {"nprobe":20}, + {"nprobe":30}, + {"nprobe":40}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500} + ], + "search_result_file" : "result/deep-100M/faiss_ivf_pq/M48-nlist16K" + }, + { + "name" : "faiss_ivf_pq.M48-nlist50K", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":50000, "M":48}, + "file" : "index/deep-100M/faiss_ivf_pq/M48-nlist50K", + "search_params" : [ + {"nprobe":20}, + {"nprobe":30}, + {"nprobe":40}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/deep-100M/faiss_ivf_pq/M48-nlist50K" + }, + { + "name" : "faiss_ivf_pq.M48-nlist100K", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":100000, "M":48}, + "file" : "index/deep-100M/faiss_ivf_pq/M48-nlist100K", + "search_params" : [ + {"nprobe":20}, + {"nprobe":30}, + {"nprobe":40}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/deep-100M/faiss_ivf_pq/M48-nlist100K" + }, + + + { + "name" : "ivf_flat.nlist100K", + "algo" : "ivf_flat", + "build_param": { + "nlist" : 100000, + "niter" : 25, + "ratio" : 5 + }, + "file" : "index/deep-100M/ivf_flat/nlist100K", + "search_params" : [ + {"max_batch":10000, "max_k":10, "nprobe":20}, + {"max_batch":10000, "max_k":10, "nprobe":30}, + {"max_batch":10000, "max_k":10, "nprobe":40}, + {"max_batch":10000, "max_k":10, "nprobe":50}, + {"max_batch":10000, "max_k":10, "nprobe":100}, + {"max_batch":10000, "max_k":10, "nprobe":200}, + {"max_batch":10000, "max_k":10, "nprobe":500}, + {"max_batch":10000, "max_k":10, "nprobe":1000} + ], + "search_result_file" : "result/deep-100M/ivf_flat/nlist100K" + }, + + + ] +} diff --git a/cpp/bench/ann/conf/deep-1B.json b/cpp/bench/ann/conf/deep-1B.json new file mode 100644 index 0000000000..50d1b87602 --- /dev/null +++ b/cpp/bench/ann/conf/deep-1B.json @@ -0,0 +1,38 @@ +{ + "dataset" : { + "name" : "deep-1B", + "base_file" : "data/deep-1B/base.1B.fbin", + "query_file" : "data/deep-1B/query.public.10K.fbin", + // although distance should be "euclidean", faiss becomes much slower for that + "distance" : "inner_product" + }, + + "search_basic_param" : { + "batch_size" : 10000, + "k" : 10, + "run_count" : 2 + }, + + "index" : [ + { + "name" : "faiss_ivf_pq.M48-nlist50K", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":50000, "M":48}, + "file" : "index/deep-1B/faiss_ivf_pq/M48-nlist50K", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000}, + {"nprobe":2000} + ], + "search_result_file" : "result/deep-1B/faiss_ivf_pq/M48-nlist50K" + }, + + + ] +} diff --git a/cpp/bench/ann/conf/glove-100-inner.json b/cpp/bench/ann/conf/glove-100-inner.json new file mode 100644 index 0000000000..d210aca654 --- /dev/null +++ b/cpp/bench/ann/conf/glove-100-inner.json @@ -0,0 +1,797 @@ +{ + "dataset" : { + "name" : "glove-100-inner", + "base_file" : "data/glove-100-inner/base.fbin", + "query_file" : "data/glove-100-inner/query.fbin", + "distance" : "inner_product" + }, + + "search_basic_param" : { + "batch_size" : 1, + "k" : 10, + "run_count" : 3 + }, + + "index" : [ + { + "name" : "hnswlib.M4", + "algo" : "hnswlib", + "build_param": {"M":4, "efConstruction":500, "numThreads":4}, + "file" : "index/glove-100-inner/hnswlib/M4", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/glove-100-inner/hnswlib/M4" + }, + + { + "name" : "hnswlib.M8", + "algo" : "hnswlib", + "build_param": {"M":8, "efConstruction":500, "numThreads":4}, + "file" : "index/glove-100-inner/hnswlib/M8", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/glove-100-inner/hnswlib/M8" + }, + + { + "name" : "hnswlib.M12", + "algo" : "hnswlib", + "build_param": {"M":12, "efConstruction":500, "numThreads":4}, + "file" : "index/glove-100-inner/hnswlib/M12", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/glove-100-inner/hnswlib/M12" + }, + + { + "name" : "hnswlib.M16", + "algo" : "hnswlib", + "build_param": {"M":16, "efConstruction":500, "numThreads":4}, + "file" : "index/glove-100-inner/hnswlib/M16", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/glove-100-inner/hnswlib/M16" + }, + + { + "name" : "hnswlib.M24", + "algo" : "hnswlib", + "build_param": {"M":24, "efConstruction":500, "numThreads":4}, + "file" : "index/glove-100-inner/hnswlib/M24", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/glove-100-inner/hnswlib/M24" + }, + + { + "name" : "hnswlib.M36", + "algo" : "hnswlib", + "build_param": {"M":36, "efConstruction":500, "numThreads":4}, + "file" : "index/glove-100-inner/hnswlib/M36", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/glove-100-inner/hnswlib/M36" + }, + + { + "name" : "hnswlib.M48", + "algo" : "hnswlib", + "build_param": {"M":48, "efConstruction":500, "numThreads":4}, + "file" : "index/glove-100-inner/hnswlib/M48", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/glove-100-inner/hnswlib/M48" + }, + + { + "name" : "hnswlib.M64", + "algo" : "hnswlib", + "build_param": {"M":64, "efConstruction":500, "numThreads":4}, + "file" : "index/glove-100-inner/hnswlib/M64", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/glove-100-inner/hnswlib/M64" + }, + + { + "name" : "hnswlib.M96", + "algo" : "hnswlib", + "build_param": {"M":96, "efConstruction":500, "numThreads":4}, + "file" : "index/glove-100-inner/hnswlib/M96", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/glove-100-inner/hnswlib/M96" + }, + + { + "name" : "faiss_ivf_flat.nlist1024", + "algo" : "faiss_gpu_ivf_flat", + "build_param": {"nlist":1024}, + "file" : "index/glove-100-inner/faiss_ivf_flat/nlist1024", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_flat/nlist1024" + }, + + { + "name" : "faiss_ivf_flat.nlist2048", + "algo" : "faiss_gpu_ivf_flat", + "build_param": {"nlist":2048}, + "file" : "index/glove-100-inner/faiss_ivf_flat/nlist2048", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_flat/nlist2048" + }, + + { + "name" : "faiss_ivf_flat.nlist4096", + "algo" : "faiss_gpu_ivf_flat", + "build_param": {"nlist":4096}, + "file" : "index/glove-100-inner/faiss_ivf_flat/nlist4096", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_flat/nlist4096" + }, + + { + "name" : "faiss_ivf_flat.nlist8192", + "algo" : "faiss_gpu_ivf_flat", + "build_param": {"nlist":8192}, + "file" : "index/glove-100-inner/faiss_ivf_flat/nlist8192", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_flat/nlist8192" + }, + + { + "name" : "faiss_ivf_flat.nlist16384", + "algo" : "faiss_gpu_ivf_flat", + "build_param": {"nlist":16384}, + "file" : "index/glove-100-inner/faiss_ivf_flat/nlist16384", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000}, + {"nprobe":2000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_flat/nlist16384" + }, + + + + { + "name" : "faiss_ivf_pq.M2-nlist1024", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":1024, "M":2}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M2-nlist1024", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M2-nlist1024" + }, + + { + "name" : "faiss_ivf_pq.M2-nlist2048", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":2048, "M":2}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M2-nlist2048", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M2-nlist2048" + }, + + { + "name" : "faiss_ivf_pq.M2-nlist4096", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":4096, "M":2}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M2-nlist4096", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M2-nlist4096" + }, + + { + "name" : "faiss_ivf_pq.M2-nlist8192", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":8192, "M":2}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M2-nlist8192", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M2-nlist8192" + }, + + { + "name" : "faiss_ivf_pq.M2-nlist16384", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":16384, "M":2}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M2-nlist16384", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000}, + {"nprobe":2000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M2-nlist16384" + }, + + { + "name" : "faiss_ivf_pq.M4-nlist1024", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":1024, "M":4}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M4-nlist1024", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M4-nlist1024" + }, + + { + "name" : "faiss_ivf_pq.M4-nlist2048", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":2048, "M":4}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M4-nlist2048", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M4-nlist2048" + }, + + { + "name" : "faiss_ivf_pq.M4-nlist4096", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":4096, "M":4}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M4-nlist4096", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M4-nlist4096" + }, + + { + "name" : "faiss_ivf_pq.M4-nlist8192", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":8192, "M":4}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M4-nlist8192", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M4-nlist8192" + }, + + { + "name" : "faiss_ivf_pq.M4-nlist16384", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":16384, "M":4}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M4-nlist16384", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000}, + {"nprobe":2000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M4-nlist16384" + }, + + { + "name" : "faiss_ivf_pq.M20-nlist1024", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":1024, "M":20}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M20-nlist1024", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M20-nlist1024" + }, + + { + "name" : "faiss_ivf_pq.M20-nlist2048", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":2048, "M":20}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M20-nlist2048", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M20-nlist2048" + }, + + { + "name" : "faiss_ivf_pq.M20-nlist4096", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":4096, "M":20}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M20-nlist4096", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M20-nlist4096" + }, + + { + "name" : "faiss_ivf_pq.M20-nlist8192", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":8192, "M":20}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M20-nlist8192", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M20-nlist8192" + }, + + { + "name" : "faiss_ivf_pq.M20-nlist16384", + "algo" : "faiss_gpu_ivf_pq", + "build_param": {"nlist":16384, "M":20}, + "file" : "index/glove-100-inner/faiss_ivf_pq/M20-nlist16384", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000}, + {"nprobe":2000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_pq/M20-nlist16384" + }, + + + { + "name" : "faiss_ivf_sq.nlist1024-fp16", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":1024, "quantizer_type":"fp16"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist1024-fp16", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist1024-fp16" + }, + + { + "name" : "faiss_ivf_sq.nlist2048-fp16", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":2048, "quantizer_type":"fp16"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist2048-fp16", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist2048-fp16" + }, + + { + "name" : "faiss_ivf_sq.nlist4096-fp16", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":4096, "quantizer_type":"fp16"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist4096-fp16", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist4096-fp16" + }, + + { + "name" : "faiss_ivf_sq.nlist8192-fp16", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":8192, "quantizer_type":"fp16"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist8192-fp16", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist8192-fp16" + }, + + { + "name" : "faiss_ivf_sq.nlist16384-fp16", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":16384, "quantizer_type":"fp16"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist16384-fp16", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000}, + {"nprobe":2000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist16384-fp16" + }, + + + { + "name" : "faiss_ivf_sq.nlist1024-int8", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":1024, "quantizer_type":"int8"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist1024-int8", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist1024-int8" + }, + + { + "name" : "faiss_ivf_sq.nlist2048-int8", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":2048, "quantizer_type":"int8"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist2048-int8", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist2048-int8" + }, + + { + "name" : "faiss_ivf_sq.nlist4096-int8", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":4096, "quantizer_type":"int8"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist4096-int8", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist4096-int8" + }, + + { + "name" : "faiss_ivf_sq.nlist8192-int8", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":8192, "quantizer_type":"int8"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist8192-int8", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist8192-int8" + }, + + { + "name" : "faiss_ivf_sq.nlist16384-int8", + "algo" : "faiss_gpu_ivf_sq", + "build_param": {"nlist":16384, "quantizer_type":"int8"}, + "file" : "index/glove-100-inner/faiss_ivf_sq/nlist16384-int8", + "search_params" : [ + {"nprobe":1}, + {"nprobe":5}, + {"nprobe":10}, + {"nprobe":50}, + {"nprobe":100}, + {"nprobe":200}, + {"nprobe":500}, + {"nprobe":1000}, + {"nprobe":2000} + ], + "search_result_file" : "result/glove-100-inner/faiss_ivf_sq/nlist16384-int8" + }, + + { + "name" : "faiss_flat", + "algo" : "faiss_gpu_flat", + "build_param": {}, + "file" : "index/glove-100-inner/faiss_flat/flat", + "search_params" : [{}], + "search_result_file" : "result/glove-100-inner/faiss_flat/flat" + }, + + { + "name" : "ggnn.kbuild96-segment64-refine2-k10", + "algo" : "ggnn", + "build_param": { + "k_build": 96, + "segment_size": 64, + "refine_iterations": 2, + "dataset_size": 1183514, + "k": 10 + }, + "file" : "index/glove-100-inner/ggnn/kbuild96-segment64-refine2-k10", + "search_params" : [ + {"tau":0.001, "block_dim":64, "sorted_size":32}, + {"tau":0.005, "block_dim":64, "sorted_size":32}, + {"tau":0.01, "block_dim":64, "sorted_size":32}, + {"tau":0.02, "block_dim":64, "sorted_size":32}, + {"tau":0.03, "block_dim":64, "sorted_size":32}, + {"tau":0.04, "block_dim":64, "sorted_size":32}, + {"tau":0.05, "block_dim":64, "sorted_size":32}, + {"tau":0.06, "block_dim":64, "sorted_size":32}, + {"tau":0.09, "block_dim":64, "sorted_size":32}, + {"tau":0.12, "block_dim":64, "sorted_size":32}, + {"tau":0.18, "block_dim":64, "sorted_size":32}, + {"tau":0.21, "block_dim":64, "sorted_size":32}, + {"tau":0.24, "block_dim":64, "sorted_size":32}, + {"tau":0.27, "block_dim":64, "sorted_size":32}, + {"tau":0.3, "block_dim":64, "sorted_size":32}, + {"tau":0.4, "block_dim":64, "sorted_size":32}, + {"tau":0.01, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.02, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.03, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.04, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.05, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.06, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.09, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.12, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.18, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.21, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.24, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.27, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.3, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.4, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32}, + {"tau":0.5, "block_dim":128, "max_iterations":2000, "cache_size":1024, "sorted_size":32} + + ], + "search_result_file" : "result/glove-100-inner/ggnn/kbuild96-segment64-refine2-k10" + }, + + + ] + +} diff --git a/cpp/bench/ann/conf/sift-128-euclidean.json b/cpp/bench/ann/conf/sift-128-euclidean.json new file mode 100644 index 0000000000..476c363ecd --- /dev/null +++ b/cpp/bench/ann/conf/sift-128-euclidean.json @@ -0,0 +1,1321 @@ +{ + "dataset": { + "name": "sift-128-euclidean", + "base_file": "/home/cjnolet/workspace/ann_data/sift-128-euclidean/base.fbin", + "query_file": "/home/cjnolet/workspace/ann_data/sift-128-euclidean/query.fbin", + "distance": "euclidean" + }, + "search_basic_param": { + "batch_size": 5000, + "k": 10, + "run_count": 3 + }, + "index": [ + { + "name" : "hnswlib.M12", + "algo" : "hnswlib", + "build_param": {"M":12, "efConstruction":500, "numThreads":32}, + "file" : "index/sift-128-euclidean/hnswlib/M12", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/sift-128-euclidean/hnswlib/M12" + }, + { + "name" : "hnswlib.M16", + "algo" : "hnswlib", + "build_param": {"M":16, "efConstruction":500, "numThreads":32}, + "file" : "index/sift-128-euclidean/hnswlib/M16", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/sift-128-euclidean/hnswlib/M16" + }, + { + "name" : "hnswlib.M24", + "algo" : "hnswlib", + "build_param": {"M":24, "efConstruction":500, "numThreads":32}, + "file" : "index/sift-128-euclidean/hnswlib/M24", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/sift-128-euclidean/hnswlib/M24" + }, + { + "name" : "hnswlib.M36", + "algo" : "hnswlib", + "build_param": {"M":36, "efConstruction":500, "numThreads":32}, + "file" : "index/sift-128-euclidean/hnswlib/M36", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + {"ef":60, "numThreads":1}, + {"ef":80, "numThreads":1}, + {"ef":120, "numThreads":1}, + {"ef":200, "numThreads":1}, + {"ef":400, "numThreads":1}, + {"ef":600, "numThreads":1}, + {"ef":800, "numThreads":1} + ], + "search_result_file" : "result/sift-128-euclidean/hnswlib/M36" + }, + + + + + { + "name": "raft_bfknn", + "algo": "raft_bfknn", + "build_param": {}, + "file": "index/sift-128-euclidean/raft_bfknn/bfknn", + "search_params": [ + { + "probe": 1 + } + ], + "search_result_file": "result/sift-128-euclidean/raft_bfknn/bfknn" + }, + { + "name": "faiss_ivf_flat.nlist1024", + "algo": "faiss_gpu_ivf_flat", + "build_param": { + "nlist": 1024 + }, + "file": "index/sift-128-euclidean/faiss_ivf_flat/nlist1024", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_flat/nlist1024" + }, + { + "name": "faiss_ivf_flat.nlist2048", + "algo": "faiss_gpu_ivf_flat", + "build_param": { + "nlist": 2048 + }, + "file": "index/sift-128-euclidean/faiss_ivf_flat/nlist2048", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_flat/nlist2048" + }, + { + "name": "faiss_ivf_flat.nlist4096", + "algo": "faiss_gpu_ivf_flat", + "build_param": { + "nlist": 4096 + }, + "file": "index/sift-128-euclidean/faiss_ivf_flat/nlist4096", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_flat/nlist4096" + }, + { + "name": "faiss_ivf_flat.nlist8192", + "algo": "faiss_gpu_ivf_flat", + "build_param": { + "nlist": 8192 + }, + "file": "index/sift-128-euclidean/faiss_ivf_flat/nlist8192", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_flat/nlist8192" + }, + { + "name": "faiss_ivf_flat.nlist16384", + "algo": "faiss_gpu_ivf_flat", + "build_param": { + "nlist": 16384 + }, + "file": "index/sift-128-euclidean/faiss_ivf_flat/nlist16384", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + }, + { + "nprobe": 2000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_flat/nlist16384" + }, + { + "name": "faiss_ivf_pq.M64-nlist1024", + "algo": "faiss_gpu_ivf_pq", + "build_param": { + "nlist": 1024, + "M": 64, + "useFloat16": true, + "usePrecomputed": true + }, + "file": "index/sift-128-euclidean/faiss_ivf_pq/M64-nlist1024", + "search_params": [ + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_pq/M64-nlist1024" + }, + { + "name": "faiss_ivf_pq.M64-nlist1024.noprecomp", + "algo": "faiss_gpu_ivf_pq", + "build_param": { + "nlist": 1024, + "M": 64, + "useFloat16": true, + "usePrecomputed": false + }, + "file": "index/sift-128-euclidean/faiss_ivf_pq/M64-nlist1024.noprecomp", + "search_params": [ + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_pq/M64-nlist1024" + }, + { + "name": "faiss_ivf_sq.nlist1024-fp16", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 1024, + "quantizer_type": "fp16" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist1024-fp16", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist1024-fp16" + }, + { + "name": "faiss_ivf_sq.nlist2048-fp16", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 2048, + "quantizer_type": "fp16" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist2048-fp16", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist2048-fp16" + }, + { + "name": "faiss_ivf_sq.nlist4096-fp16", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 4096, + "quantizer_type": "fp16" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist4096-fp16", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist4096-fp16" + }, + { + "name": "faiss_ivf_sq.nlist8192-fp16", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 8192, + "quantizer_type": "fp16" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist8192-fp16", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist8192-fp16" + }, + { + "name": "faiss_ivf_sq.nlist16384-fp16", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 16384, + "quantizer_type": "fp16" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist16384-fp16", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + }, + { + "nprobe": 2000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist16384-fp16" + }, + { + "name": "faiss_ivf_sq.nlist1024-int8", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 1024, + "quantizer_type": "int8" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist1024-int8", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist1024-int8" + }, + { + "name": "faiss_ivf_sq.nlist2048-int8", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 2048, + "quantizer_type": "int8" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist2048-int8", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist2048-int8" + }, + { + "name": "faiss_ivf_sq.nlist4096-int8", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 4096, + "quantizer_type": "int8" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist4096-int8", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist4096-int8" + }, + { + "name": "faiss_ivf_sq.nlist8192-int8", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 8192, + "quantizer_type": "int8" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist8192-int8", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist8192-int8" + }, + { + "name": "faiss_ivf_sq.nlist16384-int8", + "algo": "faiss_gpu_ivf_sq", + "build_param": { + "nlist": 16384, + "quantizer_type": "int8" + }, + "file": "index/sift-128-euclidean/faiss_ivf_sq/nlist16384-int8", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + }, + { + "nprobe": 2000 + } + ], + "search_result_file": "result/sift-128-euclidean/faiss_ivf_sq/nlist16384-int8" + }, + { + "name": "faiss_flat", + "algo": "faiss_gpu_flat", + "build_param": {}, + "file": "index/sift-128-euclidean/faiss_flat/flat", + "search_params": [ + {} + ], + "search_result_file": "result/sift-128-euclidean/faiss_flat/flat" + }, + + { + "name": "raft_ivf_pq.dimpq128-cluster1024", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 128, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024", + "search_params": [ + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "half", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "half", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "half", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "half", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "half", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "half", + "smemLutDtype": "half" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024" + }, + { + "name": "raft_ivf_pq.dimpq128-cluster1024-float-float", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 128, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024-float-float", + "search_params": [ + { + "k": 10, + "numProbes": 1, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 1, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 5, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024-float-float" + }, + { + "name": "raft_ivf_pq.dimpq128-cluster1024-float-half", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 128, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024-float-half", + "search_params": [ + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024-float-half" + }, + { + "name": "raft_ivf_pq.dimpq128-cluster1024-float-fp8", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 128, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024-float-fp8", + "search_params": [ + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024-float-fp8" + }, + { + "name": "raft_ivf_pq.dimpq64-cluster1024-float-fp8", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 64, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq64-cluster1024-float-fp8", + "search_params": [ + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq64-cluster1024-float-fp8" + }, + { + "name": "raft_ivf_pq.dimpq64-cluster1024-float-half", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 64, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq64-cluster1024-float-half", + "search_params": [ + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "float", + "smemLutDtype": "half" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq64-cluster1024-float-half" + }, + { + "name": "raft_ivf_pq.dimpq32-cluster1024-float-fp8", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 32, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq32-cluster1024-float-fp8", + "search_params": [ + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq32-cluster1024-float-fp8" + }, + { + "name": "raft_ivf_pq.dimpq16-cluster1024-float-fp8", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 16, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq16-cluster1024-float-fp8", + "search_params": [ + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "float", + "smemLutDtype": "fp8" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq16-cluster1024-float-fp8" + }, + { + "name": "raft_ivf_pq.dimpq128-cluster1024-half-float", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 128, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024-half-float", + "search_params": [ + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "half", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "half", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "half", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "half", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "half", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "half", + "smemLutDtype": "float" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq128-cluster1024-half-float" + }, + { + "name": "raft_ivf_pq.dimpq512-cluster1024-float-float", + "algo": "raft_ivf_pq", + "build_param": { + "nlist": 1024, + "pq_dim": 512, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_pq/dimpq512-cluster1024-float-float", + "search_params": [ + { + "k": 10, + "numProbes": 10, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 50, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 100, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 200, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 500, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + }, + { + "k": 10, + "numProbes": 1024, + "internalDistanceDtype": "float", + "smemLutDtype": "float" + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_pq/dimpq512-cluster1024-float-float" + }, + { + "name": "raft_ivf_flat.nlist1024", + "algo": "raft_ivf_flat", + "build_param": { + "nlist": 1024, + "ratio": 1, + "niter": 25 + }, + "file": "index/sift-128-euclidean/raft_ivf_flat/nlist1024", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_flat/nlist1024" + }, + { + "name": "raft_ivf_flat.nlist16384", + "algo": "raft_ivf_flat", + "build_param": { + "nlist": 16384, + "ratio": 2, + "niter": 20 + }, + "file": "index/sift-128-euclidean/raft_ivf_flat/nlist16384", + "search_params": [ + { + "nprobe": 1 + }, + { + "nprobe": 5 + }, + { + "nprobe": 10 + }, + { + "nprobe": 50 + }, + { + "nprobe": 100 + }, + { + "nprobe": 200 + }, + { + "nprobe": 500 + }, + { + "nprobe": 1000 + }, + { + "nprobe": 2000 + } + ], + "search_result_file": "result/sift-128-euclidean/raft_ivf_flat/nlist16384" + } + ] +} diff --git a/cpp/bench/ann/scripts/eval.pl b/cpp/bench/ann/scripts/eval.pl new file mode 100755 index 0000000000..81c5563d79 --- /dev/null +++ b/cpp/bench/ann/scripts/eval.pl @@ -0,0 +1,430 @@ +#!/usr/bin/perl + +# ============================================================================= +# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# +# 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. + +use warnings; +use strict; +use autodie qw(open close); +use File::Find; +use Getopt::Std; + +my $QPS = 'QPS'; +my $AVG_LATENCY = 'avg_latency(ms)'; +my $P99_LATENCY = 'p99_latency(ms)'; +my $P999_LATENCY = 'p999_latency(ms)'; +my @CONDITIONS = ([$QPS, 2000], ['recall', 0.9], ['recall', 0.95]); + + +my $USAGE = << 'END'; +usage: [-f] [-l avg|p99|p999] [-o output.csv] groundtruth.neighbors.ibin result_paths... + result_paths... are paths to the search result files. + Can specify multiple paths. + For each of them, if it's a directory, all the .txt files found under + it recursively will be regarded as inputs. + + -f: force to recompute recall and update it in result file if needed + -l: output search latency rather than QPS. Available options: + "avg" for average latency; + "p99" for 99th percentile latency; + "p999" for 99.9th percentile latency. + -o: also write result to a csv file +END + + +my %opt; +getopts('fl:o:', \%opt) + or die $USAGE; +my $force_calc_recall = exists $opt{f} ? 1 : 0; +my $csv_file; +$csv_file = $opt{o} if exists $opt{o}; +my $metric = $QPS; +if (exists $opt{l}) { + my $option = $opt{l}; + if ($option eq 'avg') { + $metric = $AVG_LATENCY; + } + elsif ($option eq 'p99') { + $metric = $P99_LATENCY; + } + elsif ($option eq 'p999') { + $metric = $P999_LATENCY; + } + else { + die + "[error] illegal value for '-l': '$option'. Must be 'avg', 'p99' or 'p999'\n"; + } +} + +@ARGV >= 2 + or die $USAGE; + + +my $truth_file = shift @ARGV; +my ($k, $dataset, $distance, $results) = get_all_results($metric, @ARGV); +if (!defined $k) { + print STDERR "no result file found\n"; + exit -1; +} +print STDERR "dataset = $dataset, distance = $distance, k = $k\n\n"; +calc_missing_recall($results, $truth_file, $force_calc_recall); + +my @results = sort { + $a->{name} cmp $b->{name} + or $a->{recall} <=> $b->{recall} + or $b->{qps} <=> $a->{qps} +} @$results; +printf("%-60s %6s %16s %s\n", '', 'Recall', $metric, 'search_param'); +for my $result (@results) { + my $fmt = ($metric eq $QPS) ? '%16.1f' : '%16.3f'; + my $qps = $result->{qps}; + $qps *= 1000 if $metric ne $QPS; # the unit of latency is ms + printf("%-60s %6.4f ${fmt} %s\n", + $result->{name}, $result->{recall}, $qps, $result->{search_param}); +} +if (defined $csv_file) { + open my $fh, '>', $csv_file; + print {$fh} ",Recall,${metric},search_param\n"; + for my $result (@results) { + my $qps = $result->{qps}; + $qps *= 1000 if $metric ne $QPS; + printf {$fh} ( + "%s,%.4f,%.3f,%s\n", $result->{name}, $result->{recall}, + $qps, $result->{search_param} + ); + } +} +print "\n"; +calc_and_print_estimation($results, $metric, \@CONDITIONS); + + + + +sub read_result { + my ($fname) = @_; + open my $fh, '<', $fname; + my %attr; + while (<$fh>) { + chomp; + next if /^\s*$/; + my $pos = index($_, ':'); + $pos != -1 + or die "[error] no ':' is found: '$_'\n"; + my $key = substr($_, 0, $pos); + my $val = substr($_, $pos + 1); + $key =~ s/^\s+|\s+$//g; + $val =~ s/^\s+|\s+$//g; + + # old version benchmark compatible + if ($key eq 'search_time') { + $key = 'average_search_time'; + $val *= $attr{batch_size}; + } + $attr{$key} = $val; + } + return \%attr; +} + +sub overwrite_recall_to_result { + my ($fname, $recall) = @_; + open my $fh_in, '<', $fname; + $recall = sprintf("%f", $recall); + my $out; + while (<$fh_in>) { + s/^recall: .*/recall: $recall/; + $out .= $_; + } + close $fh_in; + + open my $fh_out, '>', $fname; + print {$fh_out} $out; +} + +sub append_recall_to_result { + my ($fname, $recall) = @_; + open my $fh, '>>', $fname; + printf {$fh} ("recall: %f\n", $recall); +} + +sub get_all_results { + my ($metric) = shift @_; + + my %fname; + my $wanted = sub { + if (-f && /\.txt$/) { + $fname{$File::Find::name} = 1; + } + }; + find($wanted, @_); + + my $k; + my $dataset; + my $distance; + my @results; + for my $f (sort keys %fname) { + print STDERR "reading $f ...\n"; + my $attr = read_result($f); + if (!defined $k) { + $k = $attr->{k}; + $dataset = $attr->{dataset}; + $distance = $attr->{distance}; + } + else { + $attr->{k} eq $k + or die "[error] k should be $k, but is $attr->{k} in $f\n"; + $attr->{dataset} eq $dataset + or die + "[error] dataset should be $dataset, but is $attr->{dataset} in $f\n"; + $attr->{distance} eq $distance + or die + "[error] distance should be $distance, but is $attr->{distance} in $f\n"; + } + + my $batch_size = $attr->{batch_size}; + $batch_size =~ s/000000$/M/; + $batch_size =~ s/000$/K/; + my $search_param = $attr->{search_param}; + $search_param =~ s/^{//; + $search_param =~ s/}$//; + $search_param =~ s/,/ /g; + $search_param =~ s/"//g; + + my $qps; + if ($metric eq $QPS) { + $qps = $attr->{batch_size} / $attr->{average_search_time}; + } + elsif ($metric eq $AVG_LATENCY) { + $qps = $attr->{average_search_time}; + } + elsif ($metric eq $P99_LATENCY) { + exists $attr->{p99_search_time} + or die "[error] p99_search_time is not found\n"; + $qps = $attr->{p99_search_time}; + } + elsif ($metric eq $P999_LATENCY) { + exists $attr->{p999_search_time} + or die "[error] p999_search_time is not found\n"; + $qps = $attr->{p999_search_time}; + } + else { + die "[error] unknown latency type: '$metric'\n"; + } + my $result = { + file => $f, + name => "$attr->{name}-batch${batch_size}", + search_param => $search_param, + qps => $qps, + }; + + if (exists $attr->{recall}) { + $result->{recall} = $attr->{recall}; + } + push @results, $result; + } + return $k, $dataset, $distance, \@results; +} + +sub read_ibin { + my ($fname) = @_; + + open my $fh, '<:raw', $fname; + my $raw; + + read($fh, $raw, 8); + my ($nrows, $dim) = unpack('LL', $raw); + + my $expected_size = 8 + $nrows * $dim * 4; + my $size = (stat($fh))[7]; + $size == $expected_size + or die( + "[error] expected size is $expected_size, but actual size is $size\n"); + + read($fh, $raw, $nrows * $dim * 4) == $nrows * $dim * 4 + or die "[error] read $fname failed\n"; + my @data = unpack('l' x ($nrows * $dim), $raw); + return \@data, $nrows, $dim; +} + +sub pick_k_neighbors { + my ($neighbors, $nrows, $ncols, $k) = @_; + + my @res; + for my $i (0 .. $nrows - 1) { + my %neighbor_set; + for my $j (0 .. $k - 1) { + $neighbor_set{$neighbors->[$i * $ncols + $j]} = 1; + } + push @res, \%neighbor_set; + } + return \@res; +} + + +sub calc_recall { + my ($truth_k_neighbors, $result_neighbors, $nrows, $k) = @_; + + my $recall = 0; + for my $i (0 .. $nrows - 1) { + my $tp = 0; + for my $j (0 .. $k - 1) { + my $neighbor = $result_neighbors->[$i * $k + $j]; + ++$tp if exists $truth_k_neighbors->[$i]{$neighbor}; + } + $recall += $tp; + } + return $recall / $k / $nrows; +} + +sub calc_missing_recall { + my ($results, $truth_file, $force_calc_recall) = @_; + + my $need_calc_recall = grep { !exists $_->{recall} } @$results; + return unless $need_calc_recall || $force_calc_recall; + + my ($truth_neighbors, $nrows, $truth_k) = read_ibin($truth_file); + $truth_k >= $k + or die "[error] ground truth k ($truth_k) < k($k)\n"; + my $truth_k_neighbors = + pick_k_neighbors($truth_neighbors, $nrows, $truth_k, $k); + + for my $result (@$results) { + next if exists $result->{recall} && !$force_calc_recall; + + my $result_bin_file = $result->{file}; + $result_bin_file =~ s/txt$/ibin/; + print STDERR "calculating recall for $result_bin_file ...\n"; + my ($result_neighbors, $result_nrows, $result_k) = + read_ibin($result_bin_file); + $result_k == $k + or die + "[error] k should be $k, but is $result_k in $result_bin_file\n"; + $result_nrows == $nrows + or die + "[error] #row should be $nrows, but is $result_nrows in $result_bin_file\n"; + + my $recall = + calc_recall($truth_k_neighbors, $result_neighbors, $nrows, $k); + if (exists $result->{recall}) { + my $new_value = sprintf("%f", $recall); + if ($result->{recall} ne $new_value) { + print "update recall: $result->{recall} -> $new_value\n"; + overwrite_recall_to_result($result->{file}, $recall); + } + } + else { + append_recall_to_result($result->{file}, $recall); + } + $result->{recall} = $recall; + } +} + + +sub estimate { + my ($results, $condition, $value) = @_; + my %point_of; + for my $result (@$results) { + my $point; + if ($condition eq 'recall') { + $point = [$result->{recall}, $result->{qps}]; + } + else { + $point = [$result->{qps}, $result->{recall}]; + } + push @{$point_of{$result->{name}}}, $point; + } + + my @names = sort keys %point_of; + my @result; + for my $name (@names) { + my @points = sort { $a->[0] <=> $b->[0] } @{$point_of{$name}}; + if ($value < $points[0][0] || $value > $points[$#points][0]) { + push @result, -1; + next; + } + elsif ($value == $points[0][0]) { + push @result, $points[0][1]; + next; + } + + for my $i (1 .. $#points) { + if ($points[$i][0] >= $value) { + push @result, + linear_interpolation($value, @{$points[$i - 1]}, + @{$points[$i]}); + last; + } + } + } + return \@names, \@result; +} + +sub linear_interpolation { + my ($x, $x1, $y1, $x2, $y2) = @_; + return $y1 + ($x - $x1) * ($y2 - $y1) / ($x2 - $x1); +} + +sub merge { + my ($all, $new, $scale) = @_; + @$all == @$new + or die "[error] length is not equal\n"; + for my $i (0 .. @$all - 1) { + push @{$all->[$i]}, $new->[$i] * $scale; + } +} + +sub calc_and_print_estimation { + my ($results, $metric, $conditions) = @_; + + my @conditions = grep { + my $target = $_->[0]; + if ($target eq 'recall' || $target eq $metric) { + 1; + } + else { + $target eq $QPS + || $target eq $AVG_LATENCY + || $target eq $P99_LATENCY + || $target eq $P999_LATENCY + or die "[error] unknown condition: '$target'\n"; + 0; + } + } @$conditions; + + my @headers = map { + my $header; + if ($_->[0] eq 'recall') { + $header = $metric . '@recall' . $_->[1]; + } + elsif ($_->[0] eq $metric) { + $header = 'recall@' . $metric . $_->[1]; + } + $header; + } @conditions; + + my $scale = ($metric eq $QPS) ? 1 : 1000; + my $estimations; + for my $condition (@conditions) { + my ($names, $estimate) = estimate($results, @$condition); + if (!defined $estimations) { + @$estimations = map { [$_] } @$names; + } + merge($estimations, $estimate, $scale); + } + + my $fmt = "%-60s" . (" %16s" x @headers) . "\n"; + printf($fmt, '', @headers); + $fmt =~ s/16s/16.4f/g; + for (@$estimations) { + printf($fmt, @$_); + } +} diff --git a/cpp/bench/ann/scripts/fbin_to_f16bin.py b/cpp/bench/ann/scripts/fbin_to_f16bin.py new file mode 100755 index 0000000000..4ea8988d87 --- /dev/null +++ b/cpp/bench/ann/scripts/fbin_to_f16bin.py @@ -0,0 +1,46 @@ +# ============================================================================= +# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# +# 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. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +import sys +import numpy as np + + +def read_fbin(fname): + shape = np.fromfile(fname, dtype=np.uint32, count=2) + if float(shape[0]) * shape[1] * 4 > 2000000000: + data = np.memmap(fname, dtype=np.float32, offset=8, mode="r").reshape( + shape + ) + else: + data = np.fromfile(fname, dtype=np.float32, offset=8).reshape(shape) + return data + + +def write_bin(fname, data): + with open(fname, "wb") as f: + np.asarray(data.shape, dtype=np.uint32).tofile(f) + data.tofile(f) + + +if len(sys.argv) != 3: + print( + "usage: %s input.fbin output.f16bin" % (sys.argv[0]), + file=sys.stderr, + ) + sys.exit(-1) + +data = read_fbin(sys.argv[1]).astype(np.float16) +write_bin(sys.argv[2], data) diff --git a/cpp/bench/ann/scripts/hdf5_to_fbin.py b/cpp/bench/ann/scripts/hdf5_to_fbin.py new file mode 100755 index 0000000000..cfeb184ea8 --- /dev/null +++ b/cpp/bench/ann/scripts/hdf5_to_fbin.py @@ -0,0 +1,85 @@ +# ============================================================================= +# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# +# 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. + +import sys +import numpy as np +import h5py + + +def normalize(x): + norm = np.linalg.norm(x, axis=1) + return (x.T / norm).T + + +def write_bin(fname, data): + with open(fname, "wb") as f: + np.asarray(data.shape, dtype=np.uint32).tofile(f) + data.tofile(f) + + +if __name__ == "__main__": + if len(sys.argv) != 2 and len(sys.argv) != 3: + print( + "usage: %s [-n] .hdf5\n" % (sys.argv[0]), + " -n: normalize base/query set\n", + "outputs: .base.fbin\n", + " .query.fbin\n", + " .groundtruth.neighbors.ibin\n", + " .groundtruth.distances.fbin", + file=sys.stderr, + ) + sys.exit(-1) + + need_normalize = False + if len(sys.argv) == 3: + assert sys.argv[1] == "-n" + need_normalize = True + fname_prefix = sys.argv[-1] + assert fname_prefix.endswith(".hdf5") + fname_prefix = fname_prefix[:-5] + + hdf5 = h5py.File(sys.argv[-1], "r") + assert ( + hdf5.attrs["distance"] == "angular" + or hdf5.attrs["distance"] == "euclidean" + ) + assert hdf5["train"].dtype == np.float32 + assert hdf5["test"].dtype == np.float32 + assert hdf5["neighbors"].dtype == np.int32 + assert hdf5["distances"].dtype == np.float32 + + base = hdf5["train"][:] + query = hdf5["test"][:] + if need_normalize: + base = normalize(base) + query = normalize(query) + elif hdf5.attrs["distance"] == "angular": + print( + "warning: input has angular distance, specify -n to normalize base/query set!\n" + ) + + output_fname = fname_prefix + ".base.fbin" + print("writing", output_fname, "...") + write_bin(output_fname, base) + + output_fname = fname_prefix + ".query.fbin" + print("writing", output_fname, "...") + write_bin(output_fname, query) + + output_fname = fname_prefix + ".groundtruth.neighbors.ibin" + print("writing", output_fname, "...") + write_bin(output_fname, hdf5["neighbors"][:]) + + output_fname = fname_prefix + ".groundtruth.distances.fbin" + print("writing", output_fname, "...") + write_bin(output_fname, hdf5["distances"][:]) diff --git a/cpp/bench/ann/scripts/split_groundtruth.pl b/cpp/bench/ann/scripts/split_groundtruth.pl new file mode 100755 index 0000000000..b0a59f806c --- /dev/null +++ b/cpp/bench/ann/scripts/split_groundtruth.pl @@ -0,0 +1,45 @@ +#!/usr/bin/perl + +# ============================================================================= +# Copyright (c) 2020-2023, NVIDIA CORPORATION. +# +# 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. + +use warnings; +use strict; +use autodie qw(open close); + + +@ARGV == 2 + or die "usage: $0 input output_prefix\n"; + +open my $fh, '<:raw', $ARGV[0]; + +my $raw; +read($fh, $raw, 8); +my ($nrows, $dim) = unpack('LL', $raw); + +my $expected_size = 8 + $nrows * $dim * (4 + 4); +my $size = (stat($fh))[7]; +$size == $expected_size + or die("error: expected size is $expected_size, but actual size is $size\n"); + + +open my $fh_out1, '>:raw', "$ARGV[1].neighbors.ibin"; +open my $fh_out2, '>:raw', "$ARGV[1].distances.fbin"; + +print {$fh_out1} $raw; +print {$fh_out2} $raw; + +read($fh, $raw, $nrows * $dim * 4); +print {$fh_out1} $raw; +read($fh, $raw, $nrows * $dim * 4); +print {$fh_out2} $raw; diff --git a/cpp/bench/ann/src/common/ann_types.hpp b/cpp/bench/ann/src/common/ann_types.hpp new file mode 100644 index 0000000000..8f73896e07 --- /dev/null +++ b/cpp/bench/ann/src/common/ann_types.hpp @@ -0,0 +1,88 @@ + + +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ + +#pragma once +#include +#include + +#include + +namespace raft::bench::ann { + +enum class Metric { + kInnerProduct, + kEuclidean, +}; + +enum class MemoryType { + Host, + HostMmap, + Device, +}; + +struct AlgoProperty { + MemoryType dataset_memory_type; + // neighbors/distances should have same memory type as queries + MemoryType query_memory_type; + bool need_dataset_when_search; +}; + +template +class ANN { + public: + struct AnnSearchParam { + virtual ~AnnSearchParam() = default; + }; + + ANN(Metric metric, int dim) : metric_(metric), dim_(dim) {} + virtual ~ANN() = default; + + virtual void build(const T* dataset, size_t nrow, cudaStream_t stream = 0) = 0; + + virtual void set_search_param(const AnnSearchParam& param) = 0; + // TODO: this assumes that an algorithm can always return k results. + // This is not always possible. + virtual void search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream = 0) const = 0; + + virtual void save(const std::string& file) const = 0; + virtual void load(const std::string& file) = 0; + + virtual AlgoProperty get_property() const = 0; + + // Some algorithms don't save the building dataset in their indices. + // So they should be given the access to that dataset during searching. + // The advantage of this way is that index has smaller size + // and many indices can share one dataset. + // + // AlgoProperty::need_dataset_when_search of such algorithm should be true, + // and set_search_dataset() should save the passed-in pointer somewhere. + // The client code should call set_search_dataset() before searching, + // and should not release dataset before searching is finished. + virtual void set_search_dataset(const T* /*dataset*/, size_t /*nrow*/){}; + + protected: + Metric metric_; + int dim_; +}; + +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/common/benchmark.hpp b/cpp/bench/ann/src/common/benchmark.hpp new file mode 100644 index 0000000000..b4d8fbeee3 --- /dev/null +++ b/cpp/bench/ann/src/common/benchmark.hpp @@ -0,0 +1,591 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#ifdef NVTX +#include +#endif +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "benchmark_util.hpp" +#include "conf.h" +#include "dataset.h" +#include "util.h" + +using std::cerr; +using std::cout; +using std::endl; +using std::string; +using std::to_string; +using std::unordered_set; +using std::vector; + +namespace raft::bench::ann { + +inline bool check_file_exist(const std::vector& files) +{ + bool ret = true; + std::unordered_set processed; + for (const auto& file : files) { + if (processed.find(file) == processed.end() && !file_exists(file)) { + log_error("file '%s' doesn't exist or is not a regular file", file.c_str()); + ret = false; + } + processed.insert(file); + } + return ret; +} + +inline bool check_file_not_exist(const std::vector& files, bool force_overwrite) +{ + bool ret = true; + for (const auto& file : files) { + if (file_exists(file)) { + if (force_overwrite) { + log_warn("'%s' already exists, will overwrite it", file.c_str()); + } else { + log_error("'%s' already exists, use '-f' to force overwriting", file.c_str()); + ret = false; + } + } + } + return ret; +} + +inline bool check_no_duplicate_file(const std::vector& files) +{ + bool ret = true; + std::unordered_set processed; + for (const auto& file : files) { + if (processed.find(file) != processed.end()) { + log_error("'%s' occurs more than once as output file, would be overwritten", file.c_str()); + ret = false; + } + processed.insert(file); + } + return ret; +} + +inline bool mkdir(const std::vector& dirs) +{ + std::unordered_set processed; + for (const auto& dir : dirs) { + if (processed.find(dir) == processed.end() && !dir_exists(dir)) { + if (create_dir(dir)) { + log_info("mkdir '%s'", dir.c_str()); + } else { + log_error("fail to create output directory '%s'", dir.c_str()); + // won't create any other dir when problem occurs + return false; + } + } + processed.insert(dir); + } + return true; +} + +inline bool check(const std::vector& indices, + bool build_mode, + bool force_overwrite) +{ + std::vector files_should_exist; + std::vector dirs_should_exist; + std::vector output_files; + for (const auto& index : indices) { + if (build_mode) { + output_files.push_back(index.file); + output_files.push_back(index.file + ".txt"); + + auto pos = index.file.rfind('/'); + if (pos != std::string::npos) { dirs_should_exist.push_back(index.file.substr(0, pos)); } + } else { + files_should_exist.push_back(index.file); + files_should_exist.push_back(index.file + ".txt"); + + output_files.push_back(index.search_result_file + ".0.ibin"); + output_files.push_back(index.search_result_file + ".0.txt"); + + auto pos = index.search_result_file.rfind('/'); + if (pos != std::string::npos) { + dirs_should_exist.push_back(index.search_result_file.substr(0, pos)); + } + } + } + + bool ret = true; + if (!check_file_exist(files_should_exist)) { ret = false; } + if (!check_file_not_exist(output_files, force_overwrite)) { ret = false; } + if (!check_no_duplicate_file(output_files)) { ret = false; } + if (ret && !mkdir(dirs_should_exist)) { ret = false; } + return ret; +} + +inline void write_build_info(const std::string& file_prefix, + const std::string& dataset, + const std::string& distance, + const std::string& name, + const std::string& algo, + const std::string& build_param, + float build_time) +{ + std::ofstream ofs(file_prefix + ".txt"); + if (!ofs) { throw std::runtime_error("can't open build info file: " + file_prefix + ".txt"); } + ofs << "dataset: " << dataset << "\n" + << "distance: " << distance << "\n" + << "\n" + << "name: " << name << "\n" + << "algo: " << algo << "\n" + << "build_param: " << build_param << "\n" + << "build_time: " << build_time << endl; + ofs.close(); + if (!ofs) { throw std::runtime_error("can't write to build info file: " + file_prefix + ".txt"); } +} + +template +void build(const Dataset* dataset, const std::vector& indices) +{ + cudaStream_t stream; + RAFT_CUDA_TRY(cudaStreamCreate(&stream)); + + log_info( + "base set from dataset '%s', #vector = %zu", dataset->name().c_str(), dataset->base_set_size()); + + for (const auto& index : indices) { + log_info("creating algo '%s', param=%s", index.algo.c_str(), index.build_param.dump().c_str()); + auto algo = create_algo(index.algo, + dataset->distance(), + dataset->dim(), + index.refine_ratio, + index.build_param, + index.dev_list); + auto algo_property = algo->get_property(); + + const T* base_set_ptr = nullptr; + if (algo_property.dataset_memory_type == MemoryType::Host) { + log_info("%s", "loading base set to memory"); + base_set_ptr = dataset->base_set(); + } else if (algo_property.dataset_memory_type == MemoryType::HostMmap) { + log_info("%s", "mapping base set to memory"); + base_set_ptr = dataset->mapped_base_set(); + } else if (algo_property.dataset_memory_type == MemoryType::Device) { + log_info("%s", "loading base set to GPU"); + base_set_ptr = dataset->base_set_on_gpu(); + } + + log_info("building index '%s'", index.name.c_str()); + RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); +#ifdef NVTX + nvtxRangePush("build"); +#endif + Timer timer; + algo->build(base_set_ptr, dataset->base_set_size(), stream); + RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); + float elapsed_ms = timer.elapsed_ms(); +#ifdef NVTX + nvtxRangePop(); +#endif + log_info("built index in %.2f seconds", elapsed_ms / 1000.0f); + RAFT_CUDA_TRY(cudaDeviceSynchronize()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + algo->save(index.file); + write_build_info(index.file, + dataset->name(), + dataset->distance(), + index.name, + index.algo, + index.build_param.dump(), + elapsed_ms / 1000.0f); + log_info("saved index to %s", index.file.c_str()); + } + + RAFT_CUDA_TRY(cudaStreamDestroy(stream)); +} + +inline void write_search_result(const std::string& file_prefix, + const std::string& dataset, + const std::string& distance, + const std::string& name, + const std::string& algo, + const std::string& build_param, + const std::string& search_param, + int batch_size, + int run_count, + int k, + float search_time_average, + float search_time_p99, + float search_time_p999, + const int* neighbors, + size_t query_set_size) +{ + std::ofstream ofs(file_prefix + ".txt"); + if (!ofs) { throw std::runtime_error("can't open search result file: " + file_prefix + ".txt"); } + ofs << "dataset: " << dataset << "\n" + << "distance: " << distance << "\n" + << "\n" + << "name: " << name << "\n" + << "algo: " << algo << "\n" + << "build_param: " << build_param << "\n" + << "search_param: " << search_param << "\n" + << "\n" + << "batch_size: " << batch_size << "\n" + << "run_count: " << run_count << "\n" + << "k: " << k << "\n" + << "average_search_time: " << search_time_average << endl; + if (search_time_p99 != std::numeric_limits::max()) { + ofs << "p99_search_time: " << search_time_p99 << endl; + } + if (search_time_p999 != std::numeric_limits::max()) { + ofs << "p999_search_time: " << search_time_p999 << endl; + } + ofs.close(); + if (!ofs) { + throw std::runtime_error("can't write to search result file: " + file_prefix + ".txt"); + } + + BinFile neighbors_file(file_prefix + ".ibin", "w"); + neighbors_file.write(neighbors, query_set_size, k); +} + +template +inline void search(const Dataset* dataset, const std::vector& indices) +{ + if (indices.empty()) { return; } + cudaStream_t stream; + RAFT_CUDA_TRY(cudaStreamCreate(&stream)); + + log_info("loading query set from dataset '%s', #vector = %zu", + dataset->name().c_str(), + dataset->query_set_size()); + const T* query_set = dataset->query_set(); + // query set is usually much smaller than base set, so load it eagerly + const T* d_query_set = dataset->query_set_on_gpu(); + size_t query_set_size = dataset->query_set_size(); + + // currently all indices has same batch_size, k and run_count + const int batch_size = indices[0].batch_size; + const int k = indices[0].k; + const int run_count = indices[0].run_count; + log_info( + "basic search parameters: batch_size = %d, k = %d, run_count = %d", batch_size, k, run_count); + if (query_set_size % batch_size != 0) { + log_warn("query set size (%zu) % batch size (%d) != 0, the size of last batch is %zu", + query_set_size, + batch_size, + query_set_size % batch_size); + } + const size_t num_batches = (query_set_size - 1) / batch_size + 1; + std::size_t* neighbors = new std::size_t[query_set_size * k]; + int* neighbors_buf = new int[query_set_size * k]; + float* distances = new float[query_set_size * k]; + std::vector search_times; + search_times.reserve(num_batches); + std::size_t* d_neighbors; + float* d_distances; + RAFT_CUDA_TRY(cudaMalloc((void**)&d_neighbors, query_set_size * k * sizeof(*d_neighbors))); + RAFT_CUDA_TRY(cudaMalloc((void**)&d_distances, query_set_size * k * sizeof(*d_distances))); + + for (const auto& index : indices) { + log_info("creating algo '%s', param=%s", index.algo.c_str(), index.build_param.dump().c_str()); + auto algo = create_algo(index.algo, + dataset->distance(), + dataset->dim(), + index.refine_ratio, + index.build_param, + index.dev_list); + auto algo_property = algo->get_property(); + + log_info("loading index '%s' from file '%s'", index.name.c_str(), index.file.c_str()); + algo->load(index.file); + + const T* this_query_set = query_set; + std::size_t* this_neighbors = neighbors; + float* this_distances = distances; + if (algo_property.query_memory_type == MemoryType::Device) { + this_query_set = d_query_set; + this_neighbors = d_neighbors; + this_distances = d_distances; + } + + if (algo_property.need_dataset_when_search) { + log_info("loading base set from dataset '%s', #vector = %zu", + dataset->name().c_str(), + dataset->base_set_size()); + const T* base_set_ptr = nullptr; + if (algo_property.dataset_memory_type == MemoryType::Host) { + log_info("%s", "loading base set to memory"); + base_set_ptr = dataset->base_set(); + } else if (algo_property.dataset_memory_type == MemoryType::HostMmap) { + log_info("%s", "mapping base set to memory"); + base_set_ptr = dataset->mapped_base_set(); + } else if (algo_property.dataset_memory_type == MemoryType::Device) { + log_info("%s", "loading base set to GPU"); + base_set_ptr = dataset->base_set_on_gpu(); + } + algo->set_search_dataset(base_set_ptr, dataset->base_set_size()); + } + + for (int i = 0, end_i = index.search_params.size(); i != end_i; ++i) { + auto p_param = create_search_param(index.algo, index.search_params[i]); + algo->set_search_param(*p_param); + log_info("search with param: %s", index.search_params[i].dump().c_str()); + + if (algo_property.query_memory_type == MemoryType::Device) { + RAFT_CUDA_TRY(cudaMemset(d_neighbors, 0, query_set_size * k * sizeof(*d_neighbors))); + RAFT_CUDA_TRY(cudaMemset(d_distances, 0, query_set_size * k * sizeof(*d_distances))); + } else { + memset(neighbors, 0, query_set_size * k * sizeof(*neighbors)); + memset(distances, 0, query_set_size * k * sizeof(*distances)); + } + + float best_search_time_average = std::numeric_limits::max(); + float best_search_time_p99 = std::numeric_limits::max(); + float best_search_time_p999 = std::numeric_limits::max(); + for (int run = 0; run < run_count; ++run) { + log_info("run %d / %d", run + 1, run_count); + for (std::size_t batch_id = 0; batch_id < num_batches; ++batch_id) { + std::size_t row = batch_id * batch_size; + int actual_batch_size = (batch_id == num_batches - 1) ? query_set_size - row : batch_size; + RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); +#ifdef NVTX + string nvtx_label = "batch" + to_string(batch_id); + if (run_count != 1) { nvtx_label = "run" + to_string(run) + "-" + nvtx_label; } + if (batch_id == 10) { + run = run_count - 1; + break; + } +#endif + Timer timer; +#ifdef NVTX + nvtxRangePush(nvtx_label.c_str()); +#endif + algo->search(this_query_set + row * dataset->dim(), + actual_batch_size, + k, + this_neighbors + row * k, + this_distances + row * k, + stream); + RAFT_CUDA_TRY(cudaStreamSynchronize(stream)); + float elapsed_ms = timer.elapsed_ms(); +#ifdef NVTX + nvtxRangePop(); +#endif + // If the size of the last batch is less than batch_size, don't count it for + // search time. But neighbors of the last batch will still be filled, so it's + // counted for recall calculation. + if (actual_batch_size == batch_size) { + search_times.push_back(elapsed_ms / 1000.0f); // in seconds + } + } + + float search_time_average = + std::accumulate(search_times.cbegin(), search_times.cend(), 0.0f) / search_times.size(); + best_search_time_average = std::min(best_search_time_average, search_time_average); + + if (search_times.size() >= 100) { + std::sort(search_times.begin(), search_times.end()); + + auto calc_percentile_pos = [](float percentile, size_t N) { + return static_cast(std::ceil(percentile / 100.0 * N)) - 1; + }; + + float search_time_p99 = search_times[calc_percentile_pos(99, search_times.size())]; + best_search_time_p99 = std::min(best_search_time_p99, search_time_p99); + + if (search_times.size() >= 1000) { + float search_time_p999 = search_times[calc_percentile_pos(99.9, search_times.size())]; + best_search_time_p999 = std::min(best_search_time_p999, search_time_p999); + } + } + search_times.clear(); + } + RAFT_CUDA_TRY(cudaDeviceSynchronize()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + if (algo_property.query_memory_type == MemoryType::Device) { + RAFT_CUDA_TRY(cudaMemcpy(neighbors, + d_neighbors, + query_set_size * k * sizeof(*d_neighbors), + cudaMemcpyDeviceToHost)); + RAFT_CUDA_TRY(cudaMemcpy(distances, + d_distances, + query_set_size * k * sizeof(*d_distances), + cudaMemcpyDeviceToHost)); + } + + for (size_t j = 0; j < query_set_size * k; ++j) { + neighbors_buf[j] = neighbors[j]; + } + write_search_result(index.search_result_file + "." + to_string(i), + dataset->name(), + dataset->distance(), + index.name, + index.algo, + index.build_param.dump(), + index.search_params[i].dump(), + batch_size, + index.run_count, + k, + best_search_time_average, + best_search_time_p99, + best_search_time_p999, + neighbors_buf, + query_set_size); + } + + log_info("finish searching for index '%s'", index.name.c_str()); + } + + delete[] neighbors; + delete[] neighbors_buf; + delete[] distances; + RAFT_CUDA_TRY(cudaFree(d_neighbors)); + RAFT_CUDA_TRY(cudaFree(d_distances)); + RAFT_CUDA_TRY(cudaStreamDestroy(stream)); +} + +inline const std::string usage(const string& argv0) +{ + return "usage: " + argv0 + " -b|s [-c] [-f] [-i index_names] conf.json\n" + + " -b: build mode, will build index\n" + + " -s: search mode, will search using built index\n" + + " one and only one of -b and -s should be specified\n" + + " -c: just check command line options and conf.json are sensible\n" + + " won't build or search\n" + " -f: force overwriting existing output files\n" + + " -i: by default will build/search all the indices found in conf.json\n" + + " '-i' can be used to select a subset of indices\n" + + " 'index_names' is a list of comma-separated index names\n" + + " '*' is allowed as the last character of a name to select all matched indices\n" + + " for example, -i \"hnsw1,hnsw2,faiss\" or -i \"hnsw*,faiss\""; +} + +template +inline int dispatch_benchmark(Configuration& conf, + std::string& index_patterns, + bool force_overwrite, + bool only_check, + bool build_mode, + bool search_mode) +{ + try { + auto dataset_conf = conf.get_dataset_conf(); + + BinDataset dataset(dataset_conf.name, + dataset_conf.base_file, + dataset_conf.subset_first_row, + dataset_conf.subset_size, + dataset_conf.query_file, + dataset_conf.distance); + + vector indices = conf.get_indices(index_patterns); + if (!check(indices, build_mode, force_overwrite)) { return -1; } + + std::string message = "will "; + message += build_mode ? "build:" : "search:"; + for (const auto& index : indices) { + message += "\n " + index.name; + } + log_info("%s", message.c_str()); + + if (only_check) { + log_info("%s", "all check passed, quit due to option -c"); + return 0; + } + + if (build_mode) { + build(&dataset, indices); + } else if (search_mode) { + search(&dataset, indices); + } + } catch (const std::exception& e) { + log_error("exception occurred: %s", e.what()); + return -1; + } + + return 0; +} + +inline int run_main(int argc, char** argv) +{ + bool force_overwrite = false; + bool build_mode = false; + bool search_mode = false; + bool only_check = false; + std::string index_patterns("*"); + + int opt; + while ((opt = getopt(argc, argv, "bscfi:h")) != -1) { + switch (opt) { + case 'b': build_mode = true; break; + case 's': search_mode = true; break; + case 'c': only_check = true; break; + case 'f': force_overwrite = true; break; + case 'i': index_patterns = optarg; break; + case 'h': cout << usage(argv[0]) << endl; return -1; + default: cerr << "\n" << usage(argv[0]) << endl; return -1; + } + } + if (build_mode == search_mode) { + std::cerr << "one and only one of -b and -s should be specified\n\n" << usage(argv[0]) << endl; + return -1; + } + if (argc - optind != 1) { + std::cerr << usage(argv[0]) << endl; + return -1; + } + string conf_file = argv[optind]; + + std::ifstream conf_stream(conf_file.c_str()); + if (!conf_stream) { + log_error("can't open configuration file: %s", argv[optind]); + return -1; + } + + try { + Configuration conf(conf_stream); + std::string dtype = conf.get_dataset_conf().dtype; + + if (dtype == "float") { + return dispatch_benchmark( + conf, index_patterns, force_overwrite, only_check, build_mode, search_mode); + } else if (dtype == "uint8") { + return dispatch_benchmark( + conf, index_patterns, force_overwrite, only_check, build_mode, search_mode); + } else if (dtype == "int8") { + return dispatch_benchmark( + conf, index_patterns, force_overwrite, only_check, build_mode, search_mode); + } else { + log_error("datatype %s not supported", dtype); + } + + } catch (const std::exception& e) { + log_error("exception occurred: %s", e.what()); + return -1; + } + + return -1; +} +}; // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/common/benchmark_util.hpp b/cpp/bench/ann/src/common/benchmark_util.hpp new file mode 100644 index 0000000000..7005883ffc --- /dev/null +++ b/cpp/bench/ann/src/common/benchmark_util.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#pragma once + +#include "ann_types.hpp" +#include + +namespace raft::bench::ann { + +inline Metric parse_metric(const std::string& metric_str) +{ + if (metric_str == "inner_product") { + return raft::bench::ann::Metric::kInnerProduct; + } else if (metric_str == "euclidean") { + return raft::bench::ann::Metric::kEuclidean; + } else { + throw std::runtime_error("invalid metric: '" + metric_str + "'"); + } +} +}; // namespace raft::bench::ann \ No newline at end of file diff --git a/cpp/bench/ann/src/common/conf.cpp b/cpp/bench/ann/src/common/conf.cpp new file mode 100644 index 0000000000..f690f68783 --- /dev/null +++ b/cpp/bench/ann/src/common/conf.cpp @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 "conf.h" + +#include +#include +#include +#include +#include + +#include "util.h" + +namespace raft::bench::ann { +using std::runtime_error; +using std::string; +using std::unordered_set; +using std::vector; + +Configuration::Configuration(std::istream& conf_stream) +{ + // to enable comments in json + auto conf = nlohmann::json::parse(conf_stream, nullptr, true, true); + + parse_dataset_(conf.at("dataset")); + parse_index_(conf.at("index"), conf.at("search_basic_param")); +} + +vector Configuration::get_indices(const string& patterns) const +{ + vector names; + for (const auto& index : indices_) { + names.push_back(index.name); + } + + auto matched = match_(names, patterns); + if (matched.empty()) { throw runtime_error("no available index matches '" + patterns + "'"); } + + vector res; + for (const auto& index : indices_) { + if (matched.find(index.name) != matched.end()) { res.push_back(index); } + } + return res; +} + +void Configuration::parse_dataset_(const nlohmann::json& conf) +{ + dataset_conf_.name = conf.at("name"); + dataset_conf_.base_file = conf.at("base_file"); + dataset_conf_.query_file = conf.at("query_file"); + dataset_conf_.distance = conf.at("distance"); + + if (conf.contains("subset_first_row")) { + dataset_conf_.subset_first_row = conf.at("subset_first_row"); + } + if (conf.contains("subset_size")) { dataset_conf_.subset_size = conf.at("subset_size"); } + + if (conf.contains("dtype")) { + dataset_conf_.dtype = conf.at("dtype"); + } else { + auto filename = dataset_conf_.base_file; + if (!filename.compare(filename.size() - 4, 4, "fbin")) { + dataset_conf_.dtype = "float"; + } else if (!filename.compare(filename.size() - 5, 5, "u8bin")) { + dataset_conf_.dtype = "uint8"; + } else if (!filename.compare(filename.size() - 5, 5, "i8bin")) { + dataset_conf_.dtype = "int8"; + } else { + log_error("Could not determine data type of the dataset"); + } + } +} + +void Configuration::parse_index_(const nlohmann::json& index_conf, + const nlohmann::json& search_basic_conf) +{ + const int batch_size = search_basic_conf.at("batch_size"); + const int k = search_basic_conf.at("k"); + const int run_count = search_basic_conf.at("run_count"); + + for (const auto& conf : index_conf) { + Index index; + index.name = conf.at("name"); + index.algo = conf.at("algo"); + index.build_param = conf.at("build_param"); + index.file = conf.at("file"); + index.batch_size = batch_size; + index.k = k; + index.run_count = run_count; + + if (conf.contains("multigpu")) { + for (auto it : conf.at("multigpu")) { + index.dev_list.push_back(it); + } + if (index.dev_list.empty()) { throw std::runtime_error("dev_list shouln't be empty!"); } + index.dev_list.shrink_to_fit(); + index.build_param["multigpu"] = conf["multigpu"]; + } + + if (conf.contains("refine_ratio")) { + float refine_ratio = conf.at("refine_ratio"); + if (refine_ratio <= 1.0f) { + throw runtime_error("'" + index.name + "': refine_ratio should > 1.0"); + } + index.refine_ratio = refine_ratio; + } + + for (const auto& param : conf.at("search_params")) { + index.search_params.push_back(param); + } + index.search_result_file = conf.at("search_result_file"); + + indices_.push_back(index); + } +} + +unordered_set Configuration::match_(const vector& candidates, + const string& patterns) const +{ + unordered_set matched; + for (const auto& pat : split(patterns, ',')) { + if (pat.empty()) { continue; } + + if (pat.back() == '*') { + auto len = pat.size() - 1; + for (const auto& item : candidates) { + if (item.compare(0, len, pat, 0, len) == 0) { matched.insert(item); } + } + } else { + for (const auto& item : candidates) { + if (item == pat) { matched.insert(item); } + } + } + } + + return matched; +} + +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/common/conf.h b/cpp/bench/ann/src/common/conf.h new file mode 100644 index 0000000000..845defe94a --- /dev/null +++ b/cpp/bench/ann/src/common/conf.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#pragma once +#include +#include +#include +#include + +#define JSON_DIAGNOSTICS 1 +#include + +namespace raft::bench::ann { + +class Configuration { + public: + struct Index { + std::string name; + std::string algo; + nlohmann::json build_param; + std::string file; + std::vector dev_list; + + int batch_size; + int k; + int run_count; + std::vector search_params; + std::string search_result_file; + float refine_ratio{0.0f}; + }; + + struct DatasetConf { + std::string name; + std::string base_file; + // use only a subset of base_file, + // the range of rows is [subset_first_row, subset_first_row + subset_size) + // however, subset_size = 0 means using all rows after subset_first_row + // that is, the subset is [subset_first_row, #rows in base_file) + size_t subset_first_row{0}; + size_t subset_size{0}; + std::string query_file; + std::string distance; + + // data type of input dataset, possible values ["float", "int8", "uint8"] + std::string dtype; + }; + + Configuration(std::istream& conf_stream); + + DatasetConf get_dataset_conf() const { return dataset_conf_; } + std::vector get_indices(const std::string& patterns) const; + + private: + void parse_dataset_(const nlohmann::json& conf); + void parse_index_(const nlohmann::json& index_conf, const nlohmann::json& search_basic_conf); + std::unordered_set match_(const std::vector& candidates, + const std::string& patterns) const; + + DatasetConf dataset_conf_; + std::vector indices_; +}; + +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/common/dataset.h b/cpp/bench/ann/src/common/dataset.h new file mode 100644 index 0000000000..1244935c99 --- /dev/null +++ b/cpp/bench/ann/src/common/dataset.h @@ -0,0 +1,381 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#pragma once +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace raft::bench::ann { + +// http://big-ann-benchmarks.com/index.html: +// binary format that starts with 8 bytes of data consisting of num_points(uint32_t) +// num_dimensions(uint32) followed by num_pts x num_dimensions x sizeof(type) bytes of +// data stored one vector after another. +// Data files will have suffixes .fbin, .u8bin, and .i8bin to represent float32, uint8 +// and int8 type data. +// As extensions for this benchmark, half and int data files will have suffixes .f16bin +// and .ibin, respectively. +template +class BinFile { + public: + BinFile(const std::string& file, + const std::string& mode, + uint32_t subset_first_row = 0, + uint32_t subset_size = 0); + ~BinFile() { fclose(fp_); } + BinFile(const BinFile&) = delete; + BinFile& operator=(const BinFile&) = delete; + + void get_shape(size_t* nrows, int* ndims) + { + assert(read_mode_); + *nrows = nrows_; + *ndims = ndims_; + } + + void read(T* data) const + { + assert(read_mode_); + size_t total = static_cast(nrows_) * ndims_; + if (fread(data, sizeof(T), total, fp_) != total) { + throw std::runtime_error("fread() BinFile " + file_ + " failed"); + } + } + + void write(const T* data, uint32_t nrows, uint32_t ndims) + { + assert(!read_mode_); + if (fwrite(&nrows, sizeof(uint32_t), 1, fp_) != 1) { + throw std::runtime_error("fwrite() BinFile " + file_ + " failed"); + } + if (fwrite(&ndims, sizeof(uint32_t), 1, fp_) != 1) { + throw std::runtime_error("fwrite() BinFile " + file_ + " failed"); + } + + size_t total = static_cast(nrows) * ndims; + if (fwrite(data, sizeof(T), total, fp_) != total) { + throw std::runtime_error("fwrite() BinFile " + file_ + " failed"); + } + } + + void* map() const + { + assert(read_mode_); + int fid = fileno(fp_); + auto mmap_ptr = mmap(NULL, file_size_, PROT_READ, MAP_PRIVATE, fid, 0); + if (mmap_ptr == MAP_FAILED) { + throw std::runtime_error("mmap error: Value of errno " + std::to_string(errno) + ", " + + std::string(strerror(errno))); + } + return mmap_ptr; + } + + void unmap(void* data) const + { + if (munmap(data, file_size_) == -1) { + throw std::runtime_error("munmap error: " + std::string(strerror(errno))); + } + } + + private: + void check_suffix_(); + + std::string file_; + FILE* fp_; + bool read_mode_; + uint32_t nrows_; + uint32_t ndims_; + size_t file_size_; +}; + +template +BinFile::BinFile(const std::string& file, + const std::string& mode, + uint32_t subset_first_row, + uint32_t subset_size) + : file_(file) +{ + check_suffix_(); + + if (mode == "r") { + read_mode_ = true; + } else if (mode == "w") { + read_mode_ = false; + if (subset_first_row != 0) { + throw std::runtime_error("subset_first_row should be zero for write mode"); + } + if (subset_size != 0) { throw std::runtime_error("subset_size should be zero for write mode"); } + } else { + throw std::runtime_error("BinFile's mode must be either 'r' or 'w': " + file_); + } + + fp_ = fopen(file_.c_str(), mode.c_str()); + if (!fp_) { throw std::runtime_error("open BinFile failed: " + file_); } + + if (read_mode_) { + struct stat statbuf; + if (stat(file_.c_str(), &statbuf) != 0) { throw std::runtime_error("stat() failed: " + file_); } + file_size_ = statbuf.st_size; + + uint32_t header[2]; + if (fread(header, sizeof(uint32_t), 2, fp_) != 2) { + throw std::runtime_error("read header of BinFile failed: " + file_); + } + nrows_ = header[0]; + ndims_ = header[1]; + + size_t expected_file_size = + 2 * sizeof(uint32_t) + static_cast(nrows_) * ndims_ * sizeof(T); + if (file_size_ != expected_file_size) { + throw std::runtime_error("expected file size of " + file_ + " is " + + std::to_string(expected_file_size) + ", however, actual size is " + + std::to_string(file_size_)); + } + + if (subset_first_row >= nrows_) { + throw std::runtime_error(file_ + ": subset_first_row (" + std::to_string(subset_first_row) + + ") >= nrows (" + std::to_string(nrows_) + ")"); + } + if (subset_first_row + subset_size > nrows_) { + throw std::runtime_error(file_ + ": subset_first_row (" + std::to_string(subset_first_row) + + ") + subset_size (" + std::to_string(subset_size) + ") > nrows (" + + std::to_string(nrows_) + ")"); + } + + if (subset_first_row) { + static_assert(sizeof(long) == 8, "fseek() don't support 64-bit offset"); + if (fseek(fp_, sizeof(T) * subset_first_row * ndims_, SEEK_CUR) == -1) { + throw std::runtime_error(file_ + ": fseek failed"); + } + nrows_ -= subset_first_row; + } + if (subset_size) { nrows_ = subset_size; } + } +} + +template +void BinFile::check_suffix_() +{ + auto pos = file_.rfind('.'); + if (pos == std::string::npos) { + throw std::runtime_error("name of BinFile doesn't have a suffix: " + file_); + } + std::string suffix = file_.substr(pos + 1); + + if constexpr (std::is_same_v) { + if (suffix != "fbin") { + throw std::runtime_error("BinFile should has .fbin suffix: " + file_); + } + } else if constexpr (std::is_same_v) { + if (suffix != "f16bin") { + throw std::runtime_error("BinFile should has .f16bin suffix: " + file_); + } + } else if constexpr (std::is_same_v) { + if (suffix != "ibin") { + throw std::runtime_error("BinFile should has .ibin suffix: " + file_); + } + } else if constexpr (std::is_same_v) { + if (suffix != "u8bin") { + throw std::runtime_error("BinFile should has .u8bin suffix: " + file_); + } + } else if constexpr (std::is_same_v) { + if (suffix != "i8bin") { + throw std::runtime_error("BinFile should has .i8bin suffix: " + file_); + } + } else { + throw std::runtime_error( + "T of BinFile should be one of float, half, int, uint8_t, or int8_t"); + } +} + +template +class Dataset { + public: + Dataset(const std::string& name) : name_(name) {} + Dataset(const std::string& name, const std::string& distance) : name_(name), distance_(distance) + { + } + Dataset(const Dataset&) = delete; + Dataset& operator=(const Dataset&) = delete; + virtual ~Dataset(); + + std::string name() const { return name_; } + std::string distance() const { return distance_; } + int dim() const { return dim_; } + size_t base_set_size() const { return base_set_size_; } + size_t query_set_size() const { return query_set_size_; } + + // load data lazily, so don't pay the overhead of reading unneeded set + // e.g. don't load base set when searching + const T* base_set() const + { + if (!base_set_) { load_base_set_(); } + return base_set_; + } + + const T* query_set() const + { + if (!query_set_) { load_query_set_(); } + return query_set_; + } + + const T* base_set_on_gpu() const; + const T* query_set_on_gpu() const; + const T* mapped_base_set() const; + + protected: + virtual void load_base_set_() const = 0; + virtual void load_query_set_() const = 0; + virtual void map_base_set_() const = 0; + + std::string name_; + std::string distance_; + int dim_; + size_t base_set_size_; + size_t query_set_size_; + + mutable T* base_set_ = nullptr; + mutable T* query_set_ = nullptr; + mutable T* d_base_set_ = nullptr; + mutable T* d_query_set_ = nullptr; + mutable T* mapped_base_set_ = nullptr; +}; + +template +Dataset::~Dataset() +{ + delete[] base_set_; + delete[] query_set_; + if (d_base_set_) { RAFT_CUDA_TRY_NO_THROW(cudaFree(d_base_set_)); } + if (d_query_set_) { RAFT_CUDA_TRY_NO_THROW(cudaFree(d_query_set_)); } +} + +template +const T* Dataset::base_set_on_gpu() const +{ + if (!d_base_set_) { + base_set(); + RAFT_CUDA_TRY(cudaMalloc((void**)&d_base_set_, base_set_size_ * dim_ * sizeof(T))); + RAFT_CUDA_TRY(cudaMemcpy( + d_base_set_, base_set_, base_set_size_ * dim_ * sizeof(T), cudaMemcpyHostToDevice)); + } + return d_base_set_; +} + +template +const T* Dataset::query_set_on_gpu() const +{ + if (!d_query_set_) { + query_set(); + RAFT_CUDA_TRY(cudaMalloc((void**)&d_query_set_, query_set_size_ * dim_ * sizeof(T))); + RAFT_CUDA_TRY(cudaMemcpy( + d_query_set_, query_set_, query_set_size_ * dim_ * sizeof(T), cudaMemcpyHostToDevice)); + } + return d_query_set_; +} + +template +const T* Dataset::mapped_base_set() const +{ + if (!mapped_base_set_) { map_base_set_(); } + return mapped_base_set_; +} + +template +class BinDataset : public Dataset { + public: + BinDataset(const std::string& name, + const std::string& base_file, + size_t subset_first_row, + size_t subset_size, + const std::string& query_file, + const std::string& distance); + ~BinDataset() + { + if (this->mapped_base_set_) { + base_file_.unmap(reinterpret_cast(this->mapped_base_set_) - subset_offset_); + } + } + + private: + void load_base_set_() const override; + void load_query_set_() const override; + void map_base_set_() const override; + + using Dataset::dim_; + using Dataset::base_set_size_; + using Dataset::query_set_size_; + + BinFile base_file_; + BinFile query_file_; + + size_t subset_offset_; +}; + +template +BinDataset::BinDataset(const std::string& name, + const std::string& base_file, + size_t subset_first_row, + size_t subset_size, + const std::string& query_file, + const std::string& distance) + : Dataset(name, distance), + base_file_(base_file, "r", subset_first_row, subset_size), + query_file_(query_file, "r"), + subset_offset_(2 * sizeof(uint32_t) + subset_first_row * dim_ * sizeof(T)) +{ + base_file_.get_shape(&base_set_size_, &dim_); + int query_dim; + query_file_.get_shape(&query_set_size_, &query_dim); + if (query_dim != dim_) { + throw std::runtime_error("base set dim (" + std::to_string(dim_) + ") != query set dim (" + + std::to_string(query_dim)); + } +} + +template +void BinDataset::load_base_set_() const +{ + this->base_set_ = new T[base_set_size_ * dim_]; + base_file_.read(this->base_set_); +} + +template +void BinDataset::load_query_set_() const +{ + this->query_set_ = new T[query_set_size_ * dim_]; + query_file_.read(this->query_set_); +} + +template +void BinDataset::map_base_set_() const +{ + char* original_map_ptr = static_cast(base_file_.map()); + this->mapped_base_set_ = reinterpret_cast(original_map_ptr + subset_offset_); +} + +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/common/util.cpp b/cpp/bench/ann/src/common/util.cpp new file mode 100644 index 0000000000..17636f76d7 --- /dev/null +++ b/cpp/bench/ann/src/common/util.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 "util.h" + +#include +#include + +#include +#include + +namespace raft::bench::ann { + +std::vector split(const std::string& s, char delimiter) +{ + std::vector tokens; + std::string token; + std::istringstream iss(s); + while (getline(iss, token, delimiter)) { + if (!token.empty()) { tokens.push_back(token); } + } + return tokens; +} + +bool file_exists(const std::string& filename) +{ + struct stat statbuf; + if (stat(filename.c_str(), &statbuf) != 0) { return false; } + return S_ISREG(statbuf.st_mode); +} + +bool dir_exists(const std::string& dir) +{ + struct stat statbuf; + if (stat(dir.c_str(), &statbuf) != 0) { return false; } + return S_ISDIR(statbuf.st_mode); +} + +bool create_dir(const std::string& dir) +{ + const auto path = split(dir, '/'); + + std::string cwd; + if (!dir.empty() && dir[0] == '/') { cwd += '/'; } + + for (const auto& p : path) { + cwd += p + "/"; + if (!dir_exists(cwd)) { + int ret = mkdir(cwd.c_str(), S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); + if (ret != 0) { return false; } + } + } + return true; +} + +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/common/util.h b/cpp/bench/ann/src/common/util.h new file mode 100644 index 0000000000..290bf4cea9 --- /dev/null +++ b/cpp/bench/ann/src/common/util.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace raft::bench::ann { + +class Timer { + public: + Timer() { reset(); } + void reset() { start_time_ = std::chrono::steady_clock::now(); } + float elapsed_ms() + { + auto end_time = std::chrono::steady_clock::now(); + auto dur = + std::chrono::duration_cast>(end_time - start_time_); + return dur.count(); + } + + private: + std::chrono::steady_clock::time_point start_time_; +}; + +std::vector split(const std::string& s, char delimiter); + +bool file_exists(const std::string& filename); +bool dir_exists(const std::string& dir); +bool create_dir(const std::string& dir); + +template +void log_(const char* level, Ts... vs) +{ + char buf[20]; + std::time_t now = std::time(nullptr); + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&now)); + printf("%s [%s] ", buf, level); + printf(vs...); + printf("\n"); + fflush(stdout); +} + +template +void log_info(Ts... vs) +{ + log_("info", vs...); +} + +template +void log_warn(Ts... vs) +{ + log_("warn", vs...); +} + +template +void log_error(Ts... vs) +{ + log_("error", vs...); +} + +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/faiss/faiss_benchmark.cu b/cpp/bench/ann/src/faiss/faiss_benchmark.cu new file mode 100644 index 0000000000..294da9a14f --- /dev/null +++ b/cpp/bench/ann/src/faiss/faiss_benchmark.cu @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 +#include +#include +#include +#include +#include +#include + +#include "../common/ann_types.hpp" +#undef WARP_SIZE +#include "faiss_wrapper.h" +#define JSON_DIAGNOSTICS 1 +#include + +namespace raft::bench::ann { + +template +void parse_build_param(const nlohmann::json& conf, + typename raft::bench::ann::FaissGpuIVFFlat::BuildParam& param) +{ + param.nlist = conf.at("nlist"); +} + +template +void parse_build_param(const nlohmann::json& conf, + typename raft::bench::ann::FaissGpuIVFPQ::BuildParam& param) +{ + param.nlist = conf.at("nlist"); + param.M = conf.at("M"); + if (conf.contains("usePrecomputed")) { + param.usePrecomputed = conf.at("usePrecomputed"); + } else { + param.usePrecomputed = false; + } + if (conf.contains("useFloat16")) { + param.useFloat16 = conf.at("useFloat16"); + } else { + param.useFloat16 = false; + } +} + +template +void parse_build_param(const nlohmann::json& conf, + typename raft::bench::ann::FaissGpuIVFSQ::BuildParam& param) +{ + param.nlist = conf.at("nlist"); + param.quantizer_type = conf.at("quantizer_type"); +} + +template +void parse_search_param(const nlohmann::json& conf, + typename raft::bench::ann::FaissGpu::SearchParam& param) +{ + param.nprobe = conf.at("nprobe"); +} + +template class Algo> +std::unique_ptr> make_algo(raft::bench::ann::Metric metric, + int dim, + const nlohmann::json& conf) +{ + typename Algo::BuildParam param; + parse_build_param(conf, param); + return std::make_unique>(metric, dim, param); +} + +template class Algo> +std::unique_ptr> make_algo(raft::bench::ann::Metric metric, + int dim, + const nlohmann::json& conf, + const std::vector& dev_list) +{ + typename Algo::BuildParam param; + parse_build_param(conf, param); + + (void)dev_list; + return std::make_unique>(metric, dim, param); +} + +template +std::unique_ptr> create_algo(const std::string& algo, + const std::string& distance, + int dim, + float refine_ratio, + const nlohmann::json& conf, + const std::vector& dev_list) +{ + // stop compiler warning; not all algorithms support multi-GPU so it may not be used + (void)dev_list; + + raft::bench::ann::Metric metric = parse_metric(distance); + std::unique_ptr> ann; + + if constexpr (std::is_same_v) { + if (algo == "faiss_gpu_ivf_flat") { + ann = make_algo(metric, dim, conf, dev_list); + } else if (algo == "faiss_gpu_ivf_pq") { + ann = make_algo(metric, dim, conf); + } else if (algo == "faiss_gpu_ivf_sq") { + ann = make_algo(metric, dim, conf); + } else if (algo == "faiss_gpu_flat") { + ann = std::make_unique>(metric, dim); + } + } + + if constexpr (std::is_same_v) {} + + if (!ann) { throw std::runtime_error("invalid algo: '" + algo + "'"); } + + if (refine_ratio > 1.0) {} + return ann; +} + +template +std::unique_ptr::AnnSearchParam> create_search_param( + const std::string& algo, const nlohmann::json& conf) +{ + if (algo == "faiss_gpu_ivf_flat" || algo == "faiss_gpu_ivf_pq" || algo == "faiss_gpu_ivf_sq") { + auto param = std::make_unique::SearchParam>(); + parse_search_param(conf, *param); + return param; + } else if (algo == "faiss_gpu_flat") { + auto param = std::make_unique::AnnSearchParam>(); + return param; + } + // else + throw std::runtime_error("invalid algo: '" + algo + "'"); +} + +} // namespace raft::bench::ann + +#include "../common/benchmark.hpp" + +int main(int argc, char** argv) { return raft::bench::ann::run_main(argc, argv); } \ No newline at end of file diff --git a/cpp/bench/ann/src/faiss/faiss_wrapper.h b/cpp/bench/ann/src/faiss/faiss_wrapper.h new file mode 100644 index 0000000000..8cfc26ea5b --- /dev/null +++ b/cpp/bench/ann/src/faiss/faiss_wrapper.h @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#ifndef FAISS_WRAPPER_H_ +#define FAISS_WRAPPER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../common/ann_types.hpp" +#include "../common/benchmark_util.hpp" +#include + +namespace { + +faiss::MetricType parse_metric_type(raft::bench::ann::Metric metric) +{ + if (metric == raft::bench::ann::Metric::kInnerProduct) { + return faiss::METRIC_INNER_PRODUCT; + } else if (metric == raft::bench::ann::Metric::kEuclidean) { + return faiss::METRIC_L2; + } else { + throw std::runtime_error("faiss supports only metric type of inner product and L2"); + } +} + +// note BLAS library can still use multi-threading, and +// setting environment variable like OPENBLAS_NUM_THREADS can control it +class OmpSingleThreadScope { + public: + OmpSingleThreadScope() + { + max_threads_ = omp_get_max_threads(); + omp_set_num_threads(1); + } + ~OmpSingleThreadScope() + { + // the best we can do + omp_set_num_threads(max_threads_); + } + + private: + int max_threads_; +}; + +} // namespace + +namespace raft::bench::ann { + +template +class FaissGpu : public ANN { + public: + using typename ANN::AnnSearchParam; + struct SearchParam : public AnnSearchParam { + int nprobe; + }; + + FaissGpu(Metric metric, int dim, int nlist); + + void build(const T* dataset, size_t nrow, cudaStream_t stream = 0) final; + + void set_search_param(const AnnSearchParam& param) override; + + // TODO: if the number of results is less than k, the remaining elements of 'neighbors' + // will be filled with (size_t)-1 + void search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream = 0) const final; + + AlgoProperty get_property() const override + { + AlgoProperty property; + // to enable building big dataset which is larger than GPU memory + property.dataset_memory_type = MemoryType::Host; + property.query_memory_type = MemoryType::Device; + property.need_dataset_when_search = false; + return property; + } + + protected: + template + void save_(const std::string& file) const; + + template + void load_(const std::string& file); + + mutable faiss::gpu::StandardGpuResources gpu_resource_; + std::unique_ptr index_; + faiss::MetricType metric_type_; + int nlist_; + int device_; +}; + +template +FaissGpu::FaissGpu(Metric metric, int dim, int nlist) + : ANN(metric, dim), metric_type_(parse_metric_type(metric)), nlist_(nlist) +{ + static_assert(std::is_same_v, "faiss support only float type"); + RAFT_CUDA_TRY(cudaGetDevice(&device_)); +} + +template +void FaissGpu::build(const T* dataset, size_t nrow, cudaStream_t stream) +{ + OmpSingleThreadScope omp_single_thread; + + gpu_resource_.setDefaultStream(device_, stream); + index_->train(nrow, dataset); // faiss::gpu::GpuIndexFlat::train() will do nothing + assert(index_->is_trained); + index_->add(nrow, dataset); +} + +template +void FaissGpu::set_search_param(const AnnSearchParam& param) +{ + int nprobe = dynamic_cast(param).nprobe; + assert(nprobe <= nlist_); + dynamic_cast(index_.get())->setNumProbes(nprobe); +} + +template +void FaissGpu::search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream) const +{ + static_assert(sizeof(size_t) == sizeof(faiss::Index::idx_t), + "sizes of size_t and faiss::Index::idx_t are different"); + gpu_resource_.setDefaultStream(device_, stream); + index_->search( + batch_size, queries, k, distances, reinterpret_cast(neighbors)); +} + +template +template +void FaissGpu::save_(const std::string& file) const +{ + OmpSingleThreadScope omp_single_thread; + + auto cpu_index = std::make_unique(); + dynamic_cast(index_.get())->copyTo(cpu_index.get()); + faiss::write_index(cpu_index.get(), file.c_str()); +} + +template +template +void FaissGpu::load_(const std::string& file) +{ + OmpSingleThreadScope omp_single_thread; + + std::unique_ptr cpu_index(dynamic_cast(faiss::read_index(file.c_str()))); + assert(cpu_index); + dynamic_cast(index_.get())->copyFrom(cpu_index.get()); +} + +template +class FaissGpuIVFFlat : public FaissGpu { + public: + struct BuildParam { + int nlist; + }; + + FaissGpuIVFFlat(Metric metric, int dim, const BuildParam& param) + : FaissGpu(metric, dim, param.nlist) + { + faiss::gpu::GpuIndexIVFFlatConfig config; + config.device = this->device_; + this->index_ = std::make_unique( + &(this->gpu_resource_), dim, param.nlist, this->metric_type_, config); + } + + void save(const std::string& file) const override + { + this->template save_(file); + } + void load(const std::string& file) override + { + this->template load_(file); + } +}; + +template +class FaissGpuIVFPQ : public FaissGpu { + public: + struct BuildParam { + int nlist; + int M; + bool useFloat16; + bool usePrecomputed; + }; + + FaissGpuIVFPQ(Metric metric, int dim, const BuildParam& param) + : FaissGpu(metric, dim, param.nlist) + { + faiss::gpu::GpuIndexIVFPQConfig config; + config.useFloat16LookupTables = param.useFloat16; + config.usePrecomputedTables = param.usePrecomputed; + config.device = this->device_; + this->index_ = + std::make_unique(&(this->gpu_resource_), + dim, + param.nlist, + param.M, + 8, // FAISS only supports bitsPerCode=8 + this->metric_type_, + config); + } + + void save(const std::string& file) const override + { + this->template save_(file); + } + void load(const std::string& file) override + { + this->template load_(file); + } +}; + +template +class FaissGpuIVFSQ : public FaissGpu { + public: + struct BuildParam { + int nlist; + std::string quantizer_type; + }; + + FaissGpuIVFSQ(Metric metric, int dim, const BuildParam& param) + : FaissGpu(metric, dim, param.nlist) + { + faiss::ScalarQuantizer::QuantizerType qtype; + if (param.quantizer_type == "fp16") { + qtype = faiss::ScalarQuantizer::QT_fp16; + } else if (param.quantizer_type == "int8") { + qtype = faiss::ScalarQuantizer::QT_8bit; + } else { + throw std::runtime_error("FaissGpuIVFSQ supports only fp16 and int8 but got " + + param.quantizer_type); + } + + faiss::gpu::GpuIndexIVFScalarQuantizerConfig config; + config.device = this->device_; + this->index_ = std::make_unique( + &(this->gpu_resource_), dim, param.nlist, qtype, this->metric_type_, true, config); + } + + void save(const std::string& file) const override + { + this->template save_( + file); + } + void load(const std::string& file) override + { + this->template load_( + file); + } +}; + +template +class FaissGpuFlat : public FaissGpu { + public: + FaissGpuFlat(Metric metric, int dim) : FaissGpu(metric, dim, 0) + { + faiss::gpu::GpuIndexFlatConfig config; + config.device = this->device_; + this->index_ = std::make_unique( + &(this->gpu_resource_), dim, this->metric_type_, config); + } + + // class FaissGpu is more like a IVF class, so need special treating here + void set_search_param(const typename ANN::AnnSearchParam&) override{}; + + void save(const std::string& file) const override + { + this->template save_(file); + } + void load(const std::string& file) override + { + this->template load_(file); + } +}; + +} // namespace raft::bench::ann + +#endif diff --git a/cpp/bench/ann/src/ggnn/ggnn_benchmark.cu b/cpp/bench/ann/src/ggnn/ggnn_benchmark.cu new file mode 100644 index 0000000000..8072cd857c --- /dev/null +++ b/cpp/bench/ann/src/ggnn/ggnn_benchmark.cu @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 +#include +#include +#include +#include +#include +#include + +#include "../common/ann_types.hpp" +#include "ggnn_wrapper.cuh" +#define JSON_DIAGNOSTICS 1 +#include + +namespace raft::bench::ann { + +template +void parse_build_param(const nlohmann::json& conf, + typename raft::bench::ann::Ggnn::BuildParam& param) +{ + param.dataset_size = conf.at("dataset_size"); + param.k = conf.at("k"); + + if (conf.contains("k_build")) { param.k_build = conf.at("k_build"); } + if (conf.contains("segment_size")) { param.segment_size = conf.at("segment_size"); } + if (conf.contains("num_layers")) { param.num_layers = conf.at("num_layers"); } + if (conf.contains("tau")) { param.tau = conf.at("tau"); } + if (conf.contains("refine_iterations")) { + param.refine_iterations = conf.at("refine_iterations"); + } +} + +template +void parse_search_param(const nlohmann::json& conf, + typename raft::bench::ann::Ggnn::SearchParam& param) +{ + param.tau = conf.at("tau"); + + if (conf.contains("block_dim")) { param.block_dim = conf.at("block_dim"); } + if (conf.contains("max_iterations")) { param.max_iterations = conf.at("max_iterations"); } + if (conf.contains("cache_size")) { param.cache_size = conf.at("cache_size"); } + if (conf.contains("sorted_size")) { param.sorted_size = conf.at("sorted_size"); } +} + +template class Algo> +std::unique_ptr> make_algo(raft::bench::ann::Metric metric, + int dim, + const nlohmann::json& conf) +{ + typename Algo::BuildParam param; + parse_build_param(conf, param); + return std::make_unique>(metric, dim, param); +} + +template class Algo> +std::unique_ptr> make_algo(raft::bench::ann::Metric metric, + int dim, + const nlohmann::json& conf, + const std::vector& dev_list) +{ + typename Algo::BuildParam param; + parse_build_param(conf, param); + + (void)dev_list; + return std::make_unique>(metric, dim, param); +} + +template +std::unique_ptr> create_algo(const std::string& algo, + const std::string& distance, + int dim, + float refine_ratio, + const nlohmann::json& conf, + const std::vector& dev_list) +{ + // stop compiler warning; not all algorithms support multi-GPU so it may not be used + (void)dev_list; + + raft::bench::ann::Metric metric = parse_metric(distance); + std::unique_ptr> ann; + + if constexpr (std::is_same_v) {} + + if constexpr (std::is_same_v) {} + + if (algo == "ggnn") { ann = make_algo(metric, dim, conf); } + if (!ann) { throw std::runtime_error("invalid algo: '" + algo + "'"); } + + if (refine_ratio > 1.0) {} + return ann; +} + +template +std::unique_ptr::AnnSearchParam> create_search_param( + const std::string& algo, const nlohmann::json& conf) +{ + if (algo == "ggnn") { + auto param = std::make_unique::SearchParam>(); + parse_search_param(conf, *param); + return param; + } + // else + throw std::runtime_error("invalid algo: '" + algo + "'"); +} + +} // namespace raft::bench::ann + +#include "../common/benchmark.hpp" + +int main(int argc, char** argv) { return raft::bench::ann::run_main(argc, argv); } \ No newline at end of file diff --git a/cpp/bench/ann/src/ggnn/ggnn_wrapper.cuh b/cpp/bench/ann/src/ggnn/ggnn_wrapper.cuh new file mode 100644 index 0000000000..fd8fe0f2ec --- /dev/null +++ b/cpp/bench/ann/src/ggnn/ggnn_wrapper.cuh @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ + +#pragma once + +#include +#include + +#include "../common/ann_types.hpp" +#include "../common/benchmark_util.hpp" +#include +#include + +namespace raft::bench::ann { + +template +class GgnnImpl; + +template +class Ggnn : public ANN { + public: + struct BuildParam { + int k_build{24}; // KBuild + int segment_size{32}; // S + int num_layers{4}; // L + float tau{0.5}; + int refine_iterations{2}; + + size_t dataset_size; + int k; // GGNN requires to know k during building + }; + + using typename ANN::AnnSearchParam; + struct SearchParam : public AnnSearchParam { + float tau; + int block_dim{32}; + int max_iterations{400}; + int cache_size{512}; + int sorted_size{256}; + }; + + Ggnn(Metric metric, int dim, const BuildParam& param); + ~Ggnn() { delete impl_; } + + void build(const T* dataset, size_t nrow, cudaStream_t stream = 0) override + { + impl_->build(dataset, nrow, stream); + } + + void set_search_param(const AnnSearchParam& param) override { impl_->set_search_param(param); } + void search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream = 0) const override + { + impl_->search(queries, batch_size, k, neighbors, distances, stream); + } + + void save(const std::string& file) const override { impl_->save(file); } + void load(const std::string& file) override { impl_->load(file); } + + AlgoProperty get_property() const override { return impl_->get_property(); } + + void set_search_dataset(const T* dataset, size_t nrow) override + { + impl_->set_search_dataset(dataset, nrow); + }; + + private: + ANN* impl_; +}; + +template +Ggnn::Ggnn(Metric metric, int dim, const BuildParam& param) : ANN(metric, dim) +{ + // ggnn/src/sift1m.cu + if (metric == Metric::kEuclidean && dim == 128 && param.k_build == 24 && param.k == 10 && + param.segment_size == 32) { + impl_ = new GgnnImpl(metric, dim, param); + } + // ggnn/src/deep1b_multi_gpu.cu, and adapt it deep1B + else if (metric == Metric::kEuclidean && dim == 96 && param.k_build == 24 && param.k == 10 && + param.segment_size == 32) { + impl_ = new GgnnImpl(metric, dim, param); + } else if (metric == Metric::kInnerProduct && dim == 96 && param.k_build == 24 && param.k == 10 && + param.segment_size == 32) { + impl_ = new GgnnImpl(metric, dim, param); + } else if (metric == Metric::kInnerProduct && dim == 96 && param.k_build == 96 && param.k == 10 && + param.segment_size == 64) { + impl_ = new GgnnImpl(metric, dim, param); + } + // ggnn/src/glove200.cu, adapt it to glove100 + else if (metric == Metric::kInnerProduct && dim == 100 && param.k_build == 96 && param.k == 10 && + param.segment_size == 64) { + impl_ = new GgnnImpl(metric, dim, param); + } else { + throw std::runtime_error( + "ggnn: not supported combination of metric, dim and build param; " + "see Ggnn's constructor in ggnn_wrapper.cuh for available combinations"); + } +} + +template +class GgnnImpl : public ANN { + public: + using typename ANN::AnnSearchParam; + + GgnnImpl(Metric metric, int dim, const typename Ggnn::BuildParam& param); + + void build(const T* dataset, size_t nrow, cudaStream_t stream = 0) override; + + void set_search_param(const AnnSearchParam& param) override; + void search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream = 0) const override; + + void save(const std::string& file) const override; + void load(const std::string& file) override; + + AlgoProperty get_property() const override + { + AlgoProperty property; + property.dataset_memory_type = MemoryType::Device; + property.query_memory_type = MemoryType::Device; + property.need_dataset_when_search = true; + return property; + } + + void set_search_dataset(const T* dataset, size_t nrow) override; + + private: + using ANN::metric_; + using ANN::dim_; + + using GGNNGPUInstance = GGNNGPUInstance; + std::unique_ptr ggnn_; + typename Ggnn::BuildParam build_param_; + typename Ggnn::SearchParam search_param_; +}; + +template +GgnnImpl::GgnnImpl(Metric metric, + int dim, + const typename Ggnn::BuildParam& param) + : ANN(metric, dim), build_param_(param) +{ + if (metric_ == Metric::kInnerProduct) { + if (measure != Cosine) { throw std::runtime_error("mis-matched metric"); } + } else if (metric_ == Metric::kEuclidean) { + if (measure != Euclidean) { throw std::runtime_error("mis-matched metric"); } + } else { + throw std::runtime_error( + "ggnn supports only metric type of InnerProduct, Cosine and Euclidean"); + } + + if (dim != D) { throw std::runtime_error("mis-matched dim"); } + + int device; + RAFT_CUDA_TRY(cudaGetDevice(&device)); + + ggnn_ = std::make_unique( + device, build_param_.dataset_size, build_param_.num_layers, true, build_param_.tau); +} + +template +void GgnnImpl::build(const T* dataset, + size_t nrow, + cudaStream_t stream) +{ + if (nrow != build_param_.dataset_size) { + throw std::runtime_error( + "build_param_.dataset_size = " + std::to_string(build_param_.dataset_size) + + " , but nrow = " + std::to_string(nrow)); + } + + ggnn_->set_base_data(dataset); + ggnn_->set_stream(stream); + ggnn_->build(0); + for (int i = 0; i < build_param_.refine_iterations; ++i) { + ggnn_->refine(); + } +} + +template +void GgnnImpl::set_search_dataset(const T* dataset, size_t nrow) +{ + if (nrow != build_param_.dataset_size) { + throw std::runtime_error( + "build_param_.dataset_size = " + std::to_string(build_param_.dataset_size) + + " , but nrow = " + std::to_string(nrow)); + } + ggnn_->set_base_data(dataset); +} + +template +void GgnnImpl::set_search_param(const AnnSearchParam& param) +{ + search_param_ = dynamic_cast::SearchParam&>(param); +} + +template +void GgnnImpl::search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream) const +{ + static_assert(sizeof(size_t) == sizeof(int64_t), "sizes of size_t and GGNN's KeyT are different"); + if (k != KQuery) { + throw std::runtime_error( + "k = " + std::to_string(k) + + ", but this GGNN instance only supports k = " + std::to_string(KQuery)); + } + + ggnn_->set_stream(stream); + RAFT_CUDA_TRY(cudaMemcpyToSymbol(c_tau_query, &search_param_.tau, sizeof(float))); + + const int block_dim = search_param_.block_dim; + const int max_iterations = search_param_.max_iterations; + const int cache_size = search_param_.cache_size; + const int sorted_size = search_param_.sorted_size; + // default value + if (block_dim == 32 && max_iterations == 400 && cache_size == 512 && sorted_size == 256) { + ggnn_->template queryLayer<32, 400, 512, 256, false>( + queries, batch_size, reinterpret_cast(neighbors), distances); + } + // ggnn/src/sift1m.cu + else if (block_dim == 32 && max_iterations == 200 && cache_size == 256 && sorted_size == 64) { + ggnn_->template queryLayer<32, 200, 256, 64, false>( + queries, batch_size, reinterpret_cast(neighbors), distances); + } + // ggnn/src/sift1m.cu + else if (block_dim == 32 && max_iterations == 400 && cache_size == 448 && sorted_size == 64) { + ggnn_->template queryLayer<32, 400, 448, 64, false>( + queries, batch_size, reinterpret_cast(neighbors), distances); + } + // ggnn/src/glove200.cu + else if (block_dim == 128 && max_iterations == 2000 && cache_size == 2048 && sorted_size == 32) { + ggnn_->template queryLayer<128, 2000, 2048, 32, false>( + queries, batch_size, reinterpret_cast(neighbors), distances); + } + // for glove100 + else if (block_dim == 64 && max_iterations == 400 && cache_size == 512 && sorted_size == 32) { + ggnn_->template queryLayer<64, 400, 512, 32, false>( + queries, batch_size, reinterpret_cast(neighbors), distances); + } else if (block_dim == 128 && max_iterations == 2000 && cache_size == 1024 && + sorted_size == 32) { + ggnn_->template queryLayer<128, 2000, 1024, 32, false>( + queries, batch_size, reinterpret_cast(neighbors), distances); + } else { + throw std::runtime_error("ggnn: not supported search param"); + } +} + +template +void GgnnImpl::save(const std::string& file) const +{ + auto& ggnn_host = ggnn_->ggnn_cpu_buffers.at(0); + auto& ggnn_device = ggnn_->ggnn_shards.at(0); + ggnn_->set_stream(0); + + ggnn_host.downloadAsync(ggnn_device); + RAFT_CUDA_TRY(cudaStreamSynchronize(ggnn_device.stream)); + ggnn_host.store(file); +} + +template +void GgnnImpl::load(const std::string& file) +{ + auto& ggnn_host = ggnn_->ggnn_cpu_buffers.at(0); + auto& ggnn_device = ggnn_->ggnn_shards.at(0); + ggnn_->set_stream(0); + + ggnn_host.load(file); + ggnn_host.uploadAsync(ggnn_device); + RAFT_CUDA_TRY(cudaStreamSynchronize(ggnn_device.stream)); +} + +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/hnswlib/hnswlib_benchmark.cpp b/cpp/bench/ann/src/hnswlib/hnswlib_benchmark.cpp new file mode 100644 index 0000000000..cd823e8a69 --- /dev/null +++ b/cpp/bench/ann/src/hnswlib/hnswlib_benchmark.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 +#include +#include +#include +#include +#include +#include + +#include "../common/benchmark_util.hpp" + +#include "../common/ann_types.hpp" +#undef WARP_SIZE +#include "hnswlib_wrapper.h" +#define JSON_DIAGNOSTICS 1 +#include + +namespace raft::bench::ann { + +template +void parse_build_param(const nlohmann::json& conf, + typename raft::bench::ann::HnswLib::BuildParam& param) +{ + param.ef_construction = conf.at("efConstruction"); + param.M = conf.at("M"); + if (conf.contains("numThreads")) { param.num_threads = conf.at("numThreads"); } +} + +template +void parse_search_param(const nlohmann::json& conf, + typename raft::bench::ann::HnswLib::SearchParam& param) +{ + param.ef = conf.at("ef"); + if (conf.contains("numThreads")) { param.num_threads = conf.at("numThreads"); } +} + +template class Algo> +std::unique_ptr> make_algo(raft::bench::ann::Metric metric, + int dim, + const nlohmann::json& conf) +{ + typename Algo::BuildParam param; + parse_build_param(conf, param); + return std::make_unique>(metric, dim, param); +} + +template class Algo> +std::unique_ptr> make_algo(raft::bench::ann::Metric metric, + int dim, + const nlohmann::json& conf, + const std::vector& dev_list) +{ + typename Algo::BuildParam param; + parse_build_param(conf, param); + + (void)dev_list; + return std::make_unique>(metric, dim, param); +} + +template +std::unique_ptr> create_algo(const std::string& algo, + const std::string& distance, + int dim, + float refine_ratio, + const nlohmann::json& conf, + const std::vector& dev_list) +{ + // stop compiler warning; not all algorithms support multi-GPU so it may not be used + (void)dev_list; + + raft::bench::ann::Metric metric = parse_metric(distance); + std::unique_ptr> ann; + + if constexpr (std::is_same_v) { + if (algo == "hnswlib") { ann = make_algo(metric, dim, conf); } + } + + if constexpr (std::is_same_v) { + if (algo == "hnswlib") { ann = make_algo(metric, dim, conf); } + } + + if (!ann) { throw std::runtime_error("invalid algo: '" + algo + "'"); } + + if (refine_ratio > 1.0) {} + return ann; +} + +template +std::unique_ptr::AnnSearchParam> create_search_param( + const std::string& algo, const nlohmann::json& conf) +{ + if (algo == "hnswlib") { + auto param = std::make_unique::SearchParam>(); + parse_search_param(conf, *param); + return param; + } + // else + throw std::runtime_error("invalid algo: '" + algo + "'"); +} + +}; // namespace raft::bench::ann + +#include "../common/benchmark.hpp" + +int main(int argc, char** argv) { return raft::bench::ann::run_main(argc, argv); } \ No newline at end of file diff --git a/cpp/bench/ann/src/hnswlib/hnswlib_wrapper.h b/cpp/bench/ann/src/hnswlib/hnswlib_wrapper.h new file mode 100644 index 0000000000..c5c3a4a2a6 --- /dev/null +++ b/cpp/bench/ann/src/hnswlib/hnswlib_wrapper.h @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/ann_types.hpp" +#include + +namespace raft::bench::ann { + +template +struct hnsw_dist_t { + using type = void; +}; + +template <> +struct hnsw_dist_t { + using type = float; +}; + +template <> +struct hnsw_dist_t { + using type = int; +}; + +class FixedThreadPool { + public: + FixedThreadPool(int num_threads) + { + if (num_threads < 1) { + throw std::runtime_error("num_threads must >= 1"); + } else if (num_threads == 1) { + return; + } + + tasks_ = new Task_[num_threads]; + + threads_.reserve(num_threads); + for (int i = 0; i < num_threads; ++i) { + threads_.emplace_back([&, i] { + auto& task = tasks_[i]; + while (true) { + std::unique_lock lock(task.mtx); + task.cv.wait(lock, + [&] { return task.has_task || finished_.load(std::memory_order_relaxed); }); + if (finished_.load(std::memory_order_relaxed)) { break; } + + task.task(); + task.has_task = false; + } + }); + } + } + + ~FixedThreadPool() + { + if (threads_.empty()) { return; } + + finished_.store(true, std::memory_order_relaxed); + for (unsigned i = 0; i < threads_.size(); ++i) { + auto& task = tasks_[i]; + std::lock_guard(task.mtx); + + task.cv.notify_one(); + threads_[i].join(); + } + + delete[] tasks_; + } + + template + void submit(Func f, IdxT len) + { + if (threads_.empty()) { + for (IdxT i = 0; i < len; ++i) { + f(i); + } + return; + } + + const int num_threads = threads_.size(); + // one extra part for competition among threads + const IdxT items_per_thread = len / (num_threads + 1); + std::atomic cnt(items_per_thread * num_threads); + + auto wrapped_f = [&](IdxT start, IdxT end) { + for (IdxT i = start; i < end; ++i) { + f(i); + } + + while (true) { + IdxT i = cnt.fetch_add(1, std::memory_order_relaxed); + if (i >= len) { break; } + f(i); + } + }; + + std::vector> futures; + futures.reserve(num_threads); + for (int i = 0; i < num_threads; ++i) { + IdxT start = i * items_per_thread; + auto& task = tasks_[i]; + { + std::lock_guard lock(task.mtx); + (void)lock; // stop nvcc warning + task.task = std::packaged_task([=] { wrapped_f(start, start + items_per_thread); }); + futures.push_back(task.task.get_future()); + task.has_task = true; + } + task.cv.notify_one(); + } + + for (auto& fut : futures) { + fut.wait(); + } + return; + } + + private: + struct alignas(64) Task_ { + std::mutex mtx; + std::condition_variable cv; + bool has_task = false; + std::packaged_task task; + }; + + Task_* tasks_; + std::vector threads_; + std::atomic finished_{false}; +}; + +template +class HnswLib : public ANN { + public: + // https://github.com/nmslib/hnswlib/blob/master/ALGO_PARAMS.md + struct BuildParam { + int M; + int ef_construction; + int num_threads{1}; + }; + + using typename ANN::AnnSearchParam; + struct SearchParam : public AnnSearchParam { + int ef; + int num_threads{1}; + }; + + HnswLib(Metric metric, int dim, const BuildParam& param); + + void build(const T* dataset, size_t nrow, cudaStream_t stream = 0) override; + + void set_search_param(const AnnSearchParam& param) override; + void search(const T* query, + int batch_size, + int k, + size_t* indices, + float* distances, + cudaStream_t stream = 0) const override; + + void save(const std::string& path_to_index) const override; + void load(const std::string& path_to_index) override; + + AlgoProperty get_property() const override + { + AlgoProperty property; + property.dataset_memory_type = MemoryType::Host; + property.query_memory_type = MemoryType::Host; + property.need_dataset_when_search = false; + return property; + } + + private: + void get_search_knn_results_(const T* query, int k, size_t* indices, float* distances) const; + + std::unique_ptr::type>> appr_alg_; + std::unique_ptr::type>> space_; + + using ANN::metric_; + using ANN::dim_; + int ef_construction_; + int m_; + int num_threads_; + std::unique_ptr thread_pool_; +}; + +template +HnswLib::HnswLib(Metric metric, int dim, const BuildParam& param) : ANN(metric, dim) +{ + assert(dim_ > 0); + static_assert(std::is_same_v || std::is_same_v); + if constexpr (std::is_same_v) { + if (metric_ != Metric::kEuclidean) { + throw std::runtime_error("hnswlib only supports Euclidean distance"); + } + } + + ef_construction_ = param.ef_construction; + m_ = param.M; + num_threads_ = param.num_threads; +} + +template +void HnswLib::build(const T* dataset, size_t nrow, cudaStream_t) +{ + if constexpr (std::is_same_v) { + if (metric_ == Metric::kInnerProduct) { + space_ = std::make_unique(dim_); + } else { + space_ = std::make_unique(dim_); + } + } else if constexpr (std::is_same_v) { + space_ = std::make_unique(dim_); + } + + appr_alg_ = std::make_unique::type>>( + space_.get(), nrow, m_, ef_construction_); + + thread_pool_ = std::make_unique(num_threads_); + const size_t items_per_thread = nrow / (num_threads_ + 1); + + thread_pool_->submit( + [&](size_t i) { + if (i < items_per_thread && i % 10000 == 0) { + char buf[20]; + std::time_t now = std::time(nullptr); + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&now)); + + printf("%s building %zu / %zu\n", buf, i, items_per_thread); + fflush(stdout); + } + + appr_alg_->addPoint(dataset + i * dim_, i); + }, + nrow); +} + +template +void HnswLib::set_search_param(const AnnSearchParam& param_) +{ + auto param = dynamic_cast(param_); + appr_alg_->ef_ = param.ef; + + if (!thread_pool_ || num_threads_ != param.num_threads) { + num_threads_ = param.num_threads; + thread_pool_ = std::make_unique(num_threads_); + } +} + +template +void HnswLib::search( + const T* query, int batch_size, int k, size_t* indices, float* distances, cudaStream_t) const +{ + thread_pool_->submit( + [&](int i) { + get_search_knn_results_(query + i * dim_, k, indices + i * k, distances + i * k); + }, + batch_size); +} + +template +void HnswLib::save(const std::string& path_to_index) const +{ + appr_alg_->saveIndex(std::string(path_to_index)); +} + +template +void HnswLib::load(const std::string& path_to_index) +{ + if constexpr (std::is_same_v) { + if (metric_ == Metric::kInnerProduct) { + space_ = std::make_unique(dim_); + } else { + space_ = std::make_unique(dim_); + } + } else if constexpr (std::is_same_v) { + space_ = std::make_unique(dim_); + } + + appr_alg_ = std::make_unique::type>>( + space_.get(), path_to_index); +} + +template +void HnswLib::get_search_knn_results_(const T* query, + int k, + size_t* indices, + float* distances) const +{ + auto result = appr_alg_->searchKnn(query, k); + assert(result.size() >= static_cast(k)); + + for (int i = k - 1; i >= 0; --i) { + indices[i] = result.top().second; + distances[i] = result.top().first; + result.pop(); + } +} + +}; // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/raft/raft_ann_bench_utils.h b/cpp/bench/ann/src/raft/raft_ann_bench_utils.h new file mode 100644 index 0000000000..cb30c2693f --- /dev/null +++ b/cpp/bench/ann/src/raft/raft_ann_bench_utils.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace raft::bench::ann { + +inline raft::distance::DistanceType parse_metric_type(raft::bench::ann::Metric metric) +{ + if (metric == raft::bench::ann::Metric::kInnerProduct) { + return raft::distance::DistanceType::InnerProduct; + } else if (metric == raft::bench::ann::Metric::kEuclidean) { + // Even for L2 expanded RAFT IVF Flat uses unexpanded formula + return raft::distance::DistanceType::L2Expanded; + } else { + throw std::runtime_error("raft supports only metric type of inner product and L2"); + } +} +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/raft/raft_benchmark.cu b/cpp/bench/ann/src/raft/raft_benchmark.cu new file mode 100644 index 0000000000..d8e98ce2a9 --- /dev/null +++ b/cpp/bench/ann/src/raft/raft_benchmark.cu @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 +#include +#include +#include +#include +#include +#include + +#ifdef RAFT_COMPILED +#include +#endif + +#include "../common/ann_types.hpp" +#include "../common/benchmark_util.hpp" +#undef WARP_SIZE +#ifdef RAFT_ANN_BENCH_USE_RAFT_BFKNN +#include "raft_wrapper.h" +#endif +#ifdef RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT +#include "raft_ivf_flat_wrapper.h" +extern template class raft::bench::ann::RaftIvfFlatGpu; +extern template class raft::bench::ann::RaftIvfFlatGpu; +extern template class raft::bench::ann::RaftIvfFlatGpu; +#endif +#ifdef RAFT_ANN_BENCH_USE_RAFT_IVF_PQ +#include "raft_ivf_pq_wrapper.h" +extern template class raft::bench::ann::RaftIvfPQ; +extern template class raft::bench::ann::RaftIvfPQ; +extern template class raft::bench::ann::RaftIvfPQ; +#endif +#define JSON_DIAGNOSTICS 1 +#include + +namespace raft::bench::ann { + +#ifdef RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT +template +void parse_build_param(const nlohmann::json& conf, + typename raft::bench::ann::RaftIvfFlatGpu::BuildParam& param) +{ + param.n_lists = conf.at("nlist"); + if (conf.contains("niter")) { param.kmeans_n_iters = conf.at("niter"); } + if (conf.contains("ratio")) { + param.kmeans_trainset_fraction = 1.0 / (double)conf.at("ratio"); + std::cout << "kmeans_trainset_fraction " << param.kmeans_trainset_fraction; + } +} + +template +void parse_search_param(const nlohmann::json& conf, + typename raft::bench::ann::RaftIvfFlatGpu::SearchParam& param) +{ + param.ivf_flat_params.n_probes = conf.at("nprobe"); +} +#endif + +#ifdef RAFT_ANN_BENCH_USE_RAFT_IVF_PQ +template +void parse_build_param(const nlohmann::json& conf, + typename raft::bench::ann::RaftIvfPQ::BuildParam& param) +{ + param.n_lists = conf.at("nlist"); + if (conf.contains("niter")) { param.kmeans_n_iters = conf.at("niter"); } + if (conf.contains("ratio")) { param.kmeans_trainset_fraction = 1.0 / (double)conf.at("ratio"); } + if (conf.contains("pq_bits")) { param.pq_bits = conf.at("pq_bits"); } + if (conf.contains("pq_dim")) { param.pq_dim = conf.at("pq_dim"); } +} + +template +void parse_search_param(const nlohmann::json& conf, + typename raft::bench::ann::RaftIvfPQ::SearchParam& param) +{ + param.pq_param.n_probes = conf.at("numProbes"); + if (conf.contains("internalDistanceDtype")) { + std::string type = conf.at("internalDistanceDtype"); + if (type == "float") { + param.pq_param.internal_distance_dtype = CUDA_R_32F; + } else if (type == "half") { + param.pq_param.internal_distance_dtype = CUDA_R_16F; + } else { + throw std::runtime_error("internalDistanceDtype: '" + type + + "', should be either 'float' or 'half'"); + } + } else { + // set half as default type + param.pq_param.internal_distance_dtype = CUDA_R_16F; + } + + if (conf.contains("smemLutDtype")) { + std::string type = conf.at("smemLutDtype"); + if (type == "float") { + param.pq_param.lut_dtype = CUDA_R_32F; + } else if (type == "half") { + param.pq_param.lut_dtype = CUDA_R_16F; + } else if (type == "fp8") { + param.pq_param.lut_dtype = CUDA_R_8U; + } else { + throw std::runtime_error("smemLutDtype: '" + type + + "', should be either 'float', 'half' or 'fp8'"); + } + } else { + // set half as default + param.pq_param.lut_dtype = CUDA_R_16F; + } +} +#endif + +template class Algo> +std::unique_ptr> make_algo(raft::bench::ann::Metric metric, + int dim, + const nlohmann::json& conf) +{ + typename Algo::BuildParam param; + parse_build_param(conf, param); + return std::make_unique>(metric, dim, param); +} + +template class Algo> +std::unique_ptr> make_algo(raft::bench::ann::Metric metric, + int dim, + const nlohmann::json& conf, + const std::vector& dev_list) +{ + typename Algo::BuildParam param; + parse_build_param(conf, param); + + (void)dev_list; + return std::make_unique>(metric, dim, param); +} + +template +std::unique_ptr> create_algo(const std::string& algo, + const std::string& distance, + int dim, + float refine_ratio, + const nlohmann::json& conf, + const std::vector& dev_list) +{ + // stop compiler warning; not all algorithms support multi-GPU so it may not be used + (void)dev_list; + + raft::bench::ann::Metric metric = parse_metric(distance); + std::unique_ptr> ann; + + if constexpr (std::is_same_v) { +#ifdef RAFT_ANN_BENCH_USE_RAFT_BFKNN + if (algo == "raft_bfknn") { ann = std::make_unique>(metric, dim); } +#endif + } + + if constexpr (std::is_same_v) {} + +#ifdef RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT + if (algo == "raft_ivf_flat") { + typename raft::bench::ann::RaftIvfFlatGpu::BuildParam param; + parse_build_param(conf, param); + ann = std::make_unique>(metric, dim, param); + } +#endif +#ifdef RAFT_ANN_BENCH_USE_RAFT_IVF_PQ + if (algo == "raft_ivf_pq") { + typename raft::bench::ann::RaftIvfPQ::BuildParam param; + parse_build_param(conf, param); + ann = + std::make_unique>(metric, dim, param, refine_ratio); + } +#endif + if (!ann) { throw std::runtime_error("invalid algo: '" + algo + "'"); } + + if (refine_ratio > 1.0) {} + return ann; +} + +template +std::unique_ptr::AnnSearchParam> create_search_param( + const std::string& algo, const nlohmann::json& conf) +{ +#ifdef RAFT_ANN_BENCH_USE_RAFT_BFKNN + if (algo == "raft_bfknn") { + auto param = std::make_unique::AnnSearchParam>(); + return param; + } +#endif +#ifdef RAFT_ANN_BENCH_USE_RAFT_IVF_FLAT + if (algo == "raft_ivf_flat") { + auto param = + std::make_unique::SearchParam>(); + parse_search_param(conf, *param); + return param; + } +#endif +#ifdef RAFT_ANN_BENCH_USE_RAFT_IVF_PQ + if (algo == "raft_ivf_pq") { + auto param = std::make_unique::SearchParam>(); + parse_search_param(conf, *param); + return param; + } +#endif + // else + throw std::runtime_error("invalid algo: '" + algo + "'"); +} + +}; // namespace raft::bench::ann + +#include "../common/benchmark.hpp" + +int main(int argc, char** argv) { return raft::bench::ann::run_main(argc, argv); } \ No newline at end of file diff --git a/cpp/bench/ann/src/raft/raft_ivf_flat.cu b/cpp/bench/ann/src/raft/raft_ivf_flat.cu new file mode 100644 index 0000000000..ff108080b5 --- /dev/null +++ b/cpp/bench/ann/src/raft/raft_ivf_flat.cu @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 "raft_ivf_flat_wrapper.h" + +#ifdef RAFT_COMPILED +#include +#endif + +namespace raft::bench::ann { +template class RaftIvfFlatGpu; +template class RaftIvfFlatGpu; +template class RaftIvfFlatGpu; +} // namespace raft::bench::ann \ No newline at end of file diff --git a/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h b/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h new file mode 100644 index 0000000000..8b2a7d329b --- /dev/null +++ b/cpp/bench/ann/src/raft/raft_ivf_flat_wrapper.h @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/ann_types.hpp" +#include "raft_ann_bench_utils.h" +#include + +namespace raft::bench::ann { + +template +class RaftIvfFlatGpu : public ANN { + public: + using typename ANN::AnnSearchParam; + + struct SearchParam : public AnnSearchParam { + raft::neighbors::ivf_flat::search_params ivf_flat_params; + }; + + using BuildParam = raft::neighbors::ivf_flat::index_params; + + RaftIvfFlatGpu(Metric metric, int dim, const BuildParam& param); + + void build(const T* dataset, size_t nrow, cudaStream_t stream) final; + + void set_search_param(const AnnSearchParam& param) override; + + // TODO: if the number of results is less than k, the remaining elements of 'neighbors' + // will be filled with (size_t)-1 + void search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream = 0) const override; + + // to enable dataset access from GPU memory + AlgoProperty get_property() const override + { + AlgoProperty property; + property.dataset_memory_type = MemoryType::Device; + property.query_memory_type = MemoryType::Device; + property.need_dataset_when_search = false; + return property; + } + void save(const std::string& file) const override; + void load(const std::string&) override; + + private: + raft::device_resources handle_; + BuildParam index_params_; + raft::neighbors::ivf_flat::search_params search_params_; + std::optional> index_; + int device_; + int dimension_; + rmm::mr::pool_memory_resource mr_; +}; + +template +RaftIvfFlatGpu::RaftIvfFlatGpu(Metric metric, int dim, const BuildParam& param) + : ANN(metric, dim), + index_params_(param), + dimension_(dim), + mr_(rmm::mr::get_current_device_resource(), 1024 * 1024 * 1024ull) +{ + index_params_.metric = parse_metric_type(metric); + RAFT_CUDA_TRY(cudaGetDevice(&device_)); +} + +template +void RaftIvfFlatGpu::build(const T* dataset, size_t nrow, cudaStream_t) +{ + index_.emplace( + raft::neighbors::ivf_flat::build(handle_, index_params_, dataset, IdxT(nrow), dimension_)); + return; +} + +template +void RaftIvfFlatGpu::set_search_param(const AnnSearchParam& param) +{ + auto search_param = dynamic_cast(param); + search_params_ = search_param.ivf_flat_params; + assert(search_params_.n_probes <= index_params_.n_lists); +} + +template +void RaftIvfFlatGpu::save(const std::string& file) const +{ + raft::neighbors::ivf_flat::serialize(handle_, file, *index_); + return; +} + +template +void RaftIvfFlatGpu::load(const std::string& file) +{ + index_ = raft::neighbors::ivf_flat::deserialize(handle_, file); + return; +} + +template +void RaftIvfFlatGpu::search( + const T* queries, int batch_size, int k, size_t* neighbors, float* distances, cudaStream_t) const +{ + rmm::mr::device_memory_resource* mr_ptr = &const_cast(this)->mr_; + static_assert(sizeof(size_t) == sizeof(IdxT), "IdxT is incompatible with size_t"); + raft::neighbors::ivf_flat::search( + handle_, search_params_, *index_, queries, batch_size, k, (IdxT*)neighbors, distances, mr_ptr); + handle_.sync_stream(); + return; +} +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/raft/raft_ivf_pq.cu b/cpp/bench/ann/src/raft/raft_ivf_pq.cu new file mode 100644 index 0000000000..338bc9a32f --- /dev/null +++ b/cpp/bench/ann/src/raft/raft_ivf_pq.cu @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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 "raft_ivf_pq_wrapper.h" + +#ifdef RAFT_COMPILED +#include +#endif + +namespace raft::bench::ann { +template class RaftIvfPQ; +template class RaftIvfPQ; +template class RaftIvfPQ; +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h b/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h new file mode 100644 index 0000000000..70dff81847 --- /dev/null +++ b/cpp/bench/ann/src/raft/raft_ivf_pq_wrapper.h @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/ann_types.hpp" +#include "raft_ann_bench_utils.h" +#include + +namespace raft::bench::ann { + +template +class RaftIvfPQ : public ANN { + public: + using typename ANN::AnnSearchParam; + + struct SearchParam : public AnnSearchParam { + raft::neighbors::ivf_pq::search_params pq_param; + }; + + using BuildParam = raft::neighbors::ivf_pq::index_params; + + RaftIvfPQ(Metric metric, int dim, const BuildParam& param, float refine_ratio); + + void build(const T* dataset, size_t nrow, cudaStream_t stream) final; + + void set_search_param(const AnnSearchParam& param) override; + void set_search_dataset(const T* dataset, size_t nrow) override; + + // TODO: if the number of results is less than k, the remaining elements of 'neighbors' + // will be filled with (size_t)-1 + void search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream = 0) const override; + + // to enable dataset access from GPU memory + AlgoProperty get_property() const override + { + AlgoProperty property; + property.dataset_memory_type = MemoryType::Host; + property.query_memory_type = MemoryType::Device; + property.need_dataset_when_search = true; // actually it is only used during refinement + return property; + } + void save(const std::string& file) const override; + void load(const std::string&) override; + + private: + raft::device_resources handle_; + BuildParam index_params_; + raft::neighbors::ivf_pq::search_params search_params_; + std::optional> index_; + int device_; + int dimension_; + float refine_ratio_ = 1.0; + rmm::mr::pool_memory_resource mr_; + raft::device_matrix_view dataset_; +}; +template +RaftIvfPQ::RaftIvfPQ(Metric metric, int dim, const BuildParam& param, float refine_ratio) + : ANN(metric, dim), + index_params_(param), + dimension_(dim), + refine_ratio_(refine_ratio), + mr_(rmm::mr::get_current_device_resource(), 1024 * 1024 * 1024ull) +{ + index_params_.metric = parse_metric_type(metric); + RAFT_CUDA_TRY(cudaGetDevice(&device_)); +} + +template +void RaftIvfPQ::save(const std::string& file) const +{ + raft::runtime::neighbors::ivf_pq::serialize(handle_, file, *index_); +} + +template +void RaftIvfPQ::load(const std::string& file) +{ + auto index_tmp = raft::neighbors::ivf_pq::index(handle_, index_params_, dimension_); + raft::runtime::neighbors::ivf_pq::deserialize(handle_, file, &index_tmp); + index_.emplace(std::move(index_tmp)); + return; +} + +template +void RaftIvfPQ::build(const T* dataset, size_t nrow, cudaStream_t) +{ + auto dataset_v = raft::make_device_matrix_view(dataset, IdxT(nrow), index_->dim()); + + index_.emplace(raft::runtime::neighbors::ivf_pq::build(handle_, index_params_, dataset_v)); + return; +} + +template +void RaftIvfPQ::set_search_param(const AnnSearchParam& param) +{ + auto search_param = dynamic_cast(param); + search_params_ = search_param.pq_param; + assert(search_params_.n_probes <= index_params_.n_lists); +} + +template +void RaftIvfPQ::set_search_dataset(const T* dataset, size_t nrow) +{ + dataset_ = raft::make_device_matrix_view(dataset, nrow, index_->dim()); +} + +template +void RaftIvfPQ::search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream) const +{ + if (refine_ratio_ > 1.0f) { + uint32_t k0 = static_cast(refine_ratio_ * k); + auto queries_v = + raft::make_device_matrix_view(queries, batch_size, index_->dim()); + auto distances_tmp = raft::make_device_matrix(handle_, batch_size, k0); + auto candidates = raft::make_device_matrix(handle_, batch_size, k0); + + raft::runtime::neighbors::ivf_pq::search( + handle_, search_params_, *index_, queries_v, candidates.view(), distances_tmp.view()); + + if (get_property().dataset_memory_type == MemoryType::Device) { + auto queries_v = + raft::make_device_matrix_view(queries, batch_size, index_->dim()); + auto neighbors_v = raft::make_device_matrix_view((IdxT*)neighbors, batch_size, k); + auto distances_v = raft::make_device_matrix_view(distances, batch_size, k); + + raft::runtime::neighbors::refine(handle_, + dataset_, + queries_v, + candidates.view(), + neighbors_v, + distances_v, + index_->metric()); + } else { + auto queries_host = raft::make_host_matrix(batch_size, index_->dim()); + auto candidates_host = raft::make_host_matrix(batch_size, k0); + auto neighbors_host = raft::make_host_matrix(batch_size, k); + auto distances_host = raft::make_host_matrix(batch_size, k); + + raft::copy(queries_host.data_handle(), queries, queries_host.size(), handle_.get_stream()); + raft::copy(candidates_host.data_handle(), + candidates.data_handle(), + candidates_host.size(), + handle_.get_stream()); + + auto dataset_v = raft::make_host_matrix_view( + dataset_.data_handle(), batch_size, index_->dim()); + + raft::runtime::neighbors::refine(handle_, + dataset_v, + queries_host.view(), + candidates_host.view(), + neighbors_host.view(), + distances_host.view(), + index_->metric()); + + raft::copy(neighbors, + (size_t*)neighbors_host.data_handle(), + neighbors_host.size(), + handle_.get_stream()); + raft::copy( + distances, distances_host.data_handle(), distances_host.size(), handle_.get_stream()); + } + } else { + auto queries_v = + raft::make_device_matrix_view(queries, batch_size, index_->dim()); + auto neighbors_v = raft::make_device_matrix_view((IdxT*)neighbors, batch_size, k); + auto distances_v = raft::make_device_matrix_view(distances, batch_size, k); + + raft::runtime::neighbors::ivf_pq::search( + handle_, search_params_, *index_, queries_v, neighbors_v, distances_v); + } + handle_.sync_stream(); + return; +} +} // namespace raft::bench::ann diff --git a/cpp/bench/ann/src/raft/raft_wrapper.h b/cpp/bench/ann/src/raft/raft_wrapper.h new file mode 100644 index 0000000000..01f206ab70 --- /dev/null +++ b/cpp/bench/ann/src/raft/raft_wrapper.h @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023, NVIDIA CORPORATION. + * + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/ann_types.hpp" + +namespace raft_temp { + +inline raft::distance::DistanceType parse_metric_type(raft::bench::ann::Metric metric) +{ + if (metric == raft::bench::ann::Metric::kInnerProduct) { + return raft::distance::DistanceType::InnerProduct; + } else if (metric == raft::bench::ann::Metric::kEuclidean) { + return raft::distance::DistanceType::L2Expanded; + } else { + throw std::runtime_error("raft supports only metric type of inner product and L2"); + } +} + +} // namespace raft_temp + +namespace raft::bench::ann { + +// brute force fused L2 KNN - RAFT +template +class RaftGpu : public ANN { + public: + using typename ANN::AnnSearchParam; + + RaftGpu(Metric metric, int dim); + + void build(const T*, size_t, cudaStream_t) final; + + void set_search_param(const AnnSearchParam& param) override; + + // TODO: if the number of results is less than k, the remaining elements of 'neighbors' + // will be filled with (size_t)-1 + void search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream = 0) const final; + + // to enable dataset access from GPU memory + AlgoProperty get_property() const override + { + AlgoProperty property; + property.dataset_memory_type = MemoryType::Device; + property.query_memory_type = MemoryType::Device; + property.need_dataset_when_search = true; + return property; + } + void set_search_dataset(const T* dataset, size_t nrow) override; + void save(const std::string& file) const override; + void load(const std::string&) override { return; }; + + protected: + raft::distance::DistanceType metric_type_; + int device_; + const T* dataset_; + size_t nrow_; +}; + +template +RaftGpu::RaftGpu(Metric metric, int dim) + : ANN(metric, dim), metric_type_(raft_temp::parse_metric_type(metric)) +{ + static_assert(std::is_same_v, "raft support only float type"); + assert(metric_type_ == raft::distance::DistanceType::L2Expanded); + RAFT_CUDA_TRY(cudaGetDevice(&device_)); +} + +template +void RaftGpu::build(const T*, size_t, cudaStream_t) +{ + // as this is brute force algo so no index building required + return; +} + +template +void RaftGpu::set_search_param(const AnnSearchParam&) +{ + // Nothing to set here as it is brute force implementation +} + +template +void RaftGpu::set_search_dataset(const T* dataset, size_t nrow) +{ + dataset_ = dataset; + nrow_ = nrow; +} + +template +void RaftGpu::save(const std::string& file) const +{ + // create a empty index file as no index to store. + std::fstream fp; + fp.open(file.c_str(), std::ios::out); + if (!fp) { + printf("Error in creating file!!!\n"); + ; + return; + } + fp.close(); +} + +template +void RaftGpu::search(const T* queries, + int batch_size, + int k, + size_t* neighbors, + float* distances, + cudaStream_t stream) const +{ + raft::spatial::knn::detail::fusedL2Knn(this->dim_, + reinterpret_cast(neighbors), + distances, + dataset_, + queries, + nrow_, + static_cast(batch_size), + k, + true, + true, + stream, + metric_type_); +} + +} // namespace raft::bench::ann diff --git a/cpp/bench/prims/CMakeLists.txt b/cpp/bench/prims/CMakeLists.txt new file mode 100644 index 0000000000..f03a552c1d --- /dev/null +++ b/cpp/bench/prims/CMakeLists.txt @@ -0,0 +1,141 @@ +# ============================================================================= +# Copyright (c) 2022-2023, NVIDIA CORPORATION. +# +# 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. +# ============================================================================= + +# ################################################################################################## +# * compiler function ----------------------------------------------------------------------------- + +function(ConfigureBench) + + set(options OPTIONAL LIB) + set(oneValueArgs NAME) + set(multiValueArgs PATH TARGETS CONFIGURATIONS) + + cmake_parse_arguments(ConfigureBench "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(BENCH_NAME ${ConfigureBench_NAME}) + + add_executable(${BENCH_NAME} ${ConfigureBench_PATH}) + + target_link_libraries( + ${BENCH_NAME} + PRIVATE raft::raft + raft_internal + $<$:raft::compiled> + benchmark::benchmark + Threads::Threads + $ + $ + ) + + set_target_properties( + ${BENCH_NAME} + PROPERTIES # set target compile options + INSTALL_RPATH "\$ORIGIN/../../../lib" + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CUDA_STANDARD 17 + CUDA_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + INTERFACE_POSITION_INDEPENDENT_CODE ON + ) + + target_compile_options( + ${BENCH_NAME} PRIVATE "$<$:${RAFT_CXX_FLAGS}>" + "$<$:${RAFT_CUDA_FLAGS}>" + ) + + target_include_directories(${BENCH_NAME} PUBLIC "$") + + install( + TARGETS ${BENCH_NAME} + COMPONENT testing + DESTINATION bin/gbench/prims/libraft + EXCLUDE_FROM_ALL + ) + +endfunction() + +if(BUILD_BENCH) + ConfigureBench( + NAME CLUSTER_BENCH PATH bench/prims/cluster/kmeans_balanced.cu bench/prims/cluster/kmeans.cu + bench/prims/main.cpp OPTIONAL LIB + ) + + ConfigureBench( + NAME TUNE_DISTANCE PATH bench/prims/distance/tune_pairwise/kernel.cu + bench/prims/distance/tune_pairwise/bench.cu bench/prims/main.cpp + ) + + ConfigureBench( + NAME + DISTANCE_BENCH + PATH + bench/prims/distance/distance_cosine.cu + bench/prims/distance/distance_exp_l2.cu + bench/prims/distance/distance_l1.cu + bench/prims/distance/distance_unexp_l2.cu + bench/prims/distance/fused_l2_nn.cu + bench/prims/distance/masked_nn.cu + bench/prims/distance/kernels.cu + bench/prims/main.cpp + OPTIONAL + LIB + ) + + ConfigureBench( + NAME + LINALG_BENCH + PATH + bench/prims/linalg/add.cu + bench/prims/linalg/map_then_reduce.cu + bench/prims/linalg/matrix_vector_op.cu + bench/prims/linalg/norm.cu + bench/prims/linalg/normalize.cu + bench/prims/linalg/reduce_cols_by_key.cu + bench/prims/linalg/reduce_rows_by_key.cu + bench/prims/linalg/reduce.cu + bench/prims/main.cpp + ) + + ConfigureBench( + NAME MATRIX_BENCH PATH bench/prims/matrix/argmin.cu bench/prims/matrix/gather.cu + bench/prims/matrix/select_k.cu bench/prims/main.cpp OPTIONAL LIB + ) + + ConfigureBench( + NAME RANDOM_BENCH PATH bench/prims/random/make_blobs.cu bench/prims/random/permute.cu + bench/prims/random/rng.cu bench/prims/main.cpp + ) + + ConfigureBench(NAME SPARSE_BENCH PATH bench/prims/sparse/convert_csr.cu bench/prims/main.cpp) + + ConfigureBench( + NAME + NEIGHBORS_BENCH + PATH + bench/prims/neighbors/knn/brute_force_float_int64_t.cu + bench/prims/neighbors/knn/brute_force_float_uint32_t.cu + bench/prims/neighbors/knn/ivf_flat_float_int64_t.cu + bench/prims/neighbors/knn/ivf_flat_int8_t_int64_t.cu + bench/prims/neighbors/knn/ivf_flat_uint8_t_int64_t.cu + bench/prims/neighbors/knn/ivf_pq_float_int64_t.cu + bench/prims/neighbors/knn/ivf_pq_int8_t_int64_t.cu + bench/prims/neighbors/knn/ivf_pq_uint8_t_int64_t.cu + bench/prims/neighbors/refine_float_int64_t.cu + bench/prims/neighbors/refine_uint8_t_int64_t.cu + bench/prims/main.cpp + OPTIONAL + LIB + ) +endif() diff --git a/cpp/bench/cluster/kmeans.cu b/cpp/bench/prims/cluster/kmeans.cu similarity index 100% rename from cpp/bench/cluster/kmeans.cu rename to cpp/bench/prims/cluster/kmeans.cu diff --git a/cpp/bench/cluster/kmeans_balanced.cu b/cpp/bench/prims/cluster/kmeans_balanced.cu similarity index 100% rename from cpp/bench/cluster/kmeans_balanced.cu rename to cpp/bench/prims/cluster/kmeans_balanced.cu diff --git a/cpp/bench/common/benchmark.hpp b/cpp/bench/prims/common/benchmark.hpp similarity index 100% rename from cpp/bench/common/benchmark.hpp rename to cpp/bench/prims/common/benchmark.hpp diff --git a/cpp/bench/distance/distance_common.cuh b/cpp/bench/prims/distance/distance_common.cuh similarity index 100% rename from cpp/bench/distance/distance_common.cuh rename to cpp/bench/prims/distance/distance_common.cuh diff --git a/cpp/bench/distance/distance_cosine.cu b/cpp/bench/prims/distance/distance_cosine.cu similarity index 94% rename from cpp/bench/distance/distance_cosine.cu rename to cpp/bench/prims/distance/distance_cosine.cu index 20f29ce4ef..c8ac8067c8 100644 --- a/cpp/bench/distance/distance_cosine.cu +++ b/cpp/bench/prims/distance/distance_cosine.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/distance/distance_exp_l2.cu b/cpp/bench/prims/distance/distance_exp_l2.cu similarity index 94% rename from cpp/bench/distance/distance_exp_l2.cu rename to cpp/bench/prims/distance/distance_exp_l2.cu index 5a3af17193..52b7fff05c 100644 --- a/cpp/bench/distance/distance_exp_l2.cu +++ b/cpp/bench/prims/distance/distance_exp_l2.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/distance/distance_l1.cu b/cpp/bench/prims/distance/distance_l1.cu similarity index 93% rename from cpp/bench/distance/distance_l1.cu rename to cpp/bench/prims/distance/distance_l1.cu index 2ad7d5e957..e80db48ef0 100644 --- a/cpp/bench/distance/distance_l1.cu +++ b/cpp/bench/prims/distance/distance_l1.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/distance/distance_unexp_l2.cu b/cpp/bench/prims/distance/distance_unexp_l2.cu similarity index 94% rename from cpp/bench/distance/distance_unexp_l2.cu rename to cpp/bench/prims/distance/distance_unexp_l2.cu index 406aca2378..7ac1a8a4b5 100644 --- a/cpp/bench/distance/distance_unexp_l2.cu +++ b/cpp/bench/prims/distance/distance_unexp_l2.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/distance/fused_l2_nn.cu b/cpp/bench/prims/distance/fused_l2_nn.cu similarity index 100% rename from cpp/bench/distance/fused_l2_nn.cu rename to cpp/bench/prims/distance/fused_l2_nn.cu diff --git a/cpp/bench/distance/kernels.cu b/cpp/bench/prims/distance/kernels.cu similarity index 100% rename from cpp/bench/distance/kernels.cu rename to cpp/bench/prims/distance/kernels.cu diff --git a/cpp/bench/distance/masked_nn.cu b/cpp/bench/prims/distance/masked_nn.cu similarity index 100% rename from cpp/bench/distance/masked_nn.cu rename to cpp/bench/prims/distance/masked_nn.cu diff --git a/cpp/bench/distance/tune_pairwise/bench.cu b/cpp/bench/prims/distance/tune_pairwise/bench.cu similarity index 100% rename from cpp/bench/distance/tune_pairwise/bench.cu rename to cpp/bench/prims/distance/tune_pairwise/bench.cu diff --git a/cpp/bench/distance/tune_pairwise/kernel.cu b/cpp/bench/prims/distance/tune_pairwise/kernel.cu similarity index 100% rename from cpp/bench/distance/tune_pairwise/kernel.cu rename to cpp/bench/prims/distance/tune_pairwise/kernel.cu diff --git a/cpp/bench/distance/tune_pairwise/kernel.cuh b/cpp/bench/prims/distance/tune_pairwise/kernel.cuh similarity index 100% rename from cpp/bench/distance/tune_pairwise/kernel.cuh rename to cpp/bench/prims/distance/tune_pairwise/kernel.cuh diff --git a/cpp/bench/linalg/add.cu b/cpp/bench/prims/linalg/add.cu similarity index 96% rename from cpp/bench/linalg/add.cu rename to cpp/bench/prims/linalg/add.cu index 7d00b8cbae..456214ad7b 100644 --- a/cpp/bench/linalg/add.cu +++ b/cpp/bench/prims/linalg/add.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/linalg/map_then_reduce.cu b/cpp/bench/prims/linalg/map_then_reduce.cu similarity index 97% rename from cpp/bench/linalg/map_then_reduce.cu rename to cpp/bench/prims/linalg/map_then_reduce.cu index 33a3e66264..84aebd85bf 100644 --- a/cpp/bench/linalg/map_then_reduce.cu +++ b/cpp/bench/prims/linalg/map_then_reduce.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/linalg/matrix_vector_op.cu b/cpp/bench/prims/linalg/matrix_vector_op.cu similarity index 99% rename from cpp/bench/linalg/matrix_vector_op.cu rename to cpp/bench/prims/linalg/matrix_vector_op.cu index aa388955da..d1fbaee79b 100644 --- a/cpp/bench/linalg/matrix_vector_op.cu +++ b/cpp/bench/prims/linalg/matrix_vector_op.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/linalg/norm.cu b/cpp/bench/prims/linalg/norm.cu similarity index 98% rename from cpp/bench/linalg/norm.cu rename to cpp/bench/prims/linalg/norm.cu index efecee88c9..f83953f8e4 100644 --- a/cpp/bench/linalg/norm.cu +++ b/cpp/bench/prims/linalg/norm.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/linalg/normalize.cu b/cpp/bench/prims/linalg/normalize.cu similarity index 98% rename from cpp/bench/linalg/normalize.cu rename to cpp/bench/prims/linalg/normalize.cu index d01473ffeb..ad9052a008 100644 --- a/cpp/bench/linalg/normalize.cu +++ b/cpp/bench/prims/linalg/normalize.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/linalg/reduce.cu b/cpp/bench/prims/linalg/reduce.cu similarity index 97% rename from cpp/bench/linalg/reduce.cu rename to cpp/bench/prims/linalg/reduce.cu index 015e0b8abe..cf41c5916a 100644 --- a/cpp/bench/linalg/reduce.cu +++ b/cpp/bench/prims/linalg/reduce.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/linalg/reduce_cols_by_key.cu b/cpp/bench/prims/linalg/reduce_cols_by_key.cu similarity index 98% rename from cpp/bench/linalg/reduce_cols_by_key.cu rename to cpp/bench/prims/linalg/reduce_cols_by_key.cu index 43aeb69ab0..ac0c612ee4 100644 --- a/cpp/bench/linalg/reduce_cols_by_key.cu +++ b/cpp/bench/prims/linalg/reduce_cols_by_key.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/linalg/reduce_rows_by_key.cu b/cpp/bench/prims/linalg/reduce_rows_by_key.cu similarity index 98% rename from cpp/bench/linalg/reduce_rows_by_key.cu rename to cpp/bench/prims/linalg/reduce_rows_by_key.cu index 075bc7c8c4..aa9c9a1f62 100644 --- a/cpp/bench/linalg/reduce_rows_by_key.cu +++ b/cpp/bench/prims/linalg/reduce_rows_by_key.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/main.cpp b/cpp/bench/prims/main.cpp similarity index 92% rename from cpp/bench/main.cpp rename to cpp/bench/prims/main.cpp index 3162422e8e..40f539facf 100644 --- a/cpp/bench/main.cpp +++ b/cpp/bench/prims/main.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/matrix/argmin.cu b/cpp/bench/prims/matrix/argmin.cu similarity index 100% rename from cpp/bench/matrix/argmin.cu rename to cpp/bench/prims/matrix/argmin.cu diff --git a/cpp/bench/matrix/gather.cu b/cpp/bench/prims/matrix/gather.cu similarity index 100% rename from cpp/bench/matrix/gather.cu rename to cpp/bench/prims/matrix/gather.cu diff --git a/cpp/bench/matrix/select_k.cu b/cpp/bench/prims/matrix/select_k.cu similarity index 100% rename from cpp/bench/matrix/select_k.cu rename to cpp/bench/prims/matrix/select_k.cu diff --git a/cpp/bench/neighbors/knn.cuh b/cpp/bench/prims/neighbors/knn.cuh similarity index 100% rename from cpp/bench/neighbors/knn.cuh rename to cpp/bench/prims/neighbors/knn.cuh diff --git a/cpp/bench/neighbors/knn/brute_force_float_int64_t.cu b/cpp/bench/prims/neighbors/knn/brute_force_float_int64_t.cu similarity index 93% rename from cpp/bench/neighbors/knn/brute_force_float_int64_t.cu rename to cpp/bench/prims/neighbors/knn/brute_force_float_int64_t.cu index d981104e20..7df0599670 100644 --- a/cpp/bench/neighbors/knn/brute_force_float_int64_t.cu +++ b/cpp/bench/prims/neighbors/knn/brute_force_float_int64_t.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/neighbors/knn/brute_force_float_uint32_t.cu b/cpp/bench/prims/neighbors/knn/brute_force_float_uint32_t.cu similarity index 93% rename from cpp/bench/neighbors/knn/brute_force_float_uint32_t.cu rename to cpp/bench/prims/neighbors/knn/brute_force_float_uint32_t.cu index 60f7edae96..9704d39e76 100644 --- a/cpp/bench/neighbors/knn/brute_force_float_uint32_t.cu +++ b/cpp/bench/prims/neighbors/knn/brute_force_float_uint32_t.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/neighbors/knn/ivf_flat_float_int64_t.cu b/cpp/bench/prims/neighbors/knn/ivf_flat_float_int64_t.cu similarity index 93% rename from cpp/bench/neighbors/knn/ivf_flat_float_int64_t.cu rename to cpp/bench/prims/neighbors/knn/ivf_flat_float_int64_t.cu index 594d4d16d2..fbbb4f9acc 100644 --- a/cpp/bench/neighbors/knn/ivf_flat_float_int64_t.cu +++ b/cpp/bench/prims/neighbors/knn/ivf_flat_float_int64_t.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/neighbors/knn/ivf_flat_int8_t_int64_t.cu b/cpp/bench/prims/neighbors/knn/ivf_flat_int8_t_int64_t.cu similarity index 93% rename from cpp/bench/neighbors/knn/ivf_flat_int8_t_int64_t.cu rename to cpp/bench/prims/neighbors/knn/ivf_flat_int8_t_int64_t.cu index bd268f036c..7067dbe1b6 100644 --- a/cpp/bench/neighbors/knn/ivf_flat_int8_t_int64_t.cu +++ b/cpp/bench/prims/neighbors/knn/ivf_flat_int8_t_int64_t.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/neighbors/knn/ivf_flat_uint8_t_int64_t.cu b/cpp/bench/prims/neighbors/knn/ivf_flat_uint8_t_int64_t.cu similarity index 100% rename from cpp/bench/neighbors/knn/ivf_flat_uint8_t_int64_t.cu rename to cpp/bench/prims/neighbors/knn/ivf_flat_uint8_t_int64_t.cu diff --git a/cpp/bench/neighbors/knn/ivf_pq_float_int64_t.cu b/cpp/bench/prims/neighbors/knn/ivf_pq_float_int64_t.cu similarity index 100% rename from cpp/bench/neighbors/knn/ivf_pq_float_int64_t.cu rename to cpp/bench/prims/neighbors/knn/ivf_pq_float_int64_t.cu diff --git a/cpp/bench/neighbors/knn/ivf_pq_int8_t_int64_t.cu b/cpp/bench/prims/neighbors/knn/ivf_pq_int8_t_int64_t.cu similarity index 100% rename from cpp/bench/neighbors/knn/ivf_pq_int8_t_int64_t.cu rename to cpp/bench/prims/neighbors/knn/ivf_pq_int8_t_int64_t.cu diff --git a/cpp/bench/neighbors/knn/ivf_pq_uint8_t_int64_t.cu b/cpp/bench/prims/neighbors/knn/ivf_pq_uint8_t_int64_t.cu similarity index 100% rename from cpp/bench/neighbors/knn/ivf_pq_uint8_t_int64_t.cu rename to cpp/bench/prims/neighbors/knn/ivf_pq_uint8_t_int64_t.cu diff --git a/cpp/bench/neighbors/refine.cuh b/cpp/bench/prims/neighbors/refine.cuh similarity index 100% rename from cpp/bench/neighbors/refine.cuh rename to cpp/bench/prims/neighbors/refine.cuh diff --git a/cpp/bench/neighbors/refine_float_int64_t.cu b/cpp/bench/prims/neighbors/refine_float_int64_t.cu similarity index 100% rename from cpp/bench/neighbors/refine_float_int64_t.cu rename to cpp/bench/prims/neighbors/refine_float_int64_t.cu diff --git a/cpp/bench/neighbors/refine_uint8_t_int64_t.cu b/cpp/bench/prims/neighbors/refine_uint8_t_int64_t.cu similarity index 100% rename from cpp/bench/neighbors/refine_uint8_t_int64_t.cu rename to cpp/bench/prims/neighbors/refine_uint8_t_int64_t.cu diff --git a/cpp/bench/random/make_blobs.cu b/cpp/bench/prims/random/make_blobs.cu similarity index 98% rename from cpp/bench/random/make_blobs.cu rename to cpp/bench/prims/random/make_blobs.cu index 950d80c499..f43d914cf2 100644 --- a/cpp/bench/random/make_blobs.cu +++ b/cpp/bench/prims/random/make_blobs.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/random/permute.cu b/cpp/bench/prims/random/permute.cu similarity index 100% rename from cpp/bench/random/permute.cu rename to cpp/bench/prims/random/permute.cu diff --git a/cpp/bench/random/rng.cu b/cpp/bench/prims/random/rng.cu similarity index 98% rename from cpp/bench/random/rng.cu rename to cpp/bench/prims/random/rng.cu index 147adf26ae..d15c9441d7 100644 --- a/cpp/bench/random/rng.cu +++ b/cpp/bench/prims/random/rng.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, NVIDIA CORPORATION. + * Copyright (c) 2022-2023, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/bench/sparse/convert_csr.cu b/cpp/bench/prims/sparse/convert_csr.cu similarity index 100% rename from cpp/bench/sparse/convert_csr.cu rename to cpp/bench/prims/sparse/convert_csr.cu diff --git a/cpp/cmake/modules/FindAVX.cmake b/cpp/cmake/modules/FindAVX.cmake new file mode 100644 index 0000000000..7f3b2dfc76 --- /dev/null +++ b/cpp/cmake/modules/FindAVX.cmake @@ -0,0 +1,110 @@ +# ============================================================================= +# Copyright (c) 2016- Facebook, Inc (Adam Paszke) +# Copyright (c) 2014- Facebook, Inc (Soumith Chintala) +# Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert) +# Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu) +# Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu) +# Copyright (c) 2011-2013 NYU (Clement Farabet) +# Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston) +# Copyright (c) 2006 Idiap Research Institute (Samy Bengio) +# Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz) +# +# Note: This file was copied from PyTorch and modified for use in the RAFT library. +# Refer to thirdparty/LICENSES/LICENSE.pytorch for license and additional +# copyright information. +# ============================================================================= + +INCLUDE(CheckCXXSourceRuns) + +SET(AVX_CODE + " + #include + + int main() + { + __m256 a; + a = _mm256_set1_ps(0); + return 0; + } +" +) + +SET(AVX512_CODE + " + #include + + int main() + { + __m512i a = _mm512_set_epi8(0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0); + __m512i b = a; + __mmask64 equality_mask = _mm512_cmp_epi8_mask(a, b, _MM_CMPINT_EQ); + return 0; + } +" +) + +SET(AVX2_CODE + " + #include + + int main() + { + __m256i a = {0}; + a = _mm256_abs_epi16(a); + __m256i x; + _mm256_extract_epi64(x, 0); // we rely on this in our AVX2 code + return 0; + } +" +) + +MACRO(CHECK_SSE lang type flags) + SET(__FLAG_I 1) + SET(CMAKE_REQUIRED_FLAGS_SAVE ${CMAKE_REQUIRED_FLAGS}) + FOREACH(__FLAG ${flags}) + IF(NOT ${lang}_${type}_FOUND) + SET(CMAKE_REQUIRED_FLAGS ${__FLAG}) + CHECK_CXX_SOURCE_RUNS("${${type}_CODE}" ${lang}_HAS_${type}_${__FLAG_I}) + IF(${lang}_HAS_${type}_${__FLAG_I}) + SET(${lang}_${type}_FOUND + TRUE + CACHE BOOL "${lang} ${type} support" + ) + SET(${lang}_${type}_FLAGS + "${__FLAG}" + CACHE STRING "${lang} ${type} flags" + ) + ENDIF() + MATH(EXPR __FLAG_I "${__FLAG_I}+1") + ENDIF() + ENDFOREACH() + SET(CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS_SAVE}) + + IF(NOT ${lang}_${type}_FOUND) + SET(${lang}_${type}_FOUND + FALSE + CACHE BOOL "${lang} ${type} support" + ) + SET(${lang}_${type}_FLAGS + "" + CACHE STRING "${lang} ${type} flags" + ) + ENDIF() + + MARK_AS_ADVANCED(${lang}_${type}_FOUND ${lang}_${type}_FLAGS) + +ENDMACRO() + +# CHECK_SSE(C "AVX" " ;-mavx;/arch:AVX") CHECK_SSE(C "AVX2" " ;-mavx2 -mfma;/arch:AVX2") CHECK_SSE(C +# "AVX512" " ;-mavx512f -mavx512dq -mavx512vl -mavx512bw -mfma;/arch:AVX512") +# +CHECK_SSE(CXX "AVX" " ;-mavx;/arch:AVX") +CHECK_SSE(CXX "AVX2" " ;-mavx2 -mfma;/arch:AVX2") +CHECK_SSE(CXX "AVX512" " ;-mavx512f -mavx512dq -mavx512vl -mavx512bw -mfma;/arch:AVX512") diff --git a/cpp/cmake/patches/ggnn.patch b/cpp/cmake/patches/ggnn.patch new file mode 100644 index 0000000000..95e1aaff4b --- /dev/null +++ b/cpp/cmake/patches/ggnn.patch @@ -0,0 +1,206 @@ +diff --git a/include/ggnn/cuda_knn_ggnn_gpu_instance.cuh b/include/ggnn/cuda_knn_ggnn_gpu_instance.cuh +index 8cbaf0d..6eb72ac 100644 +--- a/include/ggnn/cuda_knn_ggnn_gpu_instance.cuh ++++ b/include/ggnn/cuda_knn_ggnn_gpu_instance.cuh +@@ -41,7 +41,6 @@ limitations under the License. + #include "ggnn/sym/cuda_knn_sym_query_layer.cuh" + #include "ggnn/utils/cuda_knn_utils.cuh" + #include "ggnn/utils/cuda_knn_constants.cuh" +-#include "ggnn/utils/cuda_knn_dataset.cuh" + + template + __global__ void divide(ValueT* res, ValueT* input, ValueT N) { +@@ -98,9 +97,7 @@ struct GGNNGPUInstance { + typedef GGNNGraphDevice GGNNGraphDevice; + typedef GGNNGraphHost GGNNGraphHost; + +- const Dataset* dataset; + GGNNGraphBuffer* ggnn_buffer {nullptr}; +- GGNNQuery ggnn_query; + + // Graph Shards resident on the GPU + std::vector ggnn_shards; +@@ -117,13 +114,12 @@ struct GGNNGPUInstance { + // number of shards that need to be processed by this instance + const int num_parts; + +- GGNNGPUInstance(const int gpu_id, const Dataset* dataset, ++ GGNNGPUInstance(const int gpu_id, + const int N_shard, const int L, + const bool enable_construction, const float tau_build, + const int num_parts=1, const int num_cpu_buffers=1) : + N_shard{N_shard}, L{L}, tau_build{tau_build}, +- dataset{dataset}, gpu_id{gpu_id}, +- ggnn_query{dataset->N_query, D, KQuery, num_parts}, ++ gpu_id{gpu_id}, + num_parts{num_parts} + { + CHECK_LE(L, MAX_LAYER); +@@ -135,7 +131,6 @@ struct GGNNGPUInstance { + CHECK_EQ(current_gpu_id, gpu_id) << "cudaSetDevice() needs to be called in advance!"; + } + +- ggnn_query.loadQueriesAsync(dataset->h_query, 0); + + computeGraphParameters(); + +@@ -186,7 +181,7 @@ struct GGNNGPUInstance { + } + + GGNNGPUInstance(const GGNNGPUInstance& other) +- : dataset{nullptr}, ggnn_query{0, D, KQuery}, ++ : + gpu_id{0}, N_shard{0}, num_parts{0} { + // this exists to allow using vector::emplace_back + // when it triggers a reallocation, this code will be called. +@@ -305,6 +300,7 @@ struct GGNNGPUInstance { + + // io + ++ /* + void waitForDiskIO(const int shard_id) { + auto& cpu_buffer = ggnn_cpu_buffers[shard_id%ggnn_cpu_buffers.size()]; + if (cpu_buffer.disk_io_thread.joinable()) +@@ -468,11 +464,12 @@ struct GGNNGPUInstance { + CHECK_CUDA(cudaDeviceSynchronize()); + CHECK_CUDA(cudaPeekAtLastError()); + } ++ */ + + // graph operations + + template +- void queryLayer(const int shard_id = 0) const { ++ void queryLayer(const BaseT* d_query, int batch_size, KeyT* d_query_result_ids, ValueT* d_query_result_dists, const int shard_id = 0) const { + CHECK_CUDA(cudaSetDevice(gpu_id)); + const auto& shard = ggnn_shards.at(shard_id%ggnn_shards.size()); + +@@ -482,21 +479,21 @@ struct GGNNGPUInstance { + + int* m_dist_statistics = nullptr; + if (DIST_STATS) +- cudaMallocManaged(&m_dist_statistics, dataset->N_query * sizeof(int)); ++ cudaMallocManaged(&m_dist_statistics, batch_size * sizeof(int)); + + QueryKernel query_kernel; + query_kernel.d_base = shard.d_base; +- query_kernel.d_query = ggnn_query.d_query; ++ query_kernel.d_query = d_query; + + query_kernel.d_graph = shard.d_graph; +- query_kernel.d_query_results = ggnn_query.d_query_result_ids; +- query_kernel.d_query_results_dists = ggnn_query.d_query_result_dists; ++ query_kernel.d_query_results = d_query_result_ids; ++ query_kernel.d_query_results_dists = d_query_result_dists; + + query_kernel.d_translation = shard.d_translation; + + query_kernel.d_nn1_stats = shard.d_nn1_stats; + +- query_kernel.N = dataset->N_query; ++ query_kernel.N = batch_size; + query_kernel.N_offset = 0; + + query_kernel.d_dist_stats = m_dist_statistics; +@@ -771,6 +768,16 @@ struct GGNNGPUInstance { + sym(layer, shard_id); + } + } ++ ++ void set_stream(cudaStream_t stream) { ++ assert(ggnn_shards.size() == 1); ++ ggnn_shards.at(0).stream = stream; ++ } ++ ++ void set_base_data(const BaseT* dataset) { ++ assert(ggnn_shards.size() == 1); ++ ggnn_shards.at(0).d_base = dataset; ++ } + }; + + #endif // INCLUDE_GGNN_CUDA_KNN_GGNN_GPU_INSTANCE_CUH_ +diff --git a/include/ggnn/graph/cuda_knn_ggnn_graph_device.cuh b/include/ggnn/graph/cuda_knn_ggnn_graph_device.cuh +index c94a8f1..781226d 100644 +--- a/include/ggnn/graph/cuda_knn_ggnn_graph_device.cuh ++++ b/include/ggnn/graph/cuda_knn_ggnn_graph_device.cuh +@@ -50,7 +50,7 @@ struct GGNNGraphDevice { + ValueT* d_nn1_stats; + + /// base data pointer for the shard. +- BaseT* d_base; ++ const BaseT* d_base; + + /// combined memory pool + char* d_memory; +@@ -69,7 +69,9 @@ struct GGNNGraphDevice { + const size_t selection_translation_size = align8(ST_all * sizeof(KeyT)); + const size_t nn1_stats_size = align8(2 * sizeof(ValueT)); + total_graph_size = graph_size + 2 * selection_translation_size + nn1_stats_size; +- base_size = align8(static_cast(N) * D * sizeof(BaseT)); ++ // base_size = align8(static_cast(N) * D * sizeof(BaseT)); ++ (void) N; ++ (void) D; + + const size_t total_size = base_size+total_graph_size; + +@@ -86,8 +88,7 @@ struct GGNNGraphDevice { + CHECK_CUDA(cudaMalloc(&d_memory, total_size)); + + size_t pos = 0; +- d_base = reinterpret_cast(d_memory+pos); +- pos += base_size; ++ d_base = nullptr; + d_graph = reinterpret_cast(d_memory+pos); + pos += graph_size; + d_translation = reinterpret_cast(d_memory+pos); +@@ -99,14 +100,14 @@ struct GGNNGraphDevice { + + CHECK_EQ(pos, total_size); + +- CHECK_CUDA(cudaStreamCreate(&stream)); ++ // CHECK_CUDA(cudaStreamCreate(&stream)); + + CHECK_CUDA(cudaPeekAtLastError()); + CHECK_CUDA(cudaDeviceSynchronize()); + CHECK_CUDA(cudaPeekAtLastError()); + } + +- GGNNGraphDevice(const GGNNGraphDevice& other) { ++ GGNNGraphDevice(const GGNNGraphDevice&) { + // this exists to allow using vector::emplace_back + // when it triggers a reallocation, this code will be called. + // always make sure that enough memory is reserved ahead of time. +@@ -116,7 +117,7 @@ struct GGNNGraphDevice { + ~GGNNGraphDevice() { + cudaFree(d_memory); + +- CHECK_CUDA(cudaStreamDestroy(stream)); ++ // CHECK_CUDA(cudaStreamDestroy(stream)); + } + }; + +diff --git a/include/ggnn/graph/cuda_knn_ggnn_graph_host.cuh b/include/ggnn/graph/cuda_knn_ggnn_graph_host.cuh +index 2055f9e..ef5843a 100644 +--- a/include/ggnn/graph/cuda_knn_ggnn_graph_host.cuh ++++ b/include/ggnn/graph/cuda_knn_ggnn_graph_host.cuh +@@ -92,7 +92,7 @@ struct GGNNGraphHost { + CHECK_CUDA(cudaPeekAtLastError()); + } + +- GGNNGraphHost(const GGNNGraphHost& other) { ++ GGNNGraphHost(const GGNNGraphHost&) { + // this exists to allow using vector::emplace_back + // when it triggers a reallocation, this code will be called. + // always make sure that enough memory is reserved ahead of time. +diff --git a/include/ggnn/select/cuda_knn_wrs_select_layer.cuh b/include/ggnn/select/cuda_knn_wrs_select_layer.cuh +index 49d76a1..eef69e6 100644 +--- a/include/ggnn/select/cuda_knn_wrs_select_layer.cuh ++++ b/include/ggnn/select/cuda_knn_wrs_select_layer.cuh +@@ -22,7 +22,6 @@ limitations under the License. + #include + #include + +-#include + #include + + #include "ggnn/utils/cuda_knn_constants.cuh" diff --git a/cpp/cmake/patches/nlohmann_json.patch b/cpp/cmake/patches/nlohmann_json.patch new file mode 100644 index 0000000000..83dd56bc16 --- /dev/null +++ b/cpp/cmake/patches/nlohmann_json.patch @@ -0,0 +1,38 @@ +--- nlohmann/json.hpp 2021-05-06 11:40:39.770669693 +0800 ++++ nlohmann/json_patched.hpp 2021-06-02 18:46:43.849334466 +0800 +@@ -16607,6 +16607,21 @@ + } + } + ++ ++ template ::value, int> = 0> ++ bool is_negative_number(NumberType x) ++ { ++ return x < 0; ++ } ++ ++ template < typename NumberType, ++ enable_if_t < std::is_unsigned::value, int > = 0 > ++ bool is_negative_number(NumberType /*unused*/) ++ { ++ return false; ++ } ++ + /*! + @brief dump an integer + +@@ -16649,12 +16664,11 @@ + // use a pointer to fill the buffer + auto buffer_ptr = number_buffer.begin(); // NOLINT(llvm-qualified-auto,readability-qualified-auto,cppcoreguidelines-pro-type-vararg,hicpp-vararg) + +- const bool is_negative = std::is_same::value && !(x >= 0); // see issue #755 + number_unsigned_t abs_value; + + unsigned int n_chars{}; + +- if (is_negative) ++ if (is_negative_number(x)) + { + *buffer_ptr = '-'; + abs_value = remove_sign(static_cast(x)); diff --git a/cpp/cmake/thirdparty/get_faiss.cmake b/cpp/cmake/thirdparty/get_faiss.cmake new file mode 100644 index 0000000000..b7c132f2f1 --- /dev/null +++ b/cpp/cmake/thirdparty/get_faiss.cmake @@ -0,0 +1,87 @@ +#============================================================================= +# Copyright (c) 2021-2023, NVIDIA CORPORATION. +# +# 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. +#============================================================================= + +function(find_and_configure_faiss) + set(oneValueArgs VERSION REPOSITORY PINNED_TAG BUILD_STATIC_LIBS EXCLUDE_FROM_ALL) + cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" + "${multiValueArgs}" ${ARGN} ) + + rapids_find_generate_module(faiss + HEADER_NAMES faiss/IndexFlat.h + LIBRARY_NAMES faiss + ) + + set(BUILD_SHARED_LIBS ON) + if (PKG_BUILD_STATIC_LIBS) + set(BUILD_SHARED_LIBS OFF) + set(CPM_DOWNLOAD_faiss ON) + endif() + + rapids_cpm_find(faiss ${PKG_VERSION} + GLOBAL_TARGETS faiss::faiss + CPM_ARGS + GIT_REPOSITORY ${PKG_REPOSITORY} + GIT_TAG ${PKG_PINNED_TAG} + EXCLUDE_FROM_ALL ${PKG_EXCLUDE_FROM_ALL} + OPTIONS + "FAISS_ENABLE_PYTHON OFF" + "CUDAToolkit_ROOT ${CUDAToolkit_LIBRARY_DIR}" + "FAISS_ENABLE_GPU ON" + "BUILD_TESTING OFF" + "CMAKE_MESSAGE_LOG_LEVEL VERBOSE" + "FAISS_USE_CUDA_TOOLKIT_STATIC ${CUDA_STATIC_RUNTIME}" + ) + + if(TARGET faiss AND NOT TARGET faiss::faiss) + add_library(faiss::faiss ALIAS faiss) + endif() + + if(faiss_ADDED) + rapids_export(BUILD faiss + EXPORT_SET faiss-targets + GLOBAL_TARGETS faiss + NAMESPACE faiss::) + endif() + + # We generate the faiss-config files when we built faiss locally, so always do `find_dependency` + rapids_export_package(BUILD OpenMP raft-ann-bench-exports) # faiss uses openMP but doesn't export a need for it + rapids_export_package(BUILD faiss raft-ann-bench-exports GLOBAL_TARGETS faiss::faiss faiss) + rapids_export_package(INSTALL faiss raft-ann-bench-exports GLOBAL_TARGETS faiss::faiss faiss) + + # Tell cmake where it can find the generated faiss-config.cmake we wrote. + include("${rapids-cmake-dir}/export/find_package_root.cmake") + rapids_export_find_package_root(BUILD faiss [=[${CMAKE_CURRENT_LIST_DIR}]=] raft-ann-bench-exports) +endfunction() + +if(NOT RAFT_FAISS_GIT_TAG) + # TODO: Remove this once faiss supports FAISS_USE_CUDA_TOOLKIT_STATIC + # (https://github.com/facebookresearch/faiss/pull/2446) + set(RAFT_FAISS_GIT_TAG fea/statically-link-ctk-v1.7.0) + # set(RAFT_FAISS_GIT_TAG bde7c0027191f29c9dadafe4f6e68ca0ee31fb30) +endif() + +if(NOT RAFT_FAISS_GIT_REPOSITORY) + # TODO: Remove this once faiss supports FAISS_USE_CUDA_TOOLKIT_STATIC + # (https://github.com/facebookresearch/faiss/pull/2446) + set(RAFT_FAISS_GIT_REPOSITORY https://github.com/trxcllnt/faiss.git) + # set(RAFT_FAISS_GIT_REPOSITORY https://github.com/facebookresearch/faiss.git) +endif() + +find_and_configure_faiss(VERSION 1.7.0 + REPOSITORY ${RAFT_FAISS_GIT_REPOSITORY} + PINNED_TAG ${RAFT_FAISS_GIT_TAG} + BUILD_STATIC_LIBS ${RAFT_USE_FAISS_STATIC} + EXCLUDE_FROM_ALL ${RAFT_EXCLUDE_FAISS_FROM_ALL}) \ No newline at end of file diff --git a/cpp/cmake/thirdparty/get_ggnn.cmake b/cpp/cmake/thirdparty/get_ggnn.cmake new file mode 100644 index 0000000000..708acb6b8d --- /dev/null +++ b/cpp/cmake/thirdparty/get_ggnn.cmake @@ -0,0 +1,44 @@ +#============================================================================= +# Copyright (c) 2023, NVIDIA CORPORATION. +# +# 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. +#============================================================================= + +function(find_and_configure_ggnn) + set(oneValueArgs VERSION FORK PINNED_TAG EXCLUDE_FROM_ALL) + cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" + "${multiValueArgs}" ${ARGN} ) + + set ( EXTERNAL_INCLUDES_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/ ) + if (NOT EXISTS ${EXTERNAL_INCLUDES_DIRECTORY}/_deps/ggnn-src/) + + execute_process ( + COMMAND git clone "https://github.com/${PKG_FORK}/ggnn" --branch ${PKG_PINNED_TAG} ggnn-src + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/_deps/ ) + + message("SOURCE ${CMAKE_CURRENT_SOURCE_DIR}") + execute_process ( + COMMAND git apply ${CMAKE_CURRENT_SOURCE_DIR}/cmake/patches/ggnn.patch + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/_deps/ggnn-src + ) + endif() + +endfunction() + +# Change pinned tag here to test a commit in CI +# To use a different RAFT locally, set the CMake variable +# CPM_raft_SOURCE=/path/to/local/raft +find_and_configure_ggnn(VERSION 0.5 + FORK cgtuebingen + PINNED_TAG release_0.5 + EXCLUDE_FROM_ALL YES) diff --git a/cpp/cmake/thirdparty/get_glog.cmake b/cpp/cmake/thirdparty/get_glog.cmake new file mode 100644 index 0000000000..9334224de5 --- /dev/null +++ b/cpp/cmake/thirdparty/get_glog.cmake @@ -0,0 +1,49 @@ +#============================================================================= +# Copyright (c) 2021-2023, NVIDIA CORPORATION. +# +# 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. +#============================================================================= + +function(find_and_configure_glog) + set(oneValueArgs VERSION FORK PINNED_TAG EXCLUDE_FROM_ALL) + cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" + "${multiValueArgs}" ${ARGN} ) + + rapids_cpm_find(glog ${PKG_VERSION} + GLOBAL_TARGETS glog::glog + BUILD_EXPORT_SET raft-exports + INSTALL_EXPORT_SET raft-exports + CPM_ARGS + GIT_REPOSITORY https://github.com/${PKG_FORK}/glog.git + GIT_TAG ${PKG_PINNED_TAG} + SOURCE_SUBDIR cpp + EXCLUDE_FROM_ALL ${PKG_EXCLUDE_FROM_ALL} + ) + + if(glog_ADDED) + message(VERBOSE "RAFT: Using glog located in ${glog_SOURCE_DIR}") + else() + message(VERBOSE "RAFT: Using glog located in ${glog_DIR}") + endif() + + +endfunction() + +# Change pinned tag here to test a commit in CI +# To use a different RAFT locally, set the CMake variable +# CPM_glog_SOURCE=/path/to/local/glog +find_and_configure_glog(VERSION 0.6.0 + FORK google + PINNED_TAG v0.6.0 + EXCLUDE_FROM_ALL ON + ) \ No newline at end of file diff --git a/cpp/cmake/thirdparty/get_hnswlib.cmake b/cpp/cmake/thirdparty/get_hnswlib.cmake new file mode 100644 index 0000000000..94033e8333 --- /dev/null +++ b/cpp/cmake/thirdparty/get_hnswlib.cmake @@ -0,0 +1,49 @@ +#============================================================================= +# Copyright (c) 2023, NVIDIA CORPORATION. +# +# 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. +#============================================================================= + +function(find_and_configure_hnswlib) + set(oneValueArgs VERSION FORK PINNED_TAG EXCLUDE_FROM_ALL) + cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" + "${multiValueArgs}" ${ARGN} ) + + set ( EXTERNAL_INCLUDES_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) + if( NOT EXISTS ${EXTERNAL_INCLUDES_DIRECTORY}/_deps/hnswlib-src ) + + execute_process ( + COMMAND git clone --branch=v0.6.2 https://github.com/nmslib/hnswlib.git hnswlib-src + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/_deps ) + + endif () + + include(cmake/modules/FindAVX.cmake) + + set(HNSW_CXX_FLAGS "") + if(CXX_AVX_FOUND) + set(HNSW_CXX_FLAGS "${HNSW_CXX_FLAGS} ${CXX_AVX_FLAGS}") + elseif(CXX_AVX2_FOUND) + set(HNSW_CXX_FLAGS "${HNSW_CXX_FLAGS} ${CXX_AVX2_FLAGS}") + elseif(CXX_AVX512_FOUND) + set(HNSW_CXX_FLAGS "${HNSW_CXX_FLAGS} ${CXX_AVX512_FLAGS}") + endif() +endfunction() + +# Change pinned tag here to test a commit in CI +# To use a different RAFT locally, set the CMake variable +# CPM_raft_SOURCE=/path/to/local/raft +find_and_configure_hnswlib(VERSION 0.6.2 + FORK nmslib + PINNED_TAG v0.6.2 + EXCLUDE_FROM_ALL YES) diff --git a/cpp/cmake/thirdparty/get_nlohmann_json.cmake b/cpp/cmake/thirdparty/get_nlohmann_json.cmake new file mode 100644 index 0000000000..5de98a47ce --- /dev/null +++ b/cpp/cmake/thirdparty/get_nlohmann_json.cmake @@ -0,0 +1,39 @@ +#============================================================================= +# Copyright (c) 2023, NVIDIA CORPORATION. +# +# 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. +#============================================================================= + +function(find_and_configure_nlohmann_json) + set(oneValueArgs VERSION FORK PINNED_TAG EXCLUDE_FROM_ALL) + cmake_parse_arguments(PKG "${options}" "${oneValueArgs}" + "${multiValueArgs}" ${ARGN} ) + + rapids_cpm_find(nlohmann_json ${PKG_VERSION} + GLOBAL_TARGETS nlohmann_json::nlohmann_json + BUILD_EXPORT_SET raft-bench-ann-exports + INSTALL_EXPORT_SET raft-bench-ann-exports + CPM_ARGS + GIT_REPOSITORY https://github.com/${PKG_FORK}/json.git + GIT_TAG ${PKG_PINNED_TAG} + EXCLUDE_FROM_ALL ${PKG_EXCLUDE_FROM_ALL}) + +endfunction() + +# Change pinned tag here to test a commit in CI +# To use a different RAFT locally, set the CMake variable +# CPM_raft_SOURCE=/path/to/local/raft +find_and_configure_nlohmann_json(VERSION 3.11.2 + FORK nlohmann + PINNED_TAG v3.11.2 + EXCLUDE_FROM_ALL YES) diff --git a/cpp/include/raft/cluster/detail/kmeans_deprecated.cuh b/cpp/include/raft/cluster/detail/kmeans_deprecated.cuh index a9d8777304..bb1d122a24 100644 --- a/cpp/include/raft/cluster/detail/kmeans_deprecated.cuh +++ b/cpp/include/raft/cluster/detail/kmeans_deprecated.cuh @@ -383,8 +383,8 @@ static int chooseNewCentroid(raft::device_resources const& handle, thrust::device_pointer_cast(dists), thrust::device_pointer_cast(dists + n), thrust::device_pointer_cast(distsCumSum)); - CHECK_CUDA(stream); - CUDA_TRY(cudaMemcpyAsync( + RAFT_CHECK_CUDA(stream); + RAFT_CUDA_TRY(cudaMemcpyAsync( &distsSum, distsCumSum + n - 1, sizeof(value_type_t), cudaMemcpyDeviceToHost, stream)); // Randomly choose observation vector @@ -523,7 +523,7 @@ static int initializeCentroids(raft::device_resources const& handle, WARNING("error in k-means++ (could not pick centroid)"); // Compute distances from ith centroid - CUDA_TRY(cudaMemsetAsync(dists + n, 0, n * sizeof(value_type_t), stream)); + RAFT_CUDA_TRY(cudaMemsetAsync(dists + n, 0, n * sizeof(value_type_t), stream)); computeDistances<<>>( n, d, 1, obs, centroids + IDX(0, i, d), dists + n); RAFT_CHECK_CUDA(stream); @@ -534,7 +534,7 @@ static int initializeCentroids(raft::device_resources const& handle, } // Compute cluster sizes - CUDA_TRY(cudaMemsetAsync(clusterSizes, 0, k * sizeof(index_type_t), stream)); + RAFT_CUDA_TRY(cudaMemsetAsync(clusterSizes, 0, k * sizeof(index_type_t), stream)); computeClusterSizes<<>>(n, codes, clusterSizes); RAFT_CHECK_CUDA(stream); @@ -598,7 +598,7 @@ static int assignCentroids(raft::device_resources const& handle, RAFT_CHECK_CUDA(stream); // Find centroid closest to each observation vector - CUDA_TRY(cudaMemsetAsync(clusterSizes, 0, k * sizeof(index_type_t), stream)); + RAFT_CUDA_TRY(cudaMemsetAsync(clusterSizes, 0, k * sizeof(index_type_t), stream)); blockDim.x = BLOCK_SIZE; blockDim.y = 1; blockDim.z = 1; @@ -606,7 +606,7 @@ static int assignCentroids(raft::device_resources const& handle, gridDim.y = 1; gridDim.z = 1; minDistances<<>>(n, k, dists, codes, clusterSizes); - CHECK_CUDA(stream); + RAFT_CHECK_CUDA(stream); // Compute residual sum of squares *residual_host = thrust::reduce( @@ -825,8 +825,8 @@ int kmeans(raft::device_resources const& handle, // Trivial cases if (k == 1) { - CUDA_TRY(cudaMemsetAsync(codes, 0, n * sizeof(index_type_t), stream)); - CUDA_TRY( + RAFT_CUDA_TRY(cudaMemsetAsync(codes, 0, n * sizeof(index_type_t), stream)); + RAFT_CUDA_TRY( cudaMemcpyAsync(clusterSizes, &n, sizeof(index_type_t), cudaMemcpyHostToDevice, stream)); if (updateCentroids(handle, n, d, k, obs, codes, clusterSizes, centroids, work, work_int)) WARNING("could not compute k-means centroids"); @@ -837,7 +837,7 @@ int kmeans(raft::device_resources const& handle, 1, std::min(ceildiv(n, BLOCK_SIZE / WARP_SIZE), grid_lower_bound)}; - CUDA_TRY(cudaMemsetAsync(work, 0, n * k * sizeof(value_type_t), stream)); + RAFT_CUDA_TRY(cudaMemsetAsync(work, 0, n * k * sizeof(value_type_t), stream)); computeDistances<<>>(n, d, 1, obs, centroids, work); RAFT_CHECK_CUDA(stream); *residual_host = thrust::reduce( diff --git a/cpp/include/raft/linalg/detail/lanczos.cuh b/cpp/include/raft/linalg/detail/lanczos.cuh index 8c0cfeba28..73d93ab535 100644 --- a/cpp/include/raft/linalg/detail/lanczos.cuh +++ b/cpp/include/raft/linalg/detail/lanczos.cuh @@ -958,7 +958,7 @@ int computeSmallestEigenvectors( (*effIter) * nEigVecs * sizeof(value_type_t), cudaMemcpyHostToDevice, stream)); - CHECK_CUDA(stream); + RAFT_CHECK_CUDA(stream); // Convert eigenvectors from Lanczos basis to standard basis RAFT_CUBLAS_TRY(cublasgemm(cublas_h, @@ -1305,7 +1305,7 @@ int computeLargestEigenvectors( cudaMemcpyHostToDevice, stream)); - CHECK_CUDA(stream); + RAFT_CHECK_CUDA(stream); // Convert eigenvectors from Lanczos basis to standard basis RAFT_CUBLAS_TRY(cublasgemm(cublas_h, diff --git a/cpp/include/raft/solver/detail/lap_functions.cuh b/cpp/include/raft/solver/detail/lap_functions.cuh index 440e6901c6..63f27e6346 100644 --- a/cpp/include/raft/solver/detail/lap_functions.cuh +++ b/cpp/include/raft/solver/detail/lap_functions.cuh @@ -113,7 +113,7 @@ inline void initialReduction(raft::device_resources const& handle, kernel_rowReduction<<>>( d_costs, d_vertices_dev.row_duals, SP, N, std::numeric_limits::max()); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); kernel_columnReduction<<>>( d_costs, d_vertices_dev.row_duals, @@ -121,7 +121,7 @@ inline void initialReduction(raft::device_resources const& handle, SP, N, std::numeric_limits::max()); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); } template @@ -159,7 +159,7 @@ inline void computeInitialAssignments(raft::device_resources const& handle, SP, N, epsilon); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); } // Function for finding row cover on individual devices. @@ -191,7 +191,7 @@ inline int computeRowCovers(raft::device_resources const& handle, kernel_computeRowCovers<<>>( d_vertices.row_assignments, d_vertices.row_covers, d_row_data.is_visited, SP, N); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); return thrust::reduce(thrust::device, d_vertices.row_covers, d_vertices.row_covers + size); } @@ -268,7 +268,7 @@ inline vertex_t zeroCoverIteration(raft::device_resources const& handle, 0, handle.get_stream()>>>( predicates_v.data(), addresses_v.data(), d_row_data_dev.is_visited, SP, N); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); M = thrust::reduce(thrust::device, addresses_v.begin(), addresses_v.end()); thrust::exclusive_scan( @@ -286,7 +286,7 @@ inline vertex_t zeroCoverIteration(raft::device_resources const& handle, SP, N); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); } } @@ -356,7 +356,7 @@ inline void reversePass(raft::device_resources const& handle, handle.get_stream()>>>( predicates_v.data(), addresses_v.data(), d_col_data_dev.is_visited, size); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); // calculate total number of vertices. std::size_t csr_size = thrust::reduce(thrust::device, addresses_v.begin(), addresses_v.end()); @@ -375,11 +375,11 @@ inline void reversePass(raft::device_resources const& handle, kernel_augmentScatter<<>>( elements_v.data(), predicates_v.data(), addresses_v.data(), size); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); kernel_reverseTraversal<<>>( elements_v.data(), d_row_data_dev, d_col_data_dev, csr_size); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); } } @@ -410,7 +410,7 @@ inline void augmentationPass(raft::device_resources const& handle, handle.get_stream()>>>( predicates_v.data(), addresses_v.data(), d_row_data_dev.is_visited, SP * N); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); // calculate total number of vertices. // TODO: should be vertex_t @@ -432,7 +432,7 @@ inline void augmentationPass(raft::device_resources const& handle, kernel_augmentScatter<<>>( elements_v.data(), predicates_v.data(), addresses_v.data(), vertex_t{SP * N}); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); kernel_augmentation<<>>( d_vertices_dev.row_assignments, @@ -443,7 +443,7 @@ inline void augmentationPass(raft::device_resources const& handle, vertex_t{N}, row_ids_csr_size); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); } } @@ -471,7 +471,7 @@ inline void dualUpdate(raft::device_resources const& handle, N, std::numeric_limits::max()); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); detail::calculateRectangularDims(blocks_per_grid, threads_per_block, total_blocks, N, SP); kernel_dualUpdate_2<<>>( @@ -488,7 +488,7 @@ inline void dualUpdate(raft::device_resources const& handle, std::numeric_limits::max(), epsilon); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); } // Function for calculating optimal objective function value using dual variables. @@ -508,7 +508,7 @@ inline void calcObjValDual(raft::device_resources const& handle, kernel_calcObjValDual<<>>( d_obj_val, d_vertices_dev.row_duals, d_vertices_dev.col_duals, SP, N); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); } // Function for calculating optimal objective function value using dual variables. @@ -529,7 +529,7 @@ inline void calcObjValPrimal(raft::device_resources const& handle, kernel_calcObjValPrimal<<>>( d_obj_val, d_costs, d_row_assignments, SP, N); - CHECK_CUDA(handle.get_stream()); + RAFT_CHECK_CUDA(handle.get_stream()); } } // namespace raft::solver::detail diff --git a/cpp/include/raft/solver/linear_assignment.cuh b/cpp/include/raft/solver/linear_assignment.cuh index 7904c04ede..6e66bafe1f 100644 --- a/cpp/include/raft/solver/linear_assignment.cuh +++ b/cpp/include/raft/solver/linear_assignment.cuh @@ -170,7 +170,7 @@ class LinearAssignmentProblem { { weight_t result; raft::update_host(&result, obj_val_primal_v.data() + spId, 1, handle_.get_stream()); - CHECK_CUDA(handle_.get_stream()); + RAFT_CHECK_CUDA(handle_.get_stream()); return result; } @@ -183,7 +183,7 @@ class LinearAssignmentProblem { { weight_t result; raft::update_host(&result, obj_val_dual_v.data() + spId, 1, handle_.get_stream()); - CHECK_CUDA(handle_.get_stream()); + RAFT_CHECK_CUDA(handle_.get_stream()); return result; } diff --git a/cpp/include/raft/sparse/solver/detail/lanczos.cuh b/cpp/include/raft/sparse/solver/detail/lanczos.cuh index 63bc98b404..67d6f6c412 100644 --- a/cpp/include/raft/sparse/solver/detail/lanczos.cuh +++ b/cpp/include/raft/sparse/solver/detail/lanczos.cuh @@ -962,7 +962,7 @@ int computeSmallestEigenvectors( (*effIter) * nEigVecs * sizeof(value_type_t), cudaMemcpyHostToDevice, stream)); - CHECK_CUDA(stream); + RAFT_CHECK_CUDA(stream); // Convert eigenvectors from Lanczos basis to standard basis RAFT_CUBLAS_TRY(raft::linalg::detail::cublasgemm(cublas_h, @@ -1312,7 +1312,7 @@ int computeLargestEigenvectors( cudaMemcpyHostToDevice, stream)); - CHECK_CUDA(stream); + RAFT_CHECK_CUDA(stream); // Convert eigenvectors from Lanczos basis to standard basis RAFT_CUBLAS_TRY(raft::linalg::detail::cublasgemm(cublas_h, diff --git a/cpp/include/raft/spectral/detail/matrix_wrappers.hpp b/cpp/include/raft/spectral/detail/matrix_wrappers.hpp index e32b718117..73518e20ef 100644 --- a/cpp/include/raft/spectral/detail/matrix_wrappers.hpp +++ b/cpp/include/raft/spectral/detail/matrix_wrappers.hpp @@ -352,7 +352,7 @@ struct laplacian_matrix_t : sparse_matrix_t { // scales y by beta: // if (beta == 0) { - CUDA_TRY(cudaMemsetAsync(y, 0, n * sizeof(value_type), stream)); + RAFT_CUDA_TRY(cudaMemsetAsync(y, 0, n * sizeof(value_type), stream)); } else if (beta != 1) { // TODO: Call from public API when ready RAFT_CUBLAS_TRY(raft::linalg::detail::cublasscal(cublas_h, n, &beta, y, 1, stream)); diff --git a/cpp/include/raft/util/cudart_utils.hpp b/cpp/include/raft/util/cudart_utils.hpp index 0a7ca23028..1134513587 100644 --- a/cpp/include/raft/util/cudart_utils.hpp +++ b/cpp/include/raft/util/cudart_utils.hpp @@ -14,14 +14,6 @@ * limitations under the License. */ -/** - * This file is deprecated and will be removed in release 22.06. - * Please use raft_runtime/cudart_utils.hpp instead. - */ - -#ifndef __RAFT_RT_CUDART_UTILS_H -#define __RAFT_RT_CUDART_UTILS_H - #pragma once #include @@ -32,7 +24,7 @@ #include #include -#include +#include #include #include @@ -43,11 +35,6 @@ #include #include -// FIXME: Remove after consumers rename -#ifndef CUDA_TRY -#define CUDA_TRY(call) RAFT_CUDA_TRY(call) -#endif - /** * @brief Debug macro to check for CUDA errors * @@ -67,16 +54,6 @@ #define RAFT_CHECK_CUDA(stream) RAFT_CUDA_TRY(cudaPeekAtLastError()); #endif -// FIXME: Remove after consumers rename -#ifndef CHECK_CUDA -#define CHECK_CUDA(call) RAFT_CHECK_CUDA(call) -#endif - -/** FIXME: remove after cuml rename */ -#ifndef CUDA_CHECK -#define CUDA_CHECK(call) RAFT_CUDA_TRY(call) -#endif - // /** // * @brief check for cuda runtime API errors but log error instead of raising // * exception. @@ -93,17 +70,6 @@ } \ } while (0) -// FIXME: Remove after cuml rename -#ifndef CUDA_CHECK_NO_THROW -#define CUDA_CHECK_NO_THROW(call) RAFT_CUDA_TRY_NO_THROW(call) -#endif - -/** - * Alias to raft scope for now. - * TODO: Rename original implementations in 22.04 to fix - * https://github.com/rapidsai/raft/issues/128 - */ - namespace raft { /** Helper method to get to know warp size in device code */ @@ -215,7 +181,7 @@ class grid_1d_block_t { template void copy(Type* dst, const Type* src, size_t len, rmm::cuda_stream_view stream) { - CUDA_CHECK(cudaMemcpyAsync(dst, src, len * sizeof(Type), cudaMemcpyDefault, stream)); + RAFT_CUDA_TRY(cudaMemcpyAsync(dst, src, len * sizeof(Type), cudaMemcpyDefault, stream)); } /** @@ -241,7 +207,8 @@ void update_host(Type* h_ptr, const Type* d_ptr, size_t len, rmm::cuda_stream_vi template void copy_async(Type* d_ptr1, const Type* d_ptr2, size_t len, rmm::cuda_stream_view stream) { - CUDA_CHECK(cudaMemcpyAsync(d_ptr1, d_ptr2, len * sizeof(Type), cudaMemcpyDeviceToDevice, stream)); + RAFT_CUDA_TRY( + cudaMemcpyAsync(d_ptr1, d_ptr2, len * sizeof(Type), cudaMemcpyDeviceToDevice, stream)); } /** @} */ @@ -270,7 +237,7 @@ void print_device_vector(const char* variable_name, OutStream& out) { auto host_mem = std::make_unique(componentsCount); - CUDA_CHECK( + RAFT_CUDA_TRY( cudaMemcpy(host_mem.get(), devMem, componentsCount * sizeof(T), cudaMemcpyDeviceToHost)); print_host_vector(variable_name, host_mem.get(), componentsCount, out); } @@ -532,5 +499,3 @@ inline auto get_pool_memory_resource(rmm::mr::device_memory_resource*& mr, size_ } } // namespace raft - -#endif diff --git a/dependencies.yaml b/dependencies.yaml index dd361a0cdf..64fd7cd454 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -15,6 +15,16 @@ files: - run_pylibraft - test_python_common - test_pylibraft + bench_ann: + output: conda + matrix: + cuda: ["11.8"] + arch: [x86_64] + includes: + - build + - develop + - cudatoolkit + - nn_bench test_cpp: output: none includes: @@ -137,6 +147,17 @@ dependencies: - output_types: [conda] packages: - clang-tools=11.1.0 + nn_bench: + common: + - output_types: [conda] + packages: + - hnswlib=0.7.0 + - nlohmann_json>=3.11.2 + - glog>=0.6.0 + - h5py>=3.8.0 + - libfaiss>=1.7.1 + - faiss-proc=*=cuda + cudatoolkit: specific: - output_types: conda diff --git a/docs/source/cuda_ann_benchmarks.md b/docs/source/cuda_ann_benchmarks.md new file mode 100644 index 0000000000..708f5f7dba --- /dev/null +++ b/docs/source/cuda_ann_benchmarks.md @@ -0,0 +1,322 @@ +# CUDA ANN Benchmarks + +This project provides a benchmark program for various ANN search implementations. It's especially suitable for comparing GPU implementations as well as comparing GPU against CPU. + +## Benchmark + +### Dependencies + +CUDA 11 and a GPU with Pascal architecture or later are required to run the benchmarks. + +Please refer to the [installation docs](https://docs.rapids.ai/api/raft/stable/build.html#cuda-gpu-requirements) for the base requirements to build RAFT. + +In addition to the base requirements for building RAFT, additional dependencies needed to build the ANN benchmarks include: +1. FAISS GPU >= 1.7.1 +2. Google Logging (GLog) +3. H5Py +4. HNSWLib +5. nlohmann_json +6. GGNN + +[rapids-cmake](https://github.com/rapidsai/rapids-cmake) is used to build the ANN benchmarks so the code for dependencies not already supplied in the CUDA toolkit will be downloaded and built automatically. + +The easiest (and most reproducible) way to install the dependencies needed to build the ANN benchmarks is to use the conda environment file located in the `conda/environments` directory of the RAFT repository. The following command will use `mamba` (which is preferred over `conda`) to build and activate a new environment for compiling the benchmarks: + +```bash +mamba env create --name raft_ann_benchmarks -f conda/environments/bench_ann_cuda-118_arch-x86_64.yaml +conda activate raft_ann_benchmarks +``` + +The above conda environment will also reduce the compile times as dependencies like FAISS will already be installed and not need to be compiled with `rapids-cmake`. + +### Compiling the Benchmarks + +After the needed dependencies are satisfied, the easiest way to compile ANN benchmarks is through the `build.sh` script in the root of the RAFT source code repository. The following will build the executables for all the support algorithms: +```bash +./build.sh bench-ann +``` + +You can limit the algorithms that are built by providing a semicolon-delimited list of executable names (each algorithm is suffixed with `_ANN_BENCH`): +```bash +./build.sh bench-ann --limit-bench-ann=HNSWLIB_ANN_BENCH;RAFT_IVF_PQ_ANN_BENCH +``` + +Available targets to use with `--limit-bench-ann` are: +- FAISS_IVF_FLAT_ANN_BENCH +- FAISS_IVF_PQ_ANN_BENCH +- FAISS_BFKNN_ANN_BENCH +- GGNN_ANN_BENCH +- HNSWLIB_ANN_BENCH +- RAFT_IVF_PQ_ANN_BENCH +- RAFT_IVF_FLAT_ANN_BENCH +- RAFT_BFKNN_ANN_BENCH + +By default, the `*_ANN_BENCH` executables program infer the dataset's datatype from the filename's extension. For example, an extension of `fbin` uses a `float` datatype, `f16bin` uses a `float16` datatype, extension of `i8bin` uses `int8_t` datatype, and `u8bin` uses `uint8_t` type. Currently, only `float`, `float16`, int8_t`, and `unit8_t` are supported. + +### Usage +There are 4 general steps to running the benchmarks: +1. Prepare Dataset +2. Build Index +3. Search Using Built Index +4. Evaluate Result + +#### End-to-end Example +An end-to-end example (run from the RAFT source code root directory): +```bash +# (1) prepare a dataset +pushd + +cd cpp/bench/ann +mkdir data && cd data +wget http://ann-benchmarks.com/glove-100-angular.hdf5 + +# option -n is used here to normalize vectors so cosine distance is converted +# to inner product; don't use -n for l2 distance +python scripts/hdf5_to_fbin.py -n glove-100-angular.hdf5 + +mkdir glove-100-inner +mv glove-100-angular.base.fbin glove-100-inner/base.fbin +mv glove-100-angular.query.fbin glove-100-inner/query.fbin +mv glove-100-angular.groundtruth.neighbors.ibin glove-100-inner/groundtruth.neighbors.ibin +mv glove-100-angular.groundtruth.distances.fbin glove-100-inner/groundtruth.distances.fbin +popd + +# (2) build index +./cpp/build/RAFT_IVF_FLAT_ANN_BENCH -b -i raft_ivf_flat.nlist1024 conf/glove-100-inner.json + +# (3) search +./cpp/build/RAFT_IVF_FLAT_ANN_BENCH -s -i raft_ivf_flat.nlist1024 conf/glove-100-inner.json + +# (4) evaluate result +pushd +cd cpp/bench/ann +./scripts/eval.pl \ + -o result.csv \ + data/glove-100-inner/groundtruth.neighbors.ibin \ + result/glove-100-inner/faiss_ivf_flat +popd + +# optional step: plot QPS-Recall figure using data in result.csv with your favorite tool +``` + +##### Step 1: Prepare Dataset +A dataset usually has 4 binary files containing database vectors, query vectors, ground truth neighbors and their corresponding distances. For example, Glove-100 dataset has files `base.fbin` (database vectors), `query.fbin` (query vectors), `groundtruth.neighbors.ibin` (ground truth neighbors), and `groundtruth.distances.fbin` (ground truth distances). The first two files are for index building and searching, while the other two are associated with a particular distance and are used for evaluation. + +The file suffixes `.fbin`, `.f16bin`, `.ibin`, `.u8bin`, and `.i8bin` denote that the data type of vectors stored in the file are `float32`, `float16`(a.k.a `half`), `int`, `uint8`, and `int8`, respectively. +These binary files are little-endian and the format is: the first 8 bytes are `num_vectors` (`uint32_t`) and `num_dimensions` (`uint32_t`), and the following `num_vectors * num_dimensions * sizeof(type)` bytes are vectors stored in row-major order. + +Some implementation can take `float16` database and query vectors as inputs and will have better performance. Use `script/fbin_to_f16bin.py` to transform dataset from `float32` to `float16` type. + +Commonly used datasets can be downloaded from two websites: +1. Million-scale datasets can be found at the [Data sets](https://github.com/erikbern/ann-benchmarks#data-sets) section of [`ann-benchmarks`](https://github.com/erikbern/ann-benchmarks). + + However, these datasets are in HDF5 format. Use `cpp/bench/ann/scripts/hdf5_to_fbin.py` to transform the format. A few Python packages are required to run it: + ```bash + pip3 install numpy h5py + ``` + The usage of this script is: + ```bash + $ cpp/bench/ann/scripts/hdf5_to_fbin.py + usage: scripts/hdf5_to_fbin.py [-n] .hdf5 + -n: normalize base/query set + outputs: .base.fbin + .query.fbin + .groundtruth.neighbors.ibin + .groundtruth.distances.fbin + ``` + So for an input `.hdf5` file, four output binary files will be produced. See previous section for an example of prepossessing GloVe dataset. + + Most datasets provided by `ann-benchmarks` use `Angular` or `Euclidean` distance. `Angular` denotes cosine distance. However, computing cosine distance reduces to computing inner product by normalizing vectors beforehand. In practice, we can always do the normalization to decrease computation cost, so it's better to measure the performance of inner product rather than cosine distance. The `-n` option of `hdf5_to_fbin.py` can be used to normalize the dataset. + +2. Billion-scale datasets can be found at [`big-ann-benchmarks`](http://big-ann-benchmarks.com). The ground truth file contains both neighbors and distances, thus should be split. A script is provided for this: + ```bash + $ cpp/bench/ann/scripts/split_groundtruth.pl + usage: script/split_groundtruth.pl input output_prefix + ``` + Take Deep-1B dataset as an example: + ```bash + pushd + cd cpp/bench/ann + mkdir -p data/deep-1B && cd data/deep-1B + # download manually "Ground Truth" file of "Yandex DEEP" + # suppose the file name is deep_new_groundtruth.public.10K.bin + ../../scripts/split_groundtruth.pl deep_new_groundtruth.public.10K.bin groundtruth + # two files 'groundtruth.neighbors.ibin' and 'groundtruth.distances.fbin' should be produced + popd + ``` + Besides ground truth files for the whole billion-scale datasets, this site also provides ground truth files for the first 10M or 100M vectors of the base sets. This mean we can use these billion-scale datasets as million-scale datasets. To facilitate this, an optional parameter `subset_size` for dataset can be used. See the next step for further explanation. + + +##### Step 2: Build Index +An index is a data structure to facilitate searching. Different algorithms may use different data structures for their index. We can use `RAFT_IVF_FLAT_ANN_BENCH -b` to build an index and save it to disk. + +To run a benchmark executable, like `RAFT_IVF_FLAT_ANN_BENCH`, a JSON configuration file is required. Refer to [`cpp/bench/ann/conf/glove-100-inner.json`](../../cpp/cpp/bench/ann/conf/glove-100-inner.json) as an example. Configuration file has 3 sections: +* `dataset` section specifies the name and files of a dataset, and also the distance in use. Since the `*_ANN_BENCH` programs are for index building and searching, only `base_file` for database vectors and `query_file` for query vectors are needed. Ground truth files are for evaluation thus not needed. + - To use only a subset of the base dataset, an optional parameter `subset_size` can be specified. It means using only the first `subset_size` vectors of `base_file` as the base dataset. +* `search_basic_param` section specifies basic parameters for searching: + - `k` is the "k" in "k-nn", that is, the number of neighbors (or results) we want from the searching. + - `run_count` means how many times we run the searching. A single run of searching will search neighbors for all vectors in `test` set. The total time used for a run is recorded, and the final searching time is the smallest one among these runs. +* `index` section specifies an array of configurations for index building and searching: + - `build_param` and `search_params` are parameters for building and searching, respectively. `search_params` is an array since we will search with different parameters to get different recall values. + - `file` is the file name of index. Building will save built index to this file, while searching will load this file. + - `search_result_file` is the file name prefix of searching results. Searching will save results to these files, and plotting script will read these files to plot results. Note this is a prefix rather than a whole file name. Suppose its value is `${prefix}`, then the real file names are like `${prefix}.0.{ibin|txt}`, `${prefix}.1.{ibin|txt}`, etc. Each of them corresponds to an item in `search_params` array. That is, for one searching parameter, there will be some corresponding search result files. + - if `multigpu` is specified, multiple GPUs will be used for index build and search. + - if `refine_ratio` is specified, refinement, as a post-processing step of search, will be done. It's for algorithms that compress vectors. For example, if `"refine_ratio" : 2` is set, 2`k` results are first computed, then exact distances of them are computed using original uncompressed vectors, and finally top `k` results among them are kept. + + +The usage of `*_ANN_BENCH` can be found by running `*_ANN_BENCH -h` on one of the executables: +```bash +$ ./cpp/build/*_ANN_BENCH -h +usage: ./cpp/build/*_ANN_BENCH -b|s [-f] [-i index_names] conf.json + -b: build mode, will build index + -s: search mode, will search using built index + one and only one of -b and -s should be specified + -f: force overwriting existing output files + -i: by default will build/search all the indices found in conf.json + '-i' can be used to select a subset of indices + 'index_names' is a list of comma-separated index names + '*' is allowed as the last character of a name to select all matched indices + for example, -i "hnsw1,hnsw2,faiss" or -i "hnsw*,faiss" +``` +* `-b`: build index. +* `-s`: do the searching with built index. +* `-f`: before doing the real task, the program checks that needed input files exist and output files don't exist. If these conditions are not met, it quits so no file would be overwritten accidentally. To ignore existing output files and force overwrite them, use the `-f` option. +* `-i`: by default, the `-b` flag will build all indices found in the configuration file, and `-s` will search using all the indices. To select a subset of indices to build or search, we can use the `-i` option. + +It's easier to describe the usage of `-i` option with an example. Suppose we have a configuration file `a.json`, and it contains: +```json + "index" : [ + { + "name" : "hnsw1", + ... + }, + { + "name" : "hnsw1", + ... + }, + { + "name" : "faiss", + ... + } + ] +``` +Then, +```bash +# build all indices: hnsw1, hnsw2 and faiss +./cpp/build/HNSWLIB_ANN_BENCH -b a.json + +# build only hnsw1 +./cpp/build/HNSWLIB_ANN_BENCH -b -i hnsw1 a.json + +# build hnsw1 and hnsw2 +./cpp/build/HNSWLIB_ANN_BENCH -b -i hnsw1,hnsw2 a.json + +# build hnsw1 and hnsw2 +./cpp/build/HNSWLIB_ANN_BENCH -b -i 'hnsw*' a.json + +# build faiss +./cpp/build/FAISS_IVF_FLAT_ANN_BENCH -b -i 'faiss' a.json +``` +In the last two commands, we use wildcard "`*`" to match both `hnsw1` and `hnsw2`. Note the use of "`*`" is quite limited. It can occur only at the end of a pattern, so both "`*nsw1`" and "`h*sw1`" are interpreted literally and will not match anything. Also note that quotation marks must be used to prevent "`*`" from being interpreted by the shell. + + +##### Step 3: Searching +Use the `-s` flag on any of the `*_ANN_BENCH` executables. Other options are the same as in step 2. + + +##### Step 4: Evaluating Results +Use `cpp/bench/ann/scripts/eval.pl` to evaluate benchmark results. The usage is: +```bash +$ cpp/bench/ann/scripts/eval.pl +usage: [-f] [-o output.csv] groundtruth.neighbors.ibin result_paths... + result_paths... are paths to the search result files. + Can specify multiple paths. + For each of them, if it's a directory, all the .txt files found under + it recursively will be regarded as inputs. + + -f: force to recompute recall and update it in result file if needed + -o: also write result to a csv file +``` +Note that there can be multiple arguments for paths of result files. Each argument can be either a file name or a path. If it's a directory, all files found under it recursively will be used as input files. +An example: +```bash +cpp/bench/ann/scripts/eval.pl groundtruth.neighbors.ibin \ + result/glove-100-angular/10/hnsw/angular_M_24_*.txt \ + result/glove-100-angular/10/faiss/ +``` +The search result files used by this command are files matching `result/glove-100-angular/10/hnsw/angular_M_24_*.txt`, and all `.txt` files under directory `result/glove-100-angular/10/faiss/` recursively. + +This script prints recall and QPS for every result file. Also, it outputs estimated "recall at QPS=2000" and "QPS at recall=0.9", which can be used to compare performance quantitatively. + +It saves recall value in result txt file, so avoids to recompute recall if the same command is run again. To force to recompute recall, option `-f` can be used. If option `-o ` is specified, a csv output file will be produced. This file can be used to plot Throughput-Recall curves. + +## Adding a new ANN algorithm +Implementation of a new algorithm should be a class that inherits `class ANN` (defined in `cpp/bench/ann/src/ann.h`) and implements all the pure virtual functions. + +In addition, it should define two `struct`s for building and searching parameters. The searching parameter class should inherit `struct ANN::AnnSearchParam`. Take `class HnswLib` as an example, its definition is: +```c++ +template +class HnswLib : public ANN { +public: + struct BuildParam { + int M; + int ef_construction; + int num_threads; + }; + + using typename ANN::AnnSearchParam; + struct SearchParam : public AnnSearchParam { + int ef; + int num_threads; + }; + + // ... +}; +``` + +The benchmark program uses JSON configuration file. To add the new algorithm to the benchmark, need be able to specify `build_param`, whose value is a JSON object, and `search_params`, whose value is an array of JSON objects, for this algorithm in configuration file. Still take the configuration for `HnswLib` as an example: +```json +{ + "name" : "...", + "algo" : "hnswlib", + "build_param": {"M":12, "efConstruction":500, "numThreads":32}, + "file" : "/path/to/file", + "search_params" : [ + {"ef":10, "numThreads":1}, + {"ef":20, "numThreads":1}, + {"ef":40, "numThreads":1}, + ], + "search_result_file" : "/path/to/file" +}, +``` + +How to interpret these JSON objects is totally left to the implementation and should be specified in `cpp/bench/ann/src/factory.cuh`: +1. First, add two functions for parsing JSON object to `struct BuildParam` and `struct SearchParam`, respectively: + ```c++ + template + void parse_build_param(const nlohmann::json& conf, + typename cuann::HnswLib::BuildParam& param) { + param.ef_construction = conf.at("efConstruction"); + param.M = conf.at("M"); + if (conf.contains("numThreads")) { + param.num_threads = conf.at("numThreads"); + } + } + + template + void parse_search_param(const nlohmann::json& conf, + typename cuann::HnswLib::SearchParam& param) { + param.ef = conf.at("ef"); + if (conf.contains("numThreads")) { + param.num_threads = conf.at("numThreads"); + } + } + ``` + +2. Next, add corresponding `if` case to functions `create_algo()` and `create_search_param()` by calling parsing functions. The string literal in `if` condition statement must be the same as the value of `algo` in configuration file. For example, + ```c++ + // JSON configuration file contains a line like: "algo" : "hnswlib" + if (algo == "hnswlib") { + // ... + } + ``` diff --git a/docs/source/index.rst b/docs/source/index.rst index 814899c36b..23e346c872 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -44,6 +44,7 @@ While not exhaustive, the following general categories help summarize the accele developer_guide.md cpp_api.rst pylibraft_api.rst + cuda_ann_benchmarks.md raft_dask_api.rst using_comms.rst using_libraft.md diff --git a/thirdparty/LICENSES/LICENSE.pytorch b/thirdparty/LICENSES/LICENSE.pytorch new file mode 100644 index 0000000000..7ad3d737a5 --- /dev/null +++ b/thirdparty/LICENSES/LICENSE.pytorch @@ -0,0 +1,77 @@ +From PyTorch: + +Copyright (c) 2016- Facebook, Inc (Adam Paszke) +Copyright (c) 2014- Facebook, Inc (Soumith Chintala) +Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert) +Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu) +Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu) +Copyright (c) 2011-2013 NYU (Clement Farabet) +Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston) +Copyright (c) 2006 Idiap Research Institute (Samy Bengio) +Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz) + +From Caffe2: + +Copyright (c) 2016-present, Facebook Inc. All rights reserved. + +All contributions by Facebook: +Copyright (c) 2016 Facebook Inc. + +All contributions by Google: +Copyright (c) 2015 Google Inc. +All rights reserved. + +All contributions by Yangqing Jia: +Copyright (c) 2015 Yangqing Jia +All rights reserved. + +All contributions by Kakao Brain: +Copyright 2019-2020 Kakao Brain + +All contributions by Cruise LLC: +Copyright (c) 2022 Cruise LLC. +All rights reserved. + +All contributions from Caffe: +Copyright(c) 2013, 2014, 2015, the respective contributors +All rights reserved. + +All other contributions: +Copyright(c) 2015, 2016 the respective contributors +All rights reserved. + +Caffe2 uses a copyright model similar to Caffe: each contributor holds +copyright over their contributions to Caffe2. The project versioning records +all such contribution and copyright details. If a contributor wants to further +mark their specific copyright on a particular contribution, they should +indicate their copyright solely in the commit message of the change when it is +committed. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America + and IDIAP Research Institute nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file From c2cb779d6c3b25d2489c7acf6340f23f5157aa9c Mon Sep 17 00:00:00 2001 From: "Corey J. Nolet" Date: Tue, 28 Mar 2023 22:50:32 -0400 Subject: [PATCH 20/25] Fixing index-url link on pip install docs (#1378) Closes #1377 Authors: - Corey J. Nolet (https://github.com/cjnolet) Approvers: - Divye Gala (https://github.com/divyegala) URL: https://github.com/rapidsai/raft/pull/1378 --- README.md | 4 ++-- docs/source/build.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 81973ff82e..b77e906262 100755 --- a/README.md +++ b/README.md @@ -215,8 +215,8 @@ After installing RAFT, `find_package(raft COMPONENTS compiled distributed)` can pylibraft and raft-dask both have experimental packages that can be [installed through pip](https://rapids.ai/pip.html#install): ```bash -pip install pylibraft-cu11 --extra-index-url=https://pypi.ngc.nvidia.com -pip install raft-dask-cu11 --extra-index-url=https://pypi.ngc.nvidia.com +pip install pylibraft-cu11 --extra-index-url=https://pypi.nvidia.com +pip install raft-dask-cu11 --extra-index-url=https://pypi.nvidia.com ``` ### CMake & CPM diff --git a/docs/source/build.md b/docs/source/build.md index e8e6ac8a14..e36f771682 100644 --- a/docs/source/build.md +++ b/docs/source/build.md @@ -21,8 +21,8 @@ After installing RAFT, `find_package(raft COMPONENTS nn distance)` can be used i pylibraft and raft-dask both have experimental packages that can be [installed through pip](https://rapids.ai/pip.html#install): ```bash -pip install pylibraft-cu11 --extra-index-url=https://pypi.ngc.nvidia.com -pip install raft-dask-cu11 --extra-index-url=https://pypi.ngc.nvidia.com +pip install pylibraft-cu11 --extra-index-url=https://pypi.nvidia.com +pip install raft-dask-cu11 --extra-index-url=https://pypi.nvidia.com ``` ## Building and installing RAFT From e963f5a64371c25f1e2e09ab1e3ca15658293e2d Mon Sep 17 00:00:00 2001 From: Allard Hendriksen Date: Wed, 29 Mar 2023 18:36:18 +0200 Subject: [PATCH 21/25] build.sh: Add option to log nvcc compile times (#1262) Add `--time` option to `build.sh` that enables compile time logging of `nvcc`. Also, add a script `cpp/scripts/analyze_nvcc_log.py` to find the translation units that take the longest time. Output looks like: ``` $ cpp/scripts/analyze_nvcc_log.py cpp/build/nvcc_compile_log.csv -- loading data -- analyzing data -- Ten longest translation units: phase index file cicc cudafe++ fatbinary gcc (compiling) gcc (preprocessing 1) gcc (preprocessing 4) ptxas total time 0 10 ions/detail/canberra_double_double_double_int.cu 42.431063 10.601856 0.020979 6.747153 3.721194 2.093567 1618.390375 1684.006186 1 11 zations/detail/canberra_float_float_float_int.cu 36.928960 9.804138 0.011537 6.796088 3.481156 1.790703 1584.262875 1643.075457 2 85 ors/specializations/refine_d_uint64_t_uint8_t.cu 602.935531 14.980877 0.529673 36.300566 6.270717 2.889723 933.622969 1597.530056 3 84 bors/specializations/refine_d_uint64_t_int8_t.cu 606.513281 16.243960 0.729282 39.981113 5.608029 3.028493 897.241469 1569.345628 4 53 stance/neighbors/ivfpq_search_int8_t_uint64_t.cu 841.049750 8.233967 1.025554 24.248578 4.069022 1.747108 631.193734 1511.567713 5 52 istance/neighbors/ivfpq_search_float_uint64_t.cu 837.241437 8.145278 1.042313 24.400606 3.433528 1.882623 627.786672 1503.932457 6 54 tance/neighbors/ivfpq_search_uint8_t_uint64_t.cu 846.706656 8.371286 1.025517 24.094691 3.432749 1.645345 618.319234 1503.595479 7 76 izations/detail/ivfpq_search_uint8_t_uint64_t.cu 698.726266 7.086368 1.050021 39.727723 3.259101 1.333935 406.509937 1157.693351 8 74 alizations/detail/ivfpq_search_float_uint64_t.cu 706.702516 6.905794 1.049731 39.923895 2.814361 2.057154 395.604000 1155.057450 9 75 lizations/detail/ivfpq_search_int8_t_uint64_t.cu 689.390281 6.483386 1.025864 39.865668 3.121696 1.297788 409.099562 1150.284245 10 83 hbors/specializations/refine_d_uint64_t_float.cu 334.705594 15.466444 0.680270 36.551977 5.405133 2.947568 715.708781 1111.465767 -- Plotting absolute compile times -- Wrote absolute compile time plot to cpp/build/nvcc_compile_log.csv.absolute.compile_times.png -- Plotting relative compile times -- Wrote relative compile time plot to cpp/build/nvcc_compile_log.csv.relative.compile_times.png ``` Authors: - Allard Hendriksen (https://github.com/ahendriksen) Approvers: - Corey J. Nolet (https://github.com/cjnolet) URL: https://github.com/rapidsai/raft/pull/1262 --- build.sh | 10 +- cpp/CMakeLists.txt | 1 + cpp/cmake/modules/ConfigureCUDA.cmake | 6 +- cpp/scripts/analyze_nvcc_log.py | 134 ++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100755 cpp/scripts/analyze_nvcc_log.py diff --git a/build.sh b/build.sh index 3758dc26c4..7e1a3e7e36 100755 --- a/build.sh +++ b/build.sh @@ -18,7 +18,7 @@ ARGS=$* # scripts, and that this script resides in the repo dir! REPODIR=$(cd $(dirname $0); pwd) -VALIDARGS="clean libraft pylibraft raft-dask docs tests template bench-prims bench-ann clean --uninstall -v -g -n --compile-lib --allgpuarch --no-nvtx --show_depr_warn -h" +VALIDARGS="clean libraft pylibraft raft-dask docs tests template bench-prims bench-ann clean --uninstall -v -g -n --compile-lib --allgpuarch --no-nvtx --show_depr_warn --time -h" HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=] [--limit-tests=] [--limit-bench-prims=] [--limit-bench-ann=] where is: clean - remove all existing build artifacts and configuration (start over) @@ -48,6 +48,8 @@ HELP="$0 [ ...] [ ...] [--cmake-args=\"\"] [--cache-tool=\\\" - pass arbitrary list of CMake configuration options (escape all quotes in argument) --cache-tool= - pass the build cache tool (eg: ccache, sccache, distcc) that will be used to speedup the build process. + --time - Enable nvcc compilation time logging into cpp/build/nvcc_compile_log.csv. + Results can be interpreted with cpp/scripts/analyze_nvcc_log.py -h - print this text default action (no args) is to build libraft, tests, pylibraft and raft-dask targets @@ -75,6 +77,7 @@ BENCH_TARGETS="CLUSTER_BENCH;NEIGHBORS_BENCH;DISTANCE_BENCH;LINALG_BENCH;MATRIX_ CACHE_ARGS="" NVTX=ON +LOG_COMPILE_TIME=OFF CLEAN=0 UNINSTALL=0 DISABLE_DEPRECATION_WARNINGS=ON @@ -322,6 +325,10 @@ fi if hasArg --no-nvtx; then NVTX=OFF fi +if hasArg --time; then + echo "-- Logging compile times to cpp/build/nvcc_compile_log.csv" + LOG_COMPILE_TIME=ON +fi if hasArg --show_depr_warn; then DISABLE_DEPRECATION_WARNINGS=OFF fi @@ -379,6 +386,7 @@ if (( ${NUMARGS} == 0 )) || hasArg libraft || hasArg docs || hasArg tests || has -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ -DRAFT_COMPILE_LIBRARY=${COMPILE_LIBRARY} \ -DRAFT_NVTX=${NVTX} \ + -DCUDA_LOG_COMPILE_TIME=${LOG_COMPILE_TIME} \ -DDISABLE_DEPRECATION_WARNINGS=${DISABLE_DEPRECATION_WARNINGS} \ -DBUILD_TESTS=${BUILD_TESTS} \ -DBUILD_PRIMS_BENCH=${BUILD_PRIMS_BENCH} \ diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 7bb458c44a..2e9c726b8e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -53,6 +53,7 @@ option(CUDA_ENABLE_LINEINFO "Enable the -lineinfo option for nvcc (useful for cuda-memcheck / profiler)" OFF ) option(CUDA_STATIC_RUNTIME "Statically link the CUDA toolkit runtime and libraries" OFF) +option(CUDA_LOG_COMPILE_TIME "Write a log of compilation times to nvcc_compile_log.csv" OFF) option(DETECT_CONDA_ENV "Enable detection of conda environment for dependencies" ON) option(DISABLE_DEPRECATION_WARNINGS "Disable deprecaction warnings " ON) option(DISABLE_OPENMP "Disable OpenMP" OFF) diff --git a/cpp/cmake/modules/ConfigureCUDA.cmake b/cpp/cmake/modules/ConfigureCUDA.cmake index 5e68ca5bc4..c733d46985 100644 --- a/cpp/cmake/modules/ConfigureCUDA.cmake +++ b/cpp/cmake/modules/ConfigureCUDA.cmake @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2018-2022, NVIDIA CORPORATION. +# Copyright (c) 2018-2023, NVIDIA CORPORATION. # # 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 @@ -21,6 +21,10 @@ if(CMAKE_COMPILER_IS_GNUCXX) list(APPEND RAFT_CXX_FLAGS -Wall -Werror -Wno-unknown-pragmas -Wno-error=deprecated-declarations) endif() +if(CUDA_LOG_COMPILE_TIME) + list(APPEND RAFT_CUDA_FLAGS "--time=nvcc_compile_log.csv") +endif() + list(APPEND RAFT_CUDA_FLAGS --expt-extended-lambda --expt-relaxed-constexpr) list(APPEND RAFT_CXX_FLAGS "-DCUDA_API_PER_THREAD_DEFAULT_STREAM") list(APPEND RAFT_CUDA_FLAGS "-DCUDA_API_PER_THREAD_DEFAULT_STREAM") diff --git a/cpp/scripts/analyze_nvcc_log.py b/cpp/scripts/analyze_nvcc_log.py new file mode 100755 index 0000000000..d06e05d265 --- /dev/null +++ b/cpp/scripts/analyze_nvcc_log.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023, NVIDIA CORPORATION. +# +# 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. + +import sys +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +from pathlib import Path +from matplotlib import colors + +def main(input_path): + input_path = Path(input_path) + print("-- loading data") + df = pd.read_csv(input_path) + + print("-- analyzing data") + # Strip spaces from column names + df = df.rename(columns=str.strip) + df["seconds"] = df["metric"] / 1000 + df["file"] = df["source file name"] + df["phase"] = df["phase name"].str.strip() + + dfp = (df + # Remove nvcc driver entries. They don't contain a source file name + .query("phase!='nvcc (driver)'") + # Make a pivot table containing files as row, phase (preprocessing, + # cicc, etc.) as column and the total times as table entries. NOTE: + # if compiled for multiple archs, the archs will be summed. + .pivot_table(index="file", values="seconds", columns="phase", aggfunc='sum')) + + dfp_sum = dfp.sum(axis="columns") + + df_fraction = dfp.divide(dfp_sum, axis="index") + df_fraction["total time"] = dfp_sum + df_fraction = df_fraction.melt(ignore_index=False, id_vars="total time", var_name="phase", value_name="fraction") + + dfp["total time"] = dfp_sum + df_absolute = dfp.melt(ignore_index=False, id_vars="total time", var_name="phase", value_name="seconds") + + # host: light red to dark red (preprocessing, cudafe, gcc (compiling)) + # device: ligt green to dark green (preprocessing, cicc, ptxas) + palette = { + "gcc (preprocessing 4)": colors.hsv_to_rgb((0, 1, 1)), + 'cudafe++': colors.hsv_to_rgb((0, 1, .75)), + 'gcc (compiling)': colors.hsv_to_rgb((0, 1, .4)), + "gcc (preprocessing 1)": colors.hsv_to_rgb((.33, 1, 1)), + 'cicc': colors.hsv_to_rgb((.33, 1, 0.75)), + 'ptxas': colors.hsv_to_rgb((.33, 1, 0.4)), + 'fatbinary': "grey", + } + + print("-- Ten longest translation units:") + colwidth = pd.get_option('display.max_colwidth') - 1 + dfp = dfp.reset_index() + dfp["file"] = dfp["file"].apply(lambda s: s[-colwidth:]) + print(dfp.sort_values("total time", ascending=False).reset_index().loc[:10]) + + print("-- Plotting absolute compile times") + abs_out_path = f"{input_path}.absolute.compile_times.png" + sns.displot( + df_absolute.sort_values("total time").reset_index(), + y="file", + hue="phase", + hue_order=reversed( + ["gcc (preprocessing 4)", 'cudafe++', 'gcc (compiling)', + "gcc (preprocessing 1)", 'cicc', 'ptxas', + 'fatbinary', + ]), + palette=palette, + weights="seconds", + multiple="stack", + kind="hist", + height=20, + ) + plt.xlabel("seconds"); + plt.savefig(abs_out_path) + print(f"-- Wrote absolute compile time plot to {abs_out_path}") + + print("-- Plotting relative compile times") + rel_out_path = f"{input_path}.relative.compile_times.png" + sns.displot( + df_fraction.sort_values('total time').reset_index(), + y="file", + hue="phase", + hue_order=reversed(["gcc (preprocessing 4)", 'cudafe++', 'gcc (compiling)', + "gcc (preprocessing 1)", 'cicc', 'ptxas', + 'fatbinary', + ]), + palette=palette, + weights="fraction", + multiple="stack", + kind="hist", + height=15, + ) + plt.xlabel("fraction"); + plt.savefig(rel_out_path) + print(f"-- Wrote relative compile time plot to {rel_out_path}") + +if __name__ == "__main__": + if len(sys.argv) != 2: + printf("""NVCC log analyzer + + Analyzes nvcc logs and outputs a figure with highest ranking translation + units. + + Usage: + python analyze_nvcc_log.py + cpp/scripts/analyze_nvcc_log.py + + Generate the nvcc log file by adding: + + list(APPEND RAFT_CUDA_FLAGS "--time=CMakeFiles/nvcc_compile_log.csv") + + to cpp/cmake/modules/ConfigureCUDA.cmake. + """) + + input_path = Path(sys.argv[1]) + if not input_path.exists(): + print(f"Path {input_path} does not exist.") + else: + main(input_path) From a88072cbe4784a25d6efef05d4de4528dd4fa5ae Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Wed, 29 Mar 2023 21:17:48 -0400 Subject: [PATCH 22/25] Update rapids version --- cpp/template/cmake/thirdparty/fetch_rapids.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/template/cmake/thirdparty/fetch_rapids.cmake b/cpp/template/cmake/thirdparty/fetch_rapids.cmake index 40ba83be9e..248f4f1af4 100644 --- a/cpp/template/cmake/thirdparty/fetch_rapids.cmake +++ b/cpp/template/cmake/thirdparty/fetch_rapids.cmake @@ -12,7 +12,7 @@ # the License. # Use this variable to update RAPIDS and RAFT versions -set(RAPIDS_VERSION "23.04") +set(RAPIDS_VERSION "23.06") if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/RAFT_RAPIDS.cmake) file(DOWNLOAD https://raw.githubusercontent.com/rapidsai/rapids-cmake/branch-${RAPIDS_VERSION}/RAPIDS.cmake From 5f0e66d1b18a8ce992db61add7e726ec4d5c2848 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Wed, 29 Mar 2023 21:38:40 -0400 Subject: [PATCH 23/25] Update pylibraft version --- dependencies.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.yaml b/dependencies.yaml index 6a07cd890d..0460e2dd81 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -278,7 +278,7 @@ dependencies: - ucx-proc=*=gpu - output_types: pyproject packages: - - pylibraft==23.4.* + - pylibraft==23.6.* test_python_common: common: - output_types: [conda, requirements, pyproject] From 3c5b8de4c791e2ab44cadbaa68acd29b8ef9dcbb Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Wed, 29 Mar 2023 21:48:31 -0400 Subject: [PATCH 24/25] Run dfg --- python/raft-dask/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/raft-dask/pyproject.toml b/python/raft-dask/pyproject.toml index 0ca9e7a876..1fb5aa8f7c 100644 --- a/python/raft-dask/pyproject.toml +++ b/python/raft-dask/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "joblib>=0.11", "numba>=0.49", "numpy>=1.21", - "pylibraft==23.4.*", + "pylibraft==23.6.*", "ucx-py==0.32.*", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ From 9bac6d53d32567d218b8188c88f4cdf665625fe4 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Thu, 30 Mar 2023 09:40:42 -0400 Subject: [PATCH 25/25] Fix dask versions in wheel build preinstallation --- .github/workflows/pr.yaml | 4 ++-- .github/workflows/test.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index bf080d6ad2..cf8f8cd4b5 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -105,7 +105,7 @@ jobs: build_type: pull-request package-name: raft_dask # Always want to test against latest dask/distributed. - test-before-amd64: "RAPIDS_PY_WHEEL_NAME=pylibraft_cu11 rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.04" - test-before-arm64: "RAPIDS_PY_WHEEL_NAME=pylibraft_cu11 rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.04" + test-before-amd64: "RAPIDS_PY_WHEEL_NAME=pylibraft_cu11 rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.06" + test-before-arm64: "RAPIDS_PY_WHEEL_NAME=pylibraft_cu11 rapids-download-wheels-from-s3 ./local-pylibraft-dep && pip install --no-deps ./local-pylibraft-dep/pylibraft*.whl && pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.06" test-unittest: "python -m pytest -v ./python/raft-dask/raft_dask/test" test-smoketest: "python ./ci/wheel_smoke_test_raft_dask.py" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f1207c3545..ebf596c958 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -51,6 +51,6 @@ jobs: date: ${{ inputs.date }} sha: ${{ inputs.sha }} package-name: raft_dask - test-before-amd64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.04" - test-before-arm64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.04" + test-before-amd64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.06" + test-before-arm64: "pip install git+https://github.com/dask/dask.git@main git+https://github.com/dask/distributed.git@main git+https://github.com/rapidsai/dask-cuda.git@branch-23.06" test-unittest: "python -m pytest -v ./python/raft-dask/raft_dask/test"