/** * Copyright (c) 2024, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek) * Copyright (c) 2023 by Contributors * @file cnumpy.h * @brief Numpy File Fetecher class. */ #ifdef HAVE_LIBRARY_LIBURING #include #endif // HAVE_LIBRARY_LIBURING #include #include #include #include #include #include #include #include #include #include #include #include #include namespace graphbolt { namespace storage { namespace { #ifdef HAVE_LIBRARY_LIBURING struct io_uring_queue_destroyer { int num_thread_; void operator()(::io_uring* queues) { if (!queues) return; for (int t = 0; t < num_thread_; t++) { // IO queue exit. ::io_uring_queue_exit(&queues[t]); } delete[] queues; } }; #endif // HAVE_LIBRARY_LIBURING } // namespace /** * @brief Disk Numpy Fetecher class. */ class OnDiskNpyArray : public torch::CustomClassHolder { // No user will need more than 1024 io_uring queues. using counting_semaphore_t = ::cuda::std::counting_semaphore<1024>; public: static constexpr int kGroupSize = 256; /** @brief Default constructor. */ OnDiskNpyArray() = default; /** * @brief Constructor with given file path and data type. * @param path Path to the on disk numpy file. * @param dtype Data type of numpy array. * * @return OnDiskNpyArray */ OnDiskNpyArray( std::string filename, torch::ScalarType dtype, const std::vector& shape, torch::optional num_threads); /** @brief Create a disk feature fetcher from numpy file. */ static c10::intrusive_ptr Create( std::string path, torch::ScalarType dtype, const std::vector& shape, torch::optional num_threads); /** @brief Deconstructor. */ ~OnDiskNpyArray(); /** * @brief Parses the header of a numpy file to extract feature information. **/ void ParseNumpyHeader(); /** * @brief Read disk numpy file based on given index and transform to * tensor. */ c10::intrusive_ptr> IndexSelect(torch::Tensor index); #ifdef HAVE_LIBRARY_LIBURING /** * @brief Index-select operation on an on-disk numpy array using IO Uring for * asynchronous I/O. * * This function performs index-select operation on an on-disk numpy array. It * uses IO Uring for asynchronous I/O to efficiently read data from disk. The * input tensor 'index' specifies the indices of features to select. The * function reads features corresponding to the indices from the disk and * returns a new tensor containing the selected features. * * @param index A 1D tensor containing the indices of features to select. * @return A tensor containing the selected features. * @throws std::runtime_error If index is out of range. */ c10::intrusive_ptr> IndexSelectIOUring( torch::Tensor index); torch::Tensor IndexSelectIOUringImpl(torch::Tensor index); #endif // HAVE_LIBRARY_LIBURING private: int64_t ReadBufferSizePerThread() const { return (aligned_length_ + block_size_) * kGroupSize * 8; } char* ReadBuffer(int thread_id) const { auto read_buffer_void_ptr = read_tensor_.data_ptr(); size_t read_buffer_size = read_tensor_.numel(); auto read_buffer = reinterpret_cast(std::align( block_size_, ReadBufferSizePerThread() * num_thread_, read_buffer_void_ptr, read_buffer_size)); TORCH_CHECK(read_buffer, "read_buffer allocation failed!"); return read_buffer + ReadBufferSizePerThread() * thread_id; } const std::string filename_; // Path to numpy file. int file_description_; // File description. int64_t block_size_; // Block size of the opened file. int64_t prefix_len_; // Length of head data in numpy file. const std::vector feature_dim_; // Shape of features, e.g. {N,M,K,L}. const torch::ScalarType dtype_; // Feature data type. const int64_t feature_size_; // Number of bytes of feature size. int64_t aligned_length_; // Aligned feature_size. int num_thread_; // Default thread number. torch::Tensor read_tensor_; // Provides temporary read buffer. #ifdef HAVE_LIBRARY_LIBURING static inline std::once_flag call_once_flag_; // Protect initialization of below. static inline int num_queues_; // Number of queues. static inline std::unique_ptr<::io_uring[], io_uring_queue_destroyer> io_uring_queue_; // io_uring queue. static inline counting_semaphore_t semaphore_{ 0}; // Control access to the io_uring queues. static inline std::mutex available_queues_mtx_; // available_queues_ mutex. static inline std::vector available_queues_; /** * @brief This class is meant to distribute the available read buffers and the * statically declared io_uring queues among the worker threads. */ class QueueAndBufferAcquirer { public: class UniqueQueue { public: UniqueQueue(int thread_id) : thread_id_(thread_id) {} UniqueQueue(const UniqueQueue&) = delete; UniqueQueue& operator=(const UniqueQueue&) = delete; /** * @brief Returns the queue back to the pool. */ ~UniqueQueue() { { // We give back the slot we used. std::lock_guard lock(available_queues_mtx_); available_queues_.push_back(thread_id_); } semaphore_.release(); } /** * @brief Returns the raw io_uring queue. */ ::io_uring& get() const { return io_uring_queue_[thread_id_]; } private: int thread_id_; }; QueueAndBufferAcquirer(OnDiskNpyArray* array) : array_(array) { semaphore_.acquire(); } ~QueueAndBufferAcquirer() { // If none of the worker threads acquire the semaphore, we make sure to // release the ticket taken in the constructor. if (!entering_first_.test_and_set(std::memory_order_relaxed)) { semaphore_.release(); } } /** * @brief Returns the secured io_uring queue and the read buffer as a pair. * The raw io_uring queue can be accessed by calling `.get()` on the * returned UniqueQueue object. * * @note The returned UniqueQueue object manages the lifetime of the * io_uring queue. Its destructor returns the queue back to the pool. */ std::pair get() { // We consume a slot from the semaphore to use a queue. if (entering_first_.test_and_set(std::memory_order_relaxed)) { semaphore_.acquire(); } const auto thread_id = [&] { std::lock_guard lock(available_queues_mtx_); TORCH_CHECK(!available_queues_.empty()); const auto thread_id = available_queues_.back(); available_queues_.pop_back(); return thread_id; }(); return { std::piecewise_construct, std::make_tuple(thread_id), std::make_tuple(array_->ReadBuffer(thread_id))}; } private: const OnDiskNpyArray* array_; std::atomic_flag entering_first_ = ATOMIC_FLAG_INIT; }; #endif // HAVE_LIBRARY_LIBURING }; } // namespace storage } // namespace graphbolt