chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:35:51 +08:00
commit c36a561cd8
2172 changed files with 455595 additions and 0 deletions
+287
View File
@@ -0,0 +1,287 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cache_policy.cc
* @brief Cache policy implementation on the CPU.
*/
#include "./cache_policy.h"
#include "./utils.h"
namespace graphbolt {
namespace storage {
template <typename CachePolicy>
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
BaseCachePolicy::QueryImpl(CachePolicy& policy, torch::Tensor keys) {
auto positions = torch::empty_like(
keys, keys.options()
.dtype(torch::kInt64)
.pinned_memory(utils::is_pinned(keys)));
auto indices = torch::empty_like(
keys, keys.options()
.dtype(torch::kInt64)
.pinned_memory(utils::is_pinned(keys)));
auto found_ptr_tensor = torch::empty_like(
keys, keys.options()
.dtype(torch::kInt64)
.pinned_memory(utils::is_pinned(keys)));
auto missing_keys = torch::empty_like(
keys, keys.options().pinned_memory(utils::is_pinned(keys)));
int64_t found_cnt = 0;
int64_t missing_cnt = keys.size(0);
AT_DISPATCH_INDEX_TYPES(
keys.scalar_type(), "BaseCachePolicy::Query::DispatchForKeys", ([&] {
auto keys_ptr = keys.data_ptr<index_t>();
auto positions_ptr = positions.data_ptr<int64_t>();
auto indices_ptr = indices.data_ptr<int64_t>();
static_assert(
sizeof(CacheKey*) == sizeof(int64_t), "You need 64 bit pointers.");
auto found_ptr =
reinterpret_cast<CacheKey**>(found_ptr_tensor.data_ptr<int64_t>());
auto missing_keys_ptr = missing_keys.data_ptr<index_t>();
for (int64_t i = 0; i < keys.size(0); i++) {
const auto key = keys_ptr[i];
auto cache_key_ptr = policy.Read(key);
if (cache_key_ptr) {
positions_ptr[found_cnt] = cache_key_ptr->getPos();
found_ptr[found_cnt] = cache_key_ptr;
indices_ptr[found_cnt++] = i;
} else {
indices_ptr[--missing_cnt] = i;
missing_keys_ptr[missing_cnt] = key;
}
}
}));
return {
positions.slice(0, 0, found_cnt), indices,
missing_keys.slice(0, found_cnt),
found_ptr_tensor.slice(0, 0, found_cnt)};
}
template <typename CachePolicy>
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
BaseCachePolicy::QueryAndReplaceImpl(CachePolicy& policy, torch::Tensor keys) {
auto positions = torch::empty_like(
keys, keys.options()
.dtype(torch::kInt64)
.pinned_memory(utils::is_pinned(keys)));
auto indices = torch::empty_like(
keys, keys.options()
.dtype(torch::kInt64)
.pinned_memory(utils::is_pinned(keys)));
auto pointers = torch::empty_like(keys, keys.options().dtype(torch::kInt64));
auto missing_keys = torch::empty_like(
keys, keys.options().pinned_memory(utils::is_pinned(keys)));
int64_t found_cnt = 0;
int64_t missing_cnt = keys.size(0);
AT_DISPATCH_INDEX_TYPES(
keys.scalar_type(), "BaseCachePolicy::Replace", ([&] {
auto keys_ptr = keys.data_ptr<index_t>();
auto positions_ptr = positions.data_ptr<int64_t>();
auto indices_ptr = indices.data_ptr<int64_t>();
static_assert(
sizeof(CacheKey*) == sizeof(int64_t), "You need 64 bit pointers.");
auto pointers_ptr =
reinterpret_cast<CacheKey**>(pointers.data_ptr<int64_t>());
auto missing_keys_ptr = missing_keys.data_ptr<index_t>();
set_t<int64_t> position_set;
position_set.reserve(keys.size(0));
// Query and Replace combined.
for (int64_t i = 0; i < keys.size(0); i++) {
const auto key = keys_ptr[i];
const auto [it, can_read] = policy.Emplace(key);
if (can_read) {
auto& cache_key = *it->second;
positions_ptr[found_cnt] = cache_key.getPos();
pointers_ptr[found_cnt] = &cache_key;
indices_ptr[found_cnt++] = i;
} else {
indices_ptr[--missing_cnt] = i;
missing_keys_ptr[missing_cnt] = key;
// Ensure that even if an offset is added, it stays negative.
auto position = std::numeric_limits<int64_t>::min();
CacheKey* cache_key_ptr = nullptr;
if (it->second == policy.getMapSentinelValue()) {
cache_key_ptr = policy.Insert(it);
position = cache_key_ptr->getPos();
TORCH_CHECK(
// We check for the uniqueness of the positions.
std::get<1>(position_set.insert(position)),
"Can't insert all, larger cache capacity is needed.");
}
positions_ptr[missing_cnt] = position;
pointers_ptr[missing_cnt] = cache_key_ptr;
}
}
}));
return {positions, indices, pointers, missing_keys.slice(0, found_cnt)};
}
template <typename CachePolicy>
std::tuple<torch::Tensor, torch::Tensor> BaseCachePolicy::ReplaceImpl(
CachePolicy& policy, torch::Tensor keys) {
auto positions = torch::empty_like(
keys, keys.options()
.dtype(torch::kInt64)
.pinned_memory(utils::is_pinned(keys)));
auto pointers = torch::empty_like(
keys, keys.options()
.dtype(torch::kInt64)
.pinned_memory(utils::is_pinned(keys)));
AT_DISPATCH_INDEX_TYPES(
keys.scalar_type(), "BaseCachePolicy::Replace", ([&] {
auto keys_ptr = keys.data_ptr<index_t>();
auto positions_ptr = positions.data_ptr<int64_t>();
static_assert(
sizeof(CacheKey*) == sizeof(int64_t), "You need 64 bit pointers.");
auto pointers_ptr =
reinterpret_cast<CacheKey**>(pointers.data_ptr<int64_t>());
set_t<int64_t> position_set;
position_set.reserve(keys.size(0));
for (int64_t i = 0; i < keys.size(0); i++) {
const auto key = keys_ptr[i];
// Ensure that even if an offset is added, it stays negative.
auto position = std::numeric_limits<int64_t>::min();
CacheKey* cache_key_ptr = nullptr;
const auto [it, _] = policy.Emplace(key);
if (it->second == policy.getMapSentinelValue()) {
cache_key_ptr = policy.Insert(it);
position = cache_key_ptr->getPos();
TORCH_CHECK(
// We check for the uniqueness of the positions.
std::get<1>(position_set.insert(position)),
"Can't insert all, larger cache capacity is needed.");
}
positions_ptr[i] = position;
pointers_ptr[i] = cache_key_ptr;
}
}));
return {positions, pointers};
}
template <bool write>
void BaseCachePolicy::ReadingWritingCompletedImpl(torch::Tensor pointers) {
static_assert(
sizeof(CacheKey*) == sizeof(int64_t), "You need 64 bit pointers.");
auto pointers_ptr =
reinterpret_cast<CacheKey**>(pointers.data_ptr<int64_t>());
for (int64_t i = 0; i < pointers.size(0); i++) {
const auto pointer = pointers_ptr[i];
if (!write || pointer) {
pointer->EndUse<write>();
}
}
}
void BaseCachePolicy::ReadingCompleted(torch::Tensor pointers) {
ReadingWritingCompletedImpl<false>(pointers);
}
void BaseCachePolicy::WritingCompleted(torch::Tensor pointers) {
ReadingWritingCompletedImpl<true>(pointers);
}
S3FifoCachePolicy::S3FifoCachePolicy(int64_t capacity)
: BaseCachePolicy(capacity),
ghost_queue_(capacity - capacity / 10),
small_queue_size_target_(capacity / 10),
small_queue_size_(0) {
TORCH_CHECK(small_queue_size_target_ > 0, "Capacity is not large enough.");
ghost_set_.reserve(ghost_queue_.Capacity());
key_to_cache_key_.reserve(kCapacityFactor * (capacity + 1));
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
S3FifoCachePolicy::Query(torch::Tensor keys) {
return QueryImpl(*this, keys);
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
S3FifoCachePolicy::QueryAndReplace(torch::Tensor keys) {
return QueryAndReplaceImpl(*this, keys);
}
std::tuple<torch::Tensor, torch::Tensor> S3FifoCachePolicy::Replace(
torch::Tensor keys) {
return ReplaceImpl(*this, keys);
}
SieveCachePolicy::SieveCachePolicy(int64_t capacity)
// Ensure that queue_ is constructed first before accessing its `.end()`.
: BaseCachePolicy(capacity), queue_(), hand_(queue_.end()) {
TORCH_CHECK(capacity > 0, "Capacity needs to be positive.");
key_to_cache_key_.reserve(kCapacityFactor * (capacity + 1));
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
SieveCachePolicy::Query(torch::Tensor keys) {
return QueryImpl(*this, keys);
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
SieveCachePolicy::QueryAndReplace(torch::Tensor keys) {
return QueryAndReplaceImpl(*this, keys);
}
std::tuple<torch::Tensor, torch::Tensor> SieveCachePolicy::Replace(
torch::Tensor keys) {
return ReplaceImpl(*this, keys);
}
LruCachePolicy::LruCachePolicy(int64_t capacity) : BaseCachePolicy(capacity) {
TORCH_CHECK(capacity > 0, "Capacity needs to be positive.");
key_to_cache_key_.reserve(kCapacityFactor * (capacity + 1));
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
LruCachePolicy::Query(torch::Tensor keys) {
return QueryImpl(*this, keys);
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
LruCachePolicy::QueryAndReplace(torch::Tensor keys) {
return QueryAndReplaceImpl(*this, keys);
}
std::tuple<torch::Tensor, torch::Tensor> LruCachePolicy::Replace(
torch::Tensor keys) {
return ReplaceImpl(*this, keys);
}
ClockCachePolicy::ClockCachePolicy(int64_t capacity)
: BaseCachePolicy(capacity) {
TORCH_CHECK(capacity > 0, "Capacity needs to be positive.");
key_to_cache_key_.reserve(kCapacityFactor * (capacity + 1));
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
ClockCachePolicy::Query(torch::Tensor keys) {
return QueryImpl(*this, keys);
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
ClockCachePolicy::QueryAndReplace(torch::Tensor keys) {
return QueryAndReplaceImpl(*this, keys);
}
std::tuple<torch::Tensor, torch::Tensor> ClockCachePolicy::Replace(
torch::Tensor keys) {
return ReplaceImpl(*this, keys);
}
} // namespace storage
} // namespace graphbolt
+673
View File
@@ -0,0 +1,673 @@
/**
* Copyright (c) 2024, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cache_policy.h
* @brief Cache policy implementation on the CPU.
*/
#ifndef GRAPHBOLT_CACHE_POLICY_H_
#define GRAPHBOLT_CACHE_POLICY_H_
#include <torch/custom_class.h>
#include <torch/torch.h>
#include <tsl/robin_map.h>
#include <tsl/robin_set.h>
#include <cuda/std/atomic>
#include <limits>
#include "./circular_queue.h"
namespace graphbolt {
namespace storage {
struct CacheKey {
auto getKey() const {
return (static_cast<int64_t>(key_higher_16_bits_) << 32) +
key_lower_32_bits_;
}
CacheKey(int64_t key) : CacheKey(key, std::numeric_limits<int64_t>::min()) {}
CacheKey(int64_t key, int64_t position)
: freq_(0),
// EndUse<true>() should be called to reset the reference count.
reference_count_(-1),
key_higher_16_bits_(key >> 32),
key_lower_32_bits_(key),
position_in_cache_(position) {
TORCH_CHECK(key == getKey());
static_assert(sizeof(CacheKey) == 2 * sizeof(int64_t));
}
CacheKey() = default;
auto getFreq() const { return freq_; }
auto getPos() const { return position_in_cache_; }
CacheKey& setPos(int64_t pos) {
position_in_cache_ = pos;
return *this;
}
CacheKey& Increment() {
freq_ = std::min(3, static_cast<int>(freq_ + 1));
return *this;
}
CacheKey& Decrement() {
freq_ = std::max(0, static_cast<int>(freq_ - 1));
return *this;
}
CacheKey& SetFreq() {
freq_ = 1;
return *this;
}
CacheKey& ResetFreq() {
freq_ = 0;
return *this;
}
CacheKey& StartRead() {
::cuda::std::atomic_ref ref(reference_count_);
// StartRead runs concurrently only with EndUse. EndUse does not need to see
// this modification at all. So we can use the relaxed memory order.
const auto old_val = ref.fetch_add(1, ::cuda::std::memory_order_relaxed);
TORCH_CHECK(
old_val < std::numeric_limits<int8_t>::max(),
"There are too many in-flight read requests to the same cache entry!");
return *this;
}
template <bool write>
CacheKey& EndUse() {
::cuda::std::atomic_ref ref(reference_count_);
// The EndUse operation needs to synchronize with the InUse operation. So we
// have an release-acquire ordering between the two.
// https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Acquire_ordering
if constexpr (write) {
ref.fetch_add(1, ::cuda::std::memory_order_release);
} else {
ref.fetch_add(-1, ::cuda::std::memory_order_release);
}
return *this;
}
bool InUse() const {
::cuda::std::atomic_ref ref(reference_count_);
// The operations after a call to this function need to happen after the
// load operation. Hence the acquire order.
return ref.load(::cuda::std::memory_order_acquire);
}
bool BeingWritten() const {
::cuda::std::atomic_ref ref(reference_count_);
// The only operation coming after this op is the StartRead operation. Since
// StartRead is a refcount increment operation, it is fine if we don't
// synchronize with EndUse ops.
return ref.load(::cuda::std::memory_order_relaxed) < 0;
}
friend std::ostream& operator<<(std::ostream& os, const CacheKey& key_ref) {
::cuda::std::atomic_ref ref(key_ref.reference_count_);
return os << '(' << key_ref.getKey() << ", " << key_ref.freq_ << ", "
<< key_ref.position_in_cache_ << ", " << ref.load() << ")";
}
private:
int8_t freq_;
// Negative values indicate writing while positive values indicate reading.
// Access only through an std::atomic_ref instance atomically.
int8_t reference_count_;
// Keys are restricted to be 48-bit unsigned integers.
uint16_t key_higher_16_bits_;
uint32_t key_lower_32_bits_;
int64_t position_in_cache_;
};
class BaseCachePolicy {
public:
BaseCachePolicy(int64_t capacity) : capacity_(capacity), cache_usage_(0) {}
BaseCachePolicy() = default;
/**
* @brief A virtual base class constructor ensures that the derived class
* destructor gets called.
*/
virtual ~BaseCachePolicy() = default;
/**
* @brief The policy query function.
* @param keys The keys to query the cache.
*
* @return (positions, indices, missing_keys, found_ptrs), where positions has
* the locations of the keys which were found in the cache, missing_keys has
* the keys that were not found and indices is defined such that
* keys[indices[:positions.size(0)]] gives us the keys for the found pointers
* and keys[indices[positions.size(0):]] is identical to missing_keys.
*/
virtual std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
Query(torch::Tensor keys) = 0;
/**
* @brief The policy query function.
* @param keys The keys to query the cache.
*
* @return (positions, indices, pointers, missing_keys), where positions has
* the locations of the keys which were emplaced into the cache, pointers
* point to the emplaced CacheKey pointers in the cache, missing_keys has the
* keys that were not found and just inserted and indices is defined such that
* keys[indices[:keys.size(0) - missing_keys.size(0)]] gives us the keys for
* the found keys and keys[indices[keys.size(0) - missing_keys.size(0):]] is
* identical to missing_keys.
*/
virtual std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
QueryAndReplace(torch::Tensor keys) = 0;
/**
* @brief The policy replace function.
* @param keys The keys to query the cache.
*
* @return (positions, pointers), where positions has the locations of the
* replaced entries and pointers point to their CacheKey pointers in the
* cache.
*/
virtual std::tuple<torch::Tensor, torch::Tensor> Replace(
torch::Tensor keys) = 0;
/**
* @brief A reader has finished reading these keys, so they can be evicted.
* @param pointers The CacheKey pointers in the cache to unmark.
*/
static void ReadingCompleted(torch::Tensor pointers);
/**
* @brief A writer has finished writing these keys, so they can be evicted.
* @param pointers The CacheKey pointers in the cache to unmark.
*/
static void WritingCompleted(torch::Tensor pointers);
protected:
template <typename K, typename V>
using map_t = tsl::robin_map<K, V>;
template <typename K>
using set_t = tsl::robin_set<K>;
template <typename iterator>
static auto& mutable_value_ref(iterator it) {
return it.value();
}
static constexpr int kCapacityFactor = 2;
template <typename CachePolicy>
static std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
QueryImpl(CachePolicy& policy, torch::Tensor keys);
template <typename CachePolicy>
static std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
QueryAndReplaceImpl(CachePolicy& policy, torch::Tensor keys);
template <typename CachePolicy>
static std::tuple<torch::Tensor, torch::Tensor> ReplaceImpl(
CachePolicy& policy, torch::Tensor keys);
template <typename T>
static void MoveToFront(
std::list<T>& from, std::list<T>& to,
typename std::list<T>::iterator it) {
std::list<T> temp;
// Transfer the element to temp to keep references valid.
auto next_it = it;
std::advance(next_it, 1);
temp.splice(temp.begin(), from, it, next_it);
// Move the element to the beginning of the queue.
to.splice(to.begin(), temp);
// The iterators and references are not invalidated.
TORCH_INTERNAL_ASSERT_DEBUG_ONLY(it == to.begin());
}
int64_t capacity_;
int64_t cache_usage_;
private:
template <bool write>
static void ReadingWritingCompletedImpl(torch::Tensor pointers);
};
/**
* @brief S3FIFO is a simple, scalable FIFObased algorithm with three static
* queues (S3-FIFO). https://dl.acm.org/doi/pdf/10.1145/3600006.3613147
**/
class S3FifoCachePolicy : public BaseCachePolicy {
public:
using map_iterator = map_t<int64_t, CacheKey*>::iterator;
/**
* @brief Constructor for the S3FifoCachePolicy class.
*
* @param capacity The capacity of the cache in terms of # elements.
*/
S3FifoCachePolicy(int64_t capacity);
S3FifoCachePolicy() = default;
S3FifoCachePolicy(S3FifoCachePolicy&&) = default;
virtual ~S3FifoCachePolicy() = default;
/**
* @brief See BaseCachePolicy::Query.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor> Query(
torch::Tensor keys);
/**
* @brief See BaseCachePolicy::QueryAndReplace.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
QueryAndReplace(torch::Tensor keys);
/**
* @brief See BaseCachePolicy::Replace.
*/
std::tuple<torch::Tensor, torch::Tensor> Replace(torch::Tensor keys);
CacheKey* Read(int64_t key) {
auto it = key_to_cache_key_.find(key);
if (it != key_to_cache_key_.end()) {
auto& cache_key = it->second->Increment();
if (!cache_key.BeingWritten()) {
return &cache_key.StartRead();
}
}
return nullptr;
}
auto getMapSentinelValue() const { return nullptr; }
std::pair<map_iterator, bool> Emplace(int64_t key) {
auto [it, inserted] = key_to_cache_key_.emplace(key, getMapSentinelValue());
bool readable = false;
if (!inserted) {
auto& cache_key = it->second->Increment();
if (!cache_key.BeingWritten()) {
cache_key.StartRead();
readable = true;
}
}
return {it, readable};
}
CacheKey* Insert(map_iterator it) {
const auto key = it->first;
const auto in_ghost_queue = ghost_set_.erase(key);
auto& queue = in_ghost_queue ? main_queue_ : small_queue_;
queue.push_front(CacheKey(key));
small_queue_size_ += 1 - in_ghost_queue;
auto cache_key_ptr = &queue.front();
mutable_value_ref(it) = cache_key_ptr;
return &cache_key_ptr->setPos(Evict());
}
private:
int64_t EvictMainQueue() {
while (true) {
auto& evicted = main_queue_.back();
if (evicted.getFreq() > 0 || evicted.InUse()) {
evicted.Decrement();
auto it = main_queue_.end();
std::advance(it, -1);
MoveToFront(main_queue_, main_queue_, it);
} else {
key_to_cache_key_.erase(evicted.getKey());
const auto evicted_pos = evicted.getPos();
main_queue_.pop_back();
return evicted_pos;
}
}
}
int64_t EvictSmallQueue() {
while (small_queue_size_ > small_queue_size_target_) {
--small_queue_size_;
auto& evicted = small_queue_.back();
if (evicted.getFreq() > 0 || evicted.InUse()) {
evicted.ResetFreq();
auto it = small_queue_.end();
std::advance(it, -1);
MoveToFront(small_queue_, main_queue_, it);
} else {
const auto evicted_key = evicted.getKey();
key_to_cache_key_.erase(evicted_key);
const auto evicted_pos = evicted.getPos();
small_queue_.pop_back();
if (ghost_queue_.IsFull()) {
ghost_set_.erase(ghost_queue_.Pop());
}
ghost_set_.insert(evicted_key);
ghost_queue_.Push(evicted_key);
return evicted_pos;
}
}
return -1;
}
int64_t Evict() {
// If the cache has space, get an unused slot otherwise perform eviction.
if (cache_usage_ < capacity_) return cache_usage_++;
const auto pos = EvictSmallQueue();
return pos >= 0 ? pos : EvictMainQueue();
}
std::list<CacheKey> small_queue_, main_queue_;
CircularQueue<int64_t> ghost_queue_;
size_t small_queue_size_target_;
// std::list<>::size() is O(N) before the CXX11 ABI which torch enforces.
size_t small_queue_size_;
set_t<int64_t> ghost_set_;
map_t<int64_t, CacheKey*> key_to_cache_key_;
};
/**
* @brief SIEVE is a simple, scalable FIFObased algorithm with a single static
* queue. https://www.usenix.org/system/files/nsdi24-zhang-yazhuo.pdf
**/
class SieveCachePolicy : public BaseCachePolicy {
public:
using map_iterator = map_t<int64_t, CacheKey*>::iterator;
/**
* @brief Constructor for the SieveCachePolicy class.
*
* @param capacity The capacity of the cache in terms of # elements.
*/
SieveCachePolicy(int64_t capacity);
SieveCachePolicy() = default;
virtual ~SieveCachePolicy() = default;
/**
* @brief See BaseCachePolicy::Query.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor> Query(
torch::Tensor keys);
/**
* @brief See BaseCachePolicy::QueryAndReplace.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
QueryAndReplace(torch::Tensor keys);
/**
* @brief See BaseCachePolicy::Replace.
*/
std::tuple<torch::Tensor, torch::Tensor> Replace(torch::Tensor keys);
CacheKey* Read(int64_t key) {
auto it = key_to_cache_key_.find(key);
if (it != key_to_cache_key_.end()) {
auto& cache_key = it->second->SetFreq();
if (!cache_key.BeingWritten()) {
return &cache_key.StartRead();
}
}
return nullptr;
}
auto getMapSentinelValue() const { return nullptr; }
std::pair<map_iterator, bool> Emplace(int64_t key) {
auto [it, inserted] = key_to_cache_key_.emplace(key, getMapSentinelValue());
bool readable = false;
if (!inserted) {
auto& cache_key = it->second->SetFreq();
if (!cache_key.BeingWritten()) {
cache_key.StartRead();
readable = true;
}
}
return {it, readable};
}
CacheKey* Insert(map_iterator it) {
const auto key = it->first;
queue_.push_front(CacheKey(key));
auto cache_key_ptr = &queue_.front();
mutable_value_ref(it) = cache_key_ptr;
return &cache_key_ptr->setPos(Evict());
}
private:
int64_t Evict() {
// If the cache has space, get an unused slot otherwise perform eviction.
if (cache_usage_ < capacity_) return cache_usage_++;
--hand_;
while (hand_->getFreq() || hand_->InUse()) {
hand_->ResetFreq();
if (hand_ == queue_.begin()) hand_ = queue_.end();
--hand_;
}
key_to_cache_key_.erase(hand_->getKey());
const auto pos = hand_->getPos();
const auto temp = hand_;
if (hand_ == queue_.begin()) {
hand_ = queue_.end();
} else {
++hand_;
}
queue_.erase(temp);
return pos;
}
std::list<CacheKey> queue_;
decltype(queue_)::iterator hand_;
map_t<int64_t, CacheKey*> key_to_cache_key_;
};
/**
* @brief LeastRecentlyUsed is a simple, scalable FIFObased algorithm with a
* single static queue.
**/
class LruCachePolicy : public BaseCachePolicy {
public:
using map_iterator = map_t<int64_t, std::list<CacheKey>::iterator>::iterator;
/**
* @brief Constructor for the LruCachePolicy class.
*
* @param capacity The capacity of the cache in terms of # elements.
*/
LruCachePolicy(int64_t capacity);
LruCachePolicy() = default;
virtual ~LruCachePolicy() = default;
/**
* @brief See BaseCachePolicy::Query.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor> Query(
torch::Tensor keys);
/**
* @brief See BaseCachePolicy::QueryAndReplace.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
QueryAndReplace(torch::Tensor keys);
/**
* @brief See BaseCachePolicy::Replace.
*/
std::tuple<torch::Tensor, torch::Tensor> Replace(torch::Tensor keys);
CacheKey* Read(int64_t key) {
auto it = key_to_cache_key_.find(key);
if (it != key_to_cache_key_.end()) {
auto& cache_key = *it->second;
MoveToFront(queue_, queue_, it->second);
if (!cache_key.BeingWritten()) {
return &cache_key.StartRead();
}
}
return nullptr;
}
auto getMapSentinelValue() { return queue_.end(); }
std::pair<map_iterator, bool> Emplace(int64_t key) {
auto [it, inserted] = key_to_cache_key_.emplace(key, getMapSentinelValue());
bool readable = false;
if (!inserted) {
auto& cache_key = *it->second;
MoveToFront(queue_, queue_, it->second);
if (!cache_key.BeingWritten()) {
cache_key.StartRead();
readable = true;
}
}
return {it, readable};
}
CacheKey* Insert(map_iterator it) {
const auto key = it->first;
queue_.push_front(CacheKey(key));
mutable_value_ref(it) = queue_.begin();
auto cache_key_ptr = &queue_.front();
return &cache_key_ptr->setPos(Evict());
}
private:
int64_t Evict() {
// If the cache has space, get an unused slot otherwise perform eviction.
if (cache_usage_ < capacity_) return cache_usage_++;
// Do not evict items that are still in use.
while (queue_.back().InUse()) {
auto it = queue_.end();
std::advance(it, -1);
// Move the last element to the front without invalidating references.
MoveToFront(queue_, queue_, it);
}
const auto& cache_key = queue_.back();
key_to_cache_key_.erase(cache_key.getKey());
const auto pos = cache_key.getPos();
queue_.pop_back();
return pos;
}
std::list<CacheKey> queue_;
map_t<int64_t, decltype(queue_)::iterator> key_to_cache_key_;
};
/**
* @brief Clock (FIFO-Reinsertion) is a simple, scalable FIFObased algorithm
* with a single static queue.
* https://people.csail.mit.edu/saltzer/Multics/MHP-Saltzer-060508/bookcases/M00s/M0104%20074-12).PDF
**/
class ClockCachePolicy : public BaseCachePolicy {
public:
using map_iterator = map_t<int64_t, CacheKey*>::iterator;
/**
* @brief Constructor for the ClockCachePolicy class.
*
* @param capacity The capacity of the cache in terms of # elements.
*/
ClockCachePolicy(int64_t capacity);
ClockCachePolicy() = default;
ClockCachePolicy(ClockCachePolicy&&) = default;
virtual ~ClockCachePolicy() = default;
/**
* @brief See BaseCachePolicy::Query.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor> Query(
torch::Tensor keys);
/**
* @brief See BaseCachePolicy::QueryAndReplace.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
QueryAndReplace(torch::Tensor keys);
/**
* @brief See BaseCachePolicy::Replace.
*/
std::tuple<torch::Tensor, torch::Tensor> Replace(torch::Tensor keys);
CacheKey* Read(int64_t key) {
auto it = key_to_cache_key_.find(key);
if (it != key_to_cache_key_.end()) {
auto& cache_key = it->second->SetFreq();
if (!cache_key.BeingWritten()) {
return &cache_key.StartRead();
}
}
return nullptr;
}
auto getMapSentinelValue() const { return nullptr; }
std::pair<map_iterator, bool> Emplace(int64_t key) {
auto [it, inserted] = key_to_cache_key_.emplace(key, getMapSentinelValue());
bool readable = false;
if (!inserted) {
auto& cache_key = it->second->SetFreq();
if (!cache_key.BeingWritten()) {
cache_key.StartRead();
readable = true;
}
}
return {it, readable};
}
CacheKey* Insert(map_iterator it) {
const auto key = it->first;
queue_.push_front(CacheKey(key));
auto cache_key_ptr = &queue_.front();
mutable_value_ref(it) = cache_key_ptr;
return &cache_key_ptr->setPos(Evict());
}
private:
int64_t Evict() {
// If the cache has space, get an unused slot otherwise perform eviction.
if (cache_usage_ < capacity_) return cache_usage_++;
while (true) {
auto& cache_key = queue_.back();
if (cache_key.getFreq() || cache_key.InUse()) {
cache_key.ResetFreq();
auto it = queue_.end();
std::advance(it, -1);
MoveToFront(queue_, queue_, it);
} else {
key_to_cache_key_.erase(cache_key.getKey());
const auto evicted_pos = cache_key.getPos();
queue_.pop_back();
return evicted_pos;
}
}
}
std::list<CacheKey> queue_;
map_t<int64_t, CacheKey*> key_to_cache_key_;
};
} // namespace storage
} // namespace graphbolt
#endif // GRAPHBOLT_CACHE_POLICY_H_
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2024, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file circular_queue.h
* @brief Circular queue implementation.
*/
#ifndef GRAPHBOLT_CIRCULAR_QUEUE_H_
#define GRAPHBOLT_CIRCULAR_QUEUE_H_
#include <memory>
namespace graphbolt {
template <typename T>
struct CircularQueue {
CircularQueue(const int64_t capacity)
: tail_(0),
head_(0),
// + 1 is needed to be able to differentiate empty and full states.
capacity_(capacity + 1),
data_{new T[capacity + 1]} {}
CircularQueue() = default;
T* Push(const T& x) {
auto insert_ptr = &data_[PostIncrement(tail_)];
*insert_ptr = x;
return insert_ptr;
}
T Pop() { return data_[PostIncrement(head_)]; }
void PopN(int64_t N) {
head_ += N;
if (head_ >= capacity_) head_ -= capacity_;
}
auto Clear() { head_ = tail_; }
T& Front() const { return data_[head_]; }
bool IsFull() const {
const auto diff = tail_ + 1 - head_;
return diff == 0 || diff == capacity_;
}
auto Size() const {
auto diff = tail_ - head_;
if (diff < 0) diff += capacity_;
return diff;
}
friend std::ostream& operator<<(
std::ostream& os, const CircularQueue& queue) {
for (auto i = queue.head_; i != queue.tail_; queue.PostIncrement(i)) {
os << queue.data_[i] << ", ";
}
return os << "\n";
}
bool IsEmpty() const { return tail_ == head_; }
auto Capacity() const { return capacity_ - 1; }
private:
int64_t PostIncrement(int64_t& i) const {
const auto ret = i++;
if (i >= capacity_) i -= capacity_;
return ret;
}
int64_t tail_;
int64_t head_;
int64_t capacity_;
std::unique_ptr<T[]> data_;
};
} // namespace graphbolt
#endif // GRAPHBOLT_CIRCULAR_QUEUE_H_
+320
View File
@@ -0,0 +1,320 @@
/**
* Copyright (c) 2024, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* Copyright (c) 2023 by Contributors
* @file cnumpy.cc
* @brief Numpy File Fetecher class.
*/
#include "./cnumpy.h"
#include "./io_uring.h"
#ifdef HAVE_LIBRARY_LIBURING
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
#include <graphbolt/async.h>
#include <torch/torch.h>
#include <atomic>
#include <cstring>
#include <fstream>
#include <memory>
#include <numeric>
#include <stdexcept>
#include <vector>
#include "./circular_queue.h"
#include "./utils.h"
namespace graphbolt {
namespace storage {
OnDiskNpyArray::OnDiskNpyArray(
std::string filename, torch::ScalarType dtype,
const std::vector<int64_t> &shape, torch::optional<int64_t> num_threads)
: filename_(filename),
feature_dim_(shape),
dtype_(dtype),
feature_size_(std::accumulate(
shape.begin() + 1, shape.end(), c10::elementSize(dtype),
std::multiplies<int64_t>())) {
#ifndef __linux__
throw std::runtime_error(
"OnDiskNpyArray is not supported on non-Linux systems.");
#endif
#ifdef HAVE_LIBRARY_LIBURING
ParseNumpyHeader();
file_description_ = ::open(filename.c_str(), O_RDONLY | O_DIRECT);
if (file_description_ < 0) {
throw std::runtime_error("npy_load: Unable to open file " + filename);
}
struct stat st;
TORCH_CHECK(::fstat(file_description_, &st) == 0);
const auto file_size = st.st_size;
block_size_ = st.st_blksize;
TORCH_CHECK(file_size - prefix_len_ >= feature_dim_[0] * feature_size_);
// The minimum page size to contain one feature.
aligned_length_ = (feature_size_ + block_size_ - 1) & ~(block_size_ - 1);
std::call_once(call_once_flag_, [&] {
// Get system max interop thread count.
num_queues_ =
io_uring::num_threads.value_or(torch::get_num_interop_threads());
TORCH_CHECK(num_queues_ > 0, "A positive # queues is required.");
io_uring_queue_ = std::unique_ptr<::io_uring[], io_uring_queue_destroyer>(
new ::io_uring[num_queues_], io_uring_queue_destroyer{num_queues_});
TORCH_CHECK(num_queues_ <= counting_semaphore_t::max());
semaphore_.release(num_queues_);
available_queues_.reserve(num_queues_);
// Init io_uring queue.
for (int64_t t = 0; t < num_queues_; t++) {
available_queues_.push_back(t);
TORCH_CHECK(
::io_uring_queue_init(2 * kGroupSize, &io_uring_queue_[t], 0) == 0);
// We have allocated 2 * kGroupSize submission queue entries and
// 4 * kGroupSize completion queue entries after this call.
}
});
num_thread_ = std::min(
static_cast<int64_t>(num_queues_), num_threads.value_or(num_queues_));
TORCH_CHECK(num_thread_ > 0, "A positive # threads is required.");
// We allocate buffers for each existing queue because we might get assigned
// any queue in range [0, num_queues_).
read_tensor_ = torch::empty(
ReadBufferSizePerThread() * num_queues_ + block_size_ - 1,
torch::TensorOptions().dtype(torch::kInt8).device(torch::kCPU));
#else
throw std::runtime_error("DiskBasedFeature is not available now.");
#endif // HAVE_LIBRARY_LIBURING
}
c10::intrusive_ptr<OnDiskNpyArray> OnDiskNpyArray::Create(
std::string path, torch::ScalarType dtype,
const std::vector<int64_t> &shape, torch::optional<int64_t> num_threads) {
return c10::make_intrusive<OnDiskNpyArray>(path, dtype, shape, num_threads);
}
OnDiskNpyArray::~OnDiskNpyArray() {
#ifdef HAVE_LIBRARY_LIBURING
TORCH_CHECK(::close(file_description_) == 0);
#endif // HAVE_LIBRARY_LIBURING
}
void OnDiskNpyArray::ParseNumpyHeader() {
// Parse numpy file header to get basic info of feature.
// Get file prefix length.
std::ifstream file(filename_);
if (!file.is_open()) {
throw std::runtime_error(
"ParseNumpyHeader: Unable to open file " + filename_);
}
std::string header;
std::getline(file, header);
// Get prefix length for computing feature offset,
// add one for new-line character.
prefix_len_ = header.size() + 1;
}
c10::intrusive_ptr<Future<torch::Tensor>> OnDiskNpyArray::IndexSelect(
torch::Tensor index) {
#ifdef HAVE_LIBRARY_LIBURING
return IndexSelectIOUring(index);
#else
TORCH_CHECK(false, "OnDiskNpyArray is not supported on non-Linux systems.");
return {};
#endif // HAVE_LIBRARY_LIBURING
}
class ReadRequest {
public:
char *destination_;
int64_t read_len_;
int64_t offset_;
int64_t block_size_;
char *aligned_read_buffer_;
auto AlignedOffset() const { return offset_ & ~(block_size_ - 1); }
auto ReadBuffer() const {
return aligned_read_buffer_ + offset_ - AlignedOffset();
}
auto AlignedReadSize() const {
const int64_t end_offset = offset_ + read_len_;
const int64_t aligned_end_offset =
(end_offset + block_size_ - 1) & ~(block_size_ - 1);
return aligned_end_offset - AlignedOffset();
}
auto MinimumReadSize() const { return offset_ + read_len_ - AlignedOffset(); }
};
#ifdef HAVE_LIBRARY_LIBURING
torch::Tensor OnDiskNpyArray::IndexSelectIOUringImpl(torch::Tensor index) {
std::vector<int64_t> shape(index.sizes().begin(), index.sizes().end());
shape.insert(shape.end(), feature_dim_.begin() + 1, feature_dim_.end());
auto result = torch::empty(
shape, index.options()
.dtype(dtype_)
.layout(torch::kStrided)
.pinned_memory(utils::is_pinned(index))
.requires_grad(false));
auto result_buffer = reinterpret_cast<char *>(result.data_ptr());
// Indicator for index error.
std::atomic<int> error_flag{};
std::atomic<int64_t> work_queue{};
// Construct a QueueAndBufferAcquirer object so that the worker threads can
// share the available queues and buffers.
QueueAndBufferAcquirer queue_source(this);
graphbolt::parallel_for_each_interop(0, num_thread_, 1, [&](int) {
// The completion queue might contain 4 * kGroupSize while we may submit
// 4 * kGroupSize more. No harm in overallocation here.
CircularQueue<ReadRequest> read_queue(8 * kGroupSize);
int64_t num_submitted = 0;
int64_t num_completed = 0;
auto [acquired_queue_handle, read_buffer_source2] = queue_source.get();
auto &io_uring_queue = acquired_queue_handle.get();
// Capturing structured binding is available only in C++20, so we rename.
auto read_buffer_source = read_buffer_source2;
auto submit_fn = [&](int64_t submission_minimum_batch_size) {
if (read_queue.Size() < submission_minimum_batch_size) return;
TORCH_CHECK( // Check for sqe overflow.
read_queue.Size() <= 2 * kGroupSize);
TORCH_CHECK( // Check for cqe overflow.
read_queue.Size() + num_submitted - num_completed <= 4 * kGroupSize);
// Submit and wait for the reads.
while (!read_queue.IsEmpty()) {
const auto submitted = ::io_uring_submit(&io_uring_queue);
TORCH_CHECK(submitted >= 0);
num_submitted += submitted;
// Pop the submitted entries from the queue.
read_queue.PopN(submitted);
}
};
for (int64_t read_buffer_slot = 0; true;) {
auto request_read_buffer = [&]() {
return read_buffer_source + (aligned_length_ + block_size_) *
(read_buffer_slot++ % (8 * kGroupSize));
};
const auto num_requested_items = std::max(
std::min(
// The condition not to overflow the completion queue.
2 * kGroupSize -
(read_queue.Size() + num_submitted - num_completed),
// The condition not to overflow the submission queue.
kGroupSize - read_queue.Size()),
int64_t{});
const auto begin =
work_queue.fetch_add(num_requested_items, std::memory_order_relaxed);
if ((begin >= index.numel() && read_queue.IsEmpty() &&
num_completed >= num_submitted) ||
// Even when we encounter out of bounds index (error_flag == 1), we
// continue. We want to ensure the reads in flight successfully
// complete to avoid the instability due to incompleted reads.
error_flag.load(std::memory_order_relaxed) > 1)
break;
const auto end = std::min(begin + num_requested_items, index.numel());
AT_DISPATCH_INDEX_TYPES(
index.scalar_type(), "IndexSelectIOUring", ([&] {
auto index_data = index.data_ptr<index_t>();
for (int64_t i = begin; i < end; ++i) {
int64_t feature_id = index_data[i];
if (feature_id < 0) feature_id += feature_dim_[0];
if (feature_id < 0 || feature_id >= feature_dim_[0]) {
error_flag.store(1, std::memory_order_relaxed);
// Simply skip the out of bounds index.
continue;
}
// calculate offset of the feature.
const int64_t offset = feature_id * feature_size_ + prefix_len_;
ReadRequest req{
result_buffer + feature_size_ * i, feature_size_, offset,
block_size_, request_read_buffer()};
// Put requests into io_uring queue.
struct io_uring_sqe *sqe = io_uring_get_sqe(&io_uring_queue);
TORCH_CHECK(sqe);
io_uring_sqe_set_data(sqe, read_queue.Push(req));
io_uring_prep_read(
sqe, file_description_, req.aligned_read_buffer_,
req.AlignedReadSize(), req.AlignedOffset());
submit_fn(kGroupSize);
}
}));
submit_fn(1); // Submit all sqes.
// Wait for the reads; completion queue entries.
struct io_uring_cqe *cqe;
TORCH_CHECK(num_submitted - num_completed <= 2 * kGroupSize);
TORCH_CHECK(
::io_uring_wait_cqe_nr(
&io_uring_queue, &cqe, num_submitted - num_completed) == 0);
// Check the reads and abort on failure.
int num_cqes_seen = 0;
unsigned head;
io_uring_for_each_cqe(&io_uring_queue, head, cqe) {
const auto &req =
*reinterpret_cast<ReadRequest *>(io_uring_cqe_get_data(cqe));
auto actual_read_len = cqe->res;
if (actual_read_len < 0) {
error_flag.store(actual_read_len, std::memory_order_relaxed);
break;
}
const auto remaining_read_len =
std::max(req.MinimumReadSize() - actual_read_len, int64_t{});
const auto remaining_useful_read_len =
std::min(remaining_read_len, req.read_len_);
const auto useful_read_len = req.read_len_ - remaining_useful_read_len;
if (remaining_read_len) {
// Remaining portion will be read as part of the next batch.
ReadRequest rest{
req.destination_ + useful_read_len, remaining_useful_read_len,
req.offset_ + useful_read_len, block_size_,
request_read_buffer()};
// Put requests into io_uring queue.
struct io_uring_sqe *sqe = io_uring_get_sqe(&io_uring_queue);
TORCH_CHECK(sqe);
io_uring_sqe_set_data(sqe, read_queue.Push(rest));
io_uring_prep_read(
sqe, file_description_, rest.aligned_read_buffer_,
rest.AlignedReadSize(), rest.AlignedOffset());
submit_fn(kGroupSize);
}
// Copy results into result_buffer.
std::memcpy(req.destination_, req.ReadBuffer(), useful_read_len);
num_cqes_seen++;
}
// Move the head pointer of completion queue.
io_uring_cq_advance(&io_uring_queue, num_cqes_seen);
num_completed += num_cqes_seen;
}
});
const auto ret_val = error_flag.load(std::memory_order_relaxed);
switch (ret_val) {
case 0: // Successful.
return result;
case 1:
throw std::out_of_range("IndexError: Index out of range.");
default:
throw std::runtime_error(
"io_uring error with errno: " + std::to_string(-ret_val));
}
}
c10::intrusive_ptr<Future<torch::Tensor>> OnDiskNpyArray::IndexSelectIOUring(
torch::Tensor index) {
return async([=, this] { return IndexSelectIOUringImpl(index); });
}
#endif // HAVE_LIBRARY_LIBURING
} // namespace storage
} // namespace graphbolt
+228
View File
@@ -0,0 +1,228 @@
/**
* 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 <liburing.h>
#endif // HAVE_LIBRARY_LIBURING
#include <graphbolt/async.h>
#include <torch/script.h>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cuda/std/semaphore>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
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<int64_t>& shape, torch::optional<int64_t> num_threads);
/** @brief Create a disk feature fetcher from numpy file. */
static c10::intrusive_ptr<OnDiskNpyArray> Create(
std::string path, torch::ScalarType dtype,
const std::vector<int64_t>& shape, torch::optional<int64_t> 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<Future<torch::Tensor>> 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<Future<torch::Tensor>> 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<char*>(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<int64_t>
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<int> 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<UniqueQueue, char*> 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
+219
View File
@@ -0,0 +1,219 @@
/**
* Copyright (c) 2023 by Contributors
* @file concurrent_id_hash_map.cc
* @brief Class about id hash map.
*/
#include "concurrent_id_hash_map.h"
#ifdef _MSC_VER
#include <intrin.h>
#endif // _MSC_VER
#include <cmath>
#include <cuda/std/atomic>
#include <numeric>
namespace {
static constexpr int64_t kEmptyKey = -1;
static constexpr int kGrainSize = 256;
// The formula is established from experience which is used to get the hashmap
// size from the input array size.
inline size_t GetMapSize(size_t num) {
size_t capacity = 1;
return capacity << static_cast<size_t>(1 + std::log2(num * 3));
}
} // namespace
namespace graphbolt {
namespace sampling {
template <typename IdType>
ConcurrentIdHashMap<IdType>::ConcurrentIdHashMap(
const torch::Tensor& ids, size_t num_seeds) {
const IdType* ids_data = ids.data_ptr<IdType>();
const size_t num_ids = static_cast<size_t>(ids.size(0));
size_t capacity = GetMapSize(num_ids);
mask_ = static_cast<IdType>(capacity - 1);
hash_map_ =
torch::full({static_cast<int64_t>(capacity * 2)}, -1, ids.options());
// This code block is to fill the ids into hash_map_.
unique_ids_ = torch::empty_like(ids);
IdType* unique_ids_data = unique_ids_.data_ptr<IdType>();
// Insert all ids into the hash map.
torch::parallel_for(0, num_ids, kGrainSize, [&](int64_t s, int64_t e) {
for (int64_t i = s; i < e; i++) {
InsertAndSetMin(ids_data[i], static_cast<IdType>(i));
}
});
// Place the first `num_seeds` ids.
unique_ids_.slice(0, 0, num_seeds) = ids.slice(0, 0, num_seeds);
auto valid_tensor = torch::empty(num_ids, ids.options().dtype(torch::kInt8));
auto valid = valid_tensor.data_ptr<int8_t>();
const int64_t num_threads = torch::get_num_threads();
std::vector<size_t> block_offset(num_threads + 1, 0);
// Count the valid numbers in each thread.
torch::parallel_for(
num_seeds, num_ids, kGrainSize, [&](int64_t s, int64_t e) {
size_t count = 0;
for (int64_t i = s; i < e; i++) {
if (MapId(ids_data[i]) == i) {
count++;
valid[i] = 1;
} else {
valid[i] = 0;
}
}
auto thread_id = torch::get_thread_num();
block_offset[thread_id + 1] = count;
});
// Get ExclusiveSum of each block.
std::partial_sum(
block_offset.begin() + 1, block_offset.end(), block_offset.begin() + 1);
unique_ids_ = unique_ids_.slice(0, 0, num_seeds + block_offset.back());
// Get unique array from ids and set value for hash map.
torch::parallel_for(
num_seeds, num_ids, kGrainSize, [&](int64_t s, int64_t e) {
auto thread_id = torch::get_thread_num();
auto pos = block_offset[thread_id] + num_seeds;
for (int64_t i = s; i < e; i++) {
if (valid[i]) {
unique_ids_data[pos] = ids_data[i];
Set(ids_data[i], pos);
pos = pos + 1;
}
}
});
}
template <typename IdType>
torch::Tensor ConcurrentIdHashMap<IdType>::MapIds(
const torch::Tensor& ids) const {
const IdType* ids_data = ids.data_ptr<IdType>();
torch::Tensor new_ids = torch::empty_like(ids);
auto num_ids = new_ids.size(0);
IdType* values_data = new_ids.data_ptr<IdType>();
torch::parallel_for(0, num_ids, kGrainSize, [&](int64_t s, int64_t e) {
for (int64_t i = s; i < e; i++) {
values_data[i] = MapId(ids_data[i]);
}
});
return new_ids;
}
template <typename IdType>
constexpr IdType getKeyIndex(IdType pos) {
return 2 * pos;
}
template <typename IdType>
constexpr IdType getValueIndex(IdType pos) {
return 2 * pos + 1;
}
template <typename IdType>
inline void ConcurrentIdHashMap<IdType>::Next(
IdType* pos, IdType* delta) const {
// Use Quadric probing.
*pos = (*pos + (*delta) * (*delta)) & mask_;
*delta = *delta + 1;
}
template <typename IdType>
inline IdType ConcurrentIdHashMap<IdType>::MapId(IdType id) const {
IdType pos = (id & mask_), delta = 1;
IdType empty_key = static_cast<IdType>(kEmptyKey);
IdType* hash_map_data = hash_map_.data_ptr<IdType>();
IdType key = hash_map_data[getKeyIndex(pos)];
while (key != empty_key && key != id) {
Next(&pos, &delta);
key = hash_map_data[getKeyIndex(pos)];
}
if (key == empty_key) {
throw std::out_of_range("Id not found: " + std::to_string(id));
}
return hash_map_data[getValueIndex(pos)];
}
template <typename IdType>
bool ConcurrentIdHashMap<IdType>::Insert(IdType id) {
IdType pos = (id & mask_), delta = 1;
InsertState state = AttemptInsertAt(pos, id);
while (state == InsertState::OCCUPIED) {
Next(&pos, &delta);
state = AttemptInsertAt(pos, id);
}
return state == InsertState::INSERTED;
}
template <typename IdType>
inline void ConcurrentIdHashMap<IdType>::Set(IdType key, IdType value) {
IdType pos = (key & mask_), delta = 1;
IdType* hash_map_data = hash_map_.data_ptr<IdType>();
while (hash_map_data[getKeyIndex(pos)] != key) {
Next(&pos, &delta);
}
hash_map_data[getValueIndex(pos)] = value;
}
template <typename IdType>
inline void ConcurrentIdHashMap<IdType>::InsertAndSet(IdType id, IdType value) {
IdType pos = (id & mask_), delta = 1;
while (AttemptInsertAt(pos, id) == InsertState::OCCUPIED) {
Next(&pos, &delta);
}
hash_map_.data_ptr<IdType>()[getValueIndex(pos)] = value;
}
template <typename IdType>
void ConcurrentIdHashMap<IdType>::InsertAndSetMin(IdType id, IdType value) {
IdType pos = (id & mask_), delta = 1;
InsertState state = AttemptInsertAt(pos, id);
while (state == InsertState::OCCUPIED) {
Next(&pos, &delta);
state = AttemptInsertAt(pos, id);
}
IdType empty_key = static_cast<IdType>(kEmptyKey);
IdType val_pos = getValueIndex(pos);
::cuda::std::atomic_ref value_ref(
reinterpret_cast<IdType*>(hash_map_.data_ptr())[val_pos]);
for (auto old_val = empty_key; old_val == empty_key || old_val > value;) {
// It is more efficient to use weak variant in a loop.
if (value_ref.compare_exchange_weak(old_val, value)) break;
}
}
template <typename IdType>
inline typename ConcurrentIdHashMap<IdType>::InsertState
ConcurrentIdHashMap<IdType>::AttemptInsertAt(int64_t pos, IdType key) {
auto expected = static_cast<IdType>(kEmptyKey);
::cuda::std::atomic_ref key_ref(
reinterpret_cast<IdType*>(hash_map_.data_ptr())[getKeyIndex(pos)]);
if (key_ref.compare_exchange_strong(expected, key)) {
return InsertState::INSERTED;
} else if (expected == key) {
return InsertState::EXISTED;
} else {
return InsertState::OCCUPIED;
}
}
template class ConcurrentIdHashMap<int32_t>;
template class ConcurrentIdHashMap<int64_t>;
} // namespace sampling
} // namespace graphbolt
+190
View File
@@ -0,0 +1,190 @@
/**
* Copyright (c) 2023 by Contributors
* @file concurrent_id_hash_map.h
* @brief Class about concurrent id hash map.
*/
#ifndef GRAPHBOLT_CONCURRENT_ID_HASH_MAP_H_
#define GRAPHBOLT_CONCURRENT_ID_HASH_MAP_H_
#include <torch/torch.h>
#include <functional>
#include <memory>
#include <vector>
namespace graphbolt {
namespace sampling {
/**
* @brief A CPU targeted hashmap for mapping duplicate and non-consecutive ids
* in the provided array to unique and consecutive ones. It utilizes
* multi-threading to accelerate the insert and search speed. Currently it is
* only designed to be used in `ToBlockCpu` for optimizing, so it only support
* key insertions once with Init function, and it does not support key deletion.
*
* The hash map should be prepared in two phases before using. With the first
* being creating the hashmap, and then initialize it with an id array which is
* divided into 2 parts: [`seed ids`, `sampled ids`]. `Seed ids` refer to
* a set ids chosen as the input for sampling process and `sampled ids` are the
* ids new sampled from the process (note the the `seed ids` might also be
* sampled in the process and included in the `sampled ids`). In result `seed
* ids` are mapped to [0, num_seed_ids) and `sampled ids` to [num_seed_ids,
* num_unique_ids). Notice that mapping order is stable for `seed ids` while not
* for the `sampled ids`.
*
* For example, for an array `A` having 4 seed ids with following entries:
* [99, 98, 100, 97, 97, 101, 101, 102, 101]
* Create the hashmap `H` with:
* `H = ConcurrentIdHashMap()` (1)
* And Init it with:
* `U = H.Init(A)` (2) (U is an id array used to store the unqiue
* ids in A).
* Then `U` should be (U is not exclusive as the overall mapping is not stable):
* [99, 98, 100, 97, 102, 101]
* And the hashmap should generate following mappings:
* * [
* {key: 99, value: 0},
* {key: 98, value: 1},
* {key: 100, value: 2},
* {key: 97, value: 3},
* {key: 102, value: 4},
* {key: 101, value: 5}
* ]
* Search the hashmap with array `I`=[98, 99, 102]:
* R = H.Map(I) (3)
* R should be:
* [1, 0, 4]
**/
template <typename IdType>
class ConcurrentIdHashMap {
private:
/**
* @brief The result state of an attempt to insert.
*/
enum class InsertState {
OCCUPIED, // Indicates that the space where an insertion is being
// attempted is already occupied by another element.
EXISTED, // Indicates that the element being inserted already exists in the
// map, and thus no insertion is performed.
INSERTED // Indicates that the insertion was successful and a new element
// was added to the map.
};
public:
/**
* @brief Initialize the hashmap with an array of ids. The first `num_seeds`
* ids are unique and must be mapped to a contiguous array starting
* from 0. The left can be duplicated and the mapping result is not stable.
* The unique'ified ids can be accessed through calling `GetUniqueIds()`;
*
* @param ids The array of the ids to be inserted.
* @param num_seeds The number of seed ids.
*/
ConcurrentIdHashMap(const torch::Tensor& ids, size_t num_seeds);
ConcurrentIdHashMap(const ConcurrentIdHashMap& other) = delete;
ConcurrentIdHashMap& operator=(const ConcurrentIdHashMap& other) = delete;
/**
* @brief Get the unique ids for the keys given in the constructor.
*/
const torch::Tensor& GetUniqueIds() const { return unique_ids_; }
/**
* @brief Find mappings of given keys.
*
* @param ids The keys to map for.
*
* @return Mapping results corresponding to `ids`.
*/
torch::Tensor MapIds(const torch::Tensor& ids) const;
private:
/**
* @brief Get the next position and delta for probing.
*
* @param[in,out] pos Calculate the next position with quadric probing.
* @param[in,out] delta Calculate the next delta by adding 1.
*/
inline void Next(IdType* pos, IdType* delta) const;
/**
* @brief Find the mapping of a given key.
*
* @param id The key to map for.
*
* @return Mapping result corresponding to `id`.
*/
inline IdType MapId(const IdType id) const;
/**
* @brief Insert an id into the hash map.
*
* @param id The id to be inserted.
*
* @return Whether the `id` is inserted or not.
*/
inline bool Insert(IdType id);
/**
* @brief Set the value for the key in the hash map.
*
* @param key The key to set for.
* @param value The value to be set for the `key`.
*
* @warning Key must exist.
*/
inline void Set(IdType key, IdType value);
/**
* @brief Insert a key into the hash map.
*
* @param id The key to be inserted.
* @param value The value to be set for the `key`.
*
*/
inline void InsertAndSet(IdType key, IdType value);
/**
* @brief Insert a key into the hash map. If the key exists, set the value
* with the smaller value.
*
* @param id The key to be inserted.
* @param value The value to be set for the `key`.
*
*/
inline void InsertAndSetMin(IdType id, IdType value);
/**
* @brief Attempt to insert the key into the hash map at the given position.
*
* @param pos The position in the hash map to be inserted at.
* @param key The key to be inserted.
*
* @return The state of the insertion.
*/
inline InsertState AttemptInsertAt(int64_t pos, IdType key);
private:
/**
* @brief Hash maps which is used to store all elements.
*/
torch::Tensor hash_map_;
/**
* @brief Holds the ids that are made unique in the constructor.
*/
torch::Tensor unique_ids_;
/**
* @brief Mask which is assisted to get the position in the table
* for a key by performing `&` operation with it.
*/
IdType mask_;
};
} // namespace sampling
} // namespace graphbolt
#endif // GRAPHBOLT_CONCURRENT_ID_HASH_MAP_H_
+231
View File
@@ -0,0 +1,231 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/common.h
* @brief Common utilities for CUDA
*/
#ifndef GRAPHBOLT_CUDA_COMMON_H_
#define GRAPHBOLT_CUDA_COMMON_H_
#include <ATen/cuda/CUDAEvent.h>
#include <c10/cuda/CUDACachingAllocator.h>
#include <c10/cuda/CUDAException.h>
#include <c10/cuda/CUDAStream.h>
#include <cuda_runtime.h>
#include <thrust/execution_policy.h>
#include <torch/script.h>
#include <memory>
#include <unordered_map>
namespace graphbolt {
namespace cuda {
/**
* @brief This class is designed to allocate workspace storage
* and to get a nonblocking thrust execution policy
* that uses torch's CUDA memory pool and the current cuda stream:
*
* cuda::CUDAWorkspaceAllocator allocator;
* const auto stream = torch::cuda::getDefaultCUDAStream();
* const auto exec_policy = thrust::cuda::par_nosync(allocator).on(stream);
*
* Now, one can pass exec_policy to thrust functions
*
* To get an integer array of size 1000 whose lifetime is managed by unique_ptr,
* use:
*
* auto int_array = allocator.AllocateStorage<int>(1000);
*
* int_array.get() gives the raw pointer.
*/
template <typename value_t = char>
struct CUDAWorkspaceAllocator {
static_assert(sizeof(char) == 1, "sizeof(char) == 1 should hold.");
// Required by thrust to satisfy allocator requirements.
using value_type = value_t;
explicit CUDAWorkspaceAllocator() {
at::globalContext().lazyInitDevice(at::kCUDA);
}
template <class U>
CUDAWorkspaceAllocator(CUDAWorkspaceAllocator<U> const&) noexcept {}
CUDAWorkspaceAllocator& operator=(const CUDAWorkspaceAllocator&) = default;
void operator()(void* ptr) const {
c10::cuda::CUDACachingAllocator::raw_delete(ptr);
}
// Required by thrust to satisfy allocator requirements.
value_type* allocate(std::ptrdiff_t size) const {
return reinterpret_cast<value_type*>(
c10::cuda::CUDACachingAllocator::raw_alloc(size * sizeof(value_type)));
}
// Required by thrust to satisfy allocator requirements.
void deallocate(value_type* ptr, std::size_t) const { operator()(ptr); }
template <typename T>
std::unique_ptr<T, CUDAWorkspaceAllocator> AllocateStorage(
std::size_t size) const {
return std::unique_ptr<T, CUDAWorkspaceAllocator>(
reinterpret_cast<T*>(
c10::cuda::CUDACachingAllocator::raw_alloc(sizeof(T) * size)),
*this);
}
};
inline auto GetAllocator() { return CUDAWorkspaceAllocator{}; }
inline auto GetCurrentStream() { return c10::cuda::getCurrentCUDAStream(); }
template <typename T>
inline bool is_zero(T size) {
return size == 0;
}
template <>
inline bool is_zero<dim3>(dim3 size) {
return size.x == 0 || size.y == 0 || size.z == 0;
}
#define CUDA_RUNTIME_CHECK(EXPR) \
do { \
cudaError_t __err = EXPR; \
if (__err != cudaSuccess) { \
auto get_error_str_err = cudaGetErrorString(__err); \
AT_ERROR("CUDA runtime error: ", get_error_str_err); \
} \
} while (0)
#define CUDA_CALL(func) C10_CUDA_CHECK((func))
#define CUDA_KERNEL_CALL(kernel, nblks, nthrs, shmem, ...) \
{ \
if (!graphbolt::cuda::is_zero((nblks)) && \
!graphbolt::cuda::is_zero((nthrs))) { \
auto stream = graphbolt::cuda::GetCurrentStream(); \
(kernel)<<<(nblks), (nthrs), (shmem), stream>>>(__VA_ARGS__); \
C10_CUDA_KERNEL_LAUNCH_CHECK(); \
} \
}
#define CUB_CALL(fn, ...) \
{ \
auto allocator = graphbolt::cuda::GetAllocator(); \
auto stream = graphbolt::cuda::GetCurrentStream(); \
size_t workspace_size = 0; \
CUDA_CALL(cub::fn(nullptr, workspace_size, __VA_ARGS__, stream)); \
auto workspace = allocator.AllocateStorage<char>(workspace_size); \
CUDA_CALL(cub::fn(workspace.get(), workspace_size, __VA_ARGS__, stream)); \
}
#define THRUST_CALL(fn, ...) \
[&] { \
auto allocator = graphbolt::cuda::GetAllocator(); \
auto stream = graphbolt::cuda::GetCurrentStream(); \
const auto exec_policy = thrust::cuda::par_nosync(allocator).on(stream); \
return thrust::fn(exec_policy, __VA_ARGS__); \
}()
/**
* @brief This class is designed to handle the copy operation of a single
* scalar_t item from a given CUDA device pointer. Later, if the object is cast
* into scalar_t, the value can be read.
*
* auto num_edges = cuda::CopyScalar(indptr.data_ptr<scalar_t>() +
* indptr.size(0) - 1);
* // Perform many operations here, they will run as normal.
* // We finally need to read num_edges.
* auto indices = torch::empty(static_cast<scalar_t>(num_edges));
*/
template <typename scalar_t>
struct CopyScalar {
CopyScalar() : is_ready_(true) { init_pinned_storage(); }
void record(at::cuda::CUDAStream stream = GetCurrentStream()) {
copy_event_.record(stream);
is_ready_ = false;
}
scalar_t* get() {
return reinterpret_cast<scalar_t*>(pinned_scalar_.data_ptr());
}
CopyScalar(const scalar_t* device_ptr) {
init_pinned_storage();
auto stream = GetCurrentStream();
CUDA_CALL(cudaMemcpyAsync(
reinterpret_cast<scalar_t*>(pinned_scalar_.data_ptr()), device_ptr,
sizeof(scalar_t), cudaMemcpyDeviceToHost, stream));
record(stream);
}
operator scalar_t() {
if (!is_ready_) {
copy_event_.synchronize();
is_ready_ = true;
}
return *get();
}
private:
void init_pinned_storage() {
pinned_scalar_ = torch::empty(
sizeof(scalar_t),
c10::TensorOptions().dtype(torch::kBool).pinned_memory(true));
}
torch::Tensor pinned_scalar_;
at::cuda::CUDAEvent copy_event_;
bool is_ready_;
};
#define GRAPHBOLT_DISPATCH_ELEMENT_SIZES(element_size, name, ...) \
[&] { \
switch (element_size) { \
case 1: { \
using element_size_t = uint8_t; \
return __VA_ARGS__(); \
} \
case 2: { \
using element_size_t = uint16_t; \
return __VA_ARGS__(); \
} \
case 4: { \
using element_size_t = uint32_t; \
return __VA_ARGS__(); \
} \
case 8: { \
using element_size_t = uint64_t; \
return __VA_ARGS__(); \
} \
case 16: { \
using element_size_t = float4; \
return __VA_ARGS__(); \
} \
default: \
TORCH_CHECK(false, name, " with the element_size is not supported!"); \
using element_size_t = uint8_t; \
return __VA_ARGS__(); \
} \
}()
} // namespace cuda
} // namespace graphbolt
#endif // GRAPHBOLT_CUDA_COMMON_H_
@@ -0,0 +1,159 @@
/**
* Copyright (c) 2024, mfbalin (Muhammed Fatih Balin)
* All rights reserved.
*
* 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.
*
* @file cuda/cooperative_minibatching_utils.cu
* @brief Cooperative Minibatching (arXiv:2310.12403) utility function
* implementations in CUDA.
*/
#include <graphbolt/cuda_ops.h>
#include <thrust/scatter.h>
#include <thrust/transform.h>
#include <cub/cub.cuh>
#include <cuda/functional>
#include "../utils.h"
#include "./common.h"
#include "./cooperative_minibatching_utils.cuh"
#include "./cooperative_minibatching_utils.h"
#include "./utils.h"
namespace graphbolt {
namespace cuda {
torch::Tensor RankAssignment(
torch::Tensor nodes, const int64_t rank, const int64_t world_size) {
auto part_ids = torch::empty_like(nodes, nodes.options().dtype(kPartDType));
auto part_ids_ptr = part_ids.data_ptr<part_t>();
AT_DISPATCH_INDEX_TYPES(
nodes.scalar_type(), "RankAssignment", ([&] {
auto nodes_ptr = nodes.data_ptr<index_t>();
THRUST_CALL(
transform, nodes_ptr, nodes_ptr + nodes.numel(), part_ids_ptr,
::cuda::proclaim_return_type<part_t>(
[rank = static_cast<uint32_t>(rank),
world_size = static_cast<uint32_t>(
world_size)] __device__(index_t id) -> part_t {
return rank_assignment(id, rank, world_size);
}));
}));
return part_ids;
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, at::cuda::CUDAEvent>
RankSortImpl(
torch::Tensor nodes, torch::Tensor part_ids, torch::Tensor offsets_dev,
const int64_t world_size) {
const int num_bits = cuda::NumberOfBits(world_size);
const auto num_batches = offsets_dev.numel() - 1;
auto offsets_dev_ptr = offsets_dev.data_ptr<int64_t>();
auto part_ids_sorted = torch::empty_like(part_ids);
auto part_ids2 = part_ids.clone();
auto part_ids2_sorted = torch::empty_like(part_ids2);
auto nodes_sorted = torch::empty_like(nodes);
auto index = torch::arange(nodes.numel(), nodes.options());
auto index_sorted = torch::empty_like(index);
return AT_DISPATCH_INDEX_TYPES(
nodes.scalar_type(), "RankSortImpl", ([&] {
CUB_CALL(
DeviceSegmentedRadixSort::SortPairs,
part_ids.data_ptr<cuda::part_t>(),
part_ids_sorted.data_ptr<cuda::part_t>(), nodes.data_ptr<index_t>(),
nodes_sorted.data_ptr<index_t>(), nodes.numel(), num_batches,
offsets_dev_ptr, offsets_dev_ptr + 1, 0, num_bits);
auto offsets = torch::empty(
num_batches * world_size + 1, c10::TensorOptions()
.dtype(offsets_dev.scalar_type())
.pinned_memory(true));
CUB_CALL(
DeviceFor::Bulk, num_batches * world_size + 1,
[=, part_ids = part_ids_sorted.data_ptr<cuda::part_t>(),
offsets = offsets.data_ptr<int64_t>()] __device__(int64_t i) {
const auto batch_id = i / world_size;
const auto rank = i % world_size;
const auto offset_begin = offsets_dev_ptr[batch_id];
const auto offset_end =
offsets_dev_ptr[::cuda::std::min(batch_id + 1, num_batches)];
offsets[i] = cub::LowerBound(
part_ids + offset_begin,
offset_end - offset_begin, rank) +
offset_begin;
});
at::cuda::CUDAEvent offsets_event;
offsets_event.record();
CUB_CALL(
DeviceSegmentedRadixSort::SortPairs,
part_ids2.data_ptr<cuda::part_t>(),
part_ids2_sorted.data_ptr<cuda::part_t>(),
index.data_ptr<index_t>(), index_sorted.data_ptr<index_t>(),
nodes.numel(), num_batches, offsets_dev_ptr, offsets_dev_ptr + 1, 0,
num_bits);
auto values = ops::IndptrEdgeIdsImpl(
offsets_dev, nodes.scalar_type(), torch::nullopt, nodes.numel());
THRUST_CALL(
scatter, values.data_ptr<index_t>(),
values.data_ptr<index_t>() + values.numel(),
index_sorted.data_ptr<index_t>(), index.data_ptr<index_t>());
return std::make_tuple(
nodes_sorted, index, offsets, std::move(offsets_event));
}));
}
std::vector<std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>> RankSort(
const std::vector<torch::Tensor>& nodes_list, const int64_t rank,
const int64_t world_size) {
const auto num_batches = nodes_list.size();
auto nodes = torch::cat(nodes_list, 0);
auto offsets = torch::empty(
num_batches + 1,
c10::TensorOptions().dtype(torch::kInt64).pinned_memory(true));
auto offsets_ptr = offsets.data_ptr<int64_t>();
offsets_ptr[0] = 0;
for (int64_t i = 0; i < num_batches; i++) {
offsets_ptr[i + 1] = offsets_ptr[i] + nodes_list[i].numel();
}
auto part_ids = RankAssignment(nodes, rank, world_size);
auto offsets_dev =
torch::empty_like(offsets, nodes.options().dtype(offsets.scalar_type()));
CUDA_CALL(cudaMemcpyAsync(
offsets_dev.data_ptr<int64_t>(), offsets_ptr,
sizeof(int64_t) * offsets.numel(), cudaMemcpyHostToDevice,
cuda::GetCurrentStream()));
auto [nodes_sorted, index_sorted, rank_offsets, rank_offsets_event] =
RankSortImpl(nodes, part_ids, offsets_dev, world_size);
std::vector<std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>> results;
rank_offsets_event.synchronize();
for (int64_t i = 0; i < num_batches; i++) {
results.emplace_back(
nodes_sorted.slice(0, offsets_ptr[i], offsets_ptr[i + 1]),
index_sorted.slice(0, offsets_ptr[i], offsets_ptr[i + 1]),
rank_offsets.slice(0, i * world_size, (i + 1) * world_size + 1));
}
return results;
}
c10::intrusive_ptr<Future<
std::vector<std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>>>>
RankSortAsync(
const std::vector<torch::Tensor>& nodes_list, const int64_t rank,
const int64_t world_size) {
return async(
[=] { return RankSort(nodes_list, rank, world_size); },
utils::is_on_gpu(nodes_list.at(0)));
}
} // namespace cuda
} // namespace graphbolt
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2024, mfbalin (Muhammed Fatih Balin)
* All rights reserved.
*
* 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.
*
* @file cuda/cooperative_minibatching_utils.cuh
* @brief Cooperative Minibatching (arXiv:2310.12403) utility device functions
* in CUDA.
*/
#ifndef GRAPHBOLT_CUDA_COOPERATIVE_MINIBATCHING_UTILS_CUH_
#define GRAPHBOLT_CUDA_COOPERATIVE_MINIBATCHING_UTILS_CUH_
#include <curand_kernel.h>
namespace graphbolt {
namespace cuda {
using part_t = uint8_t;
constexpr auto kPartDType = torch::kUInt8;
/**
* @brief Given a vertex id, the rank of current GPU and the world size, returns
* the rank that this id belongs in a deterministic manner.
*
* @param id The node id that will mapped to a rank in [0, world_size).
* @param rank The rank of the current GPU.
* @param world_size The world size, the total number of cooperating GPUs.
*
* @return The rank of the GPU the given id is mapped to.
*/
template <typename index_t>
__device__ inline auto rank_assignment(
index_t id, uint32_t rank, uint32_t world_size) {
// Consider using a faster implementation in the future.
constexpr uint64_t kCurandSeed = 999961; // Any random number.
curandStatePhilox4_32_10_t rng;
curand_init(kCurandSeed, 0, id, &rng);
return (curand(&rng) - rank) % world_size;
}
} // namespace cuda
} // namespace graphbolt
#endif // GRAPHBOLT_CUDA_COOPERATIVE_MINIBATCHING_UTILS_CUH_
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2024, mfbalin (Muhammed Fatih Balin)
* All rights reserved.
*
* 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.
*
* @file cuda/cooperative_minibatching_utils.h
* @brief Cooperative Minibatching (arXiv:2310.12403) utility function headers
* in CUDA.
*/
#ifndef GRAPHBOLT_CUDA_COOPERATIVE_MINIBATCHING_UTILS_H_
#define GRAPHBOLT_CUDA_COOPERATIVE_MINIBATCHING_UTILS_H_
#include <ATen/cuda/CUDAEvent.h>
#include <graphbolt/async.h>
#include <torch/script.h>
namespace graphbolt {
namespace cuda {
/**
* @brief Given node ids, the rank of current GPU and the world size, returns
* the ranks that the given ids belong in a deterministic manner.
*
* @param nodes Node id tensor to be mapped to a rank in [0, world_size).
* @param rank Rank of the current GPU.
* @param world_size World size, the total number of cooperating GPUs.
*
* @return The rank tensor of the GPU the given id tensor is mapped to.
*/
torch::Tensor RankAssignment(
torch::Tensor nodes, int64_t rank, int64_t world_size);
/**
* @brief Given node ids, the ranks they belong, the offsets to separate
* different node types and world size, returns node ids sorted w.r.t. the ranks
* that the given ids belong along with their new positions.
*
* @param nodes Node id tensor to be mapped to a rank in [0, world_size).
* @param part_ids Rank tensor the nodes belong to.
* @param offsets_dev Offsets to separate different node types.
* @param world_size World size, the total number of cooperating GPUs.
*
* @return (sorted_nodes, new_positions, rank_offsets, rank_offsets_event),
* where the first one includes sorted nodes, the second contains new positions
* of the given nodes, so that sorted_nodes[new_positions] == nodes, and the
* third contains the offsets of the sorted_nodes indicating
* sorted_nodes[rank_offsets[i]: rank_offsets[i + 1]] contains nodes that
* belongs to the `i`th rank. Before accessing rank_offsets on the CPU,
* `rank_offsets_event.synchronize()` is required.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, at::cuda::CUDAEvent>
RankSortImpl(
torch::Tensor nodes, torch::Tensor part_ids, torch::Tensor offsets_dev,
int64_t world_size);
/**
* @brief Given a vector of node ids, the rank of current GPU and the world
* size, returns node ids sorted w.r.t. the ranks that the given ids belong
* along with the original positions.
*
* @param nodes_list Node id tensor to be mapped to a rank in [0, world_size).
* @param rank Rank of the current GPU.
* @param world_size World size, the total number of cooperating GPUs.
*
* @return vector of (sorted_nodes, new_positions, rank_offsets), where the
* first one includes sorted nodes, the second contains new positions of the
* given nodes, so that sorted_nodes[new_positions] == nodes, and the third
* contains the offsets of the sorted_nodes indicating
* sorted_nodes[rank_offsets[i]: rank_offsets[i + 1]] contains nodes that
* belongs to the `i`th rank.
*/
std::vector<std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>> RankSort(
const std::vector<torch::Tensor>& nodes_list, int64_t rank,
int64_t world_size);
c10::intrusive_ptr<Future<
std::vector<std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>>>>
RankSortAsync(
const std::vector<torch::Tensor>& nodes_list, const int64_t rank,
const int64_t world_size);
} // namespace cuda
} // namespace graphbolt
#endif // GRAPHBOLT_CUDA_COOPERATIVE_MINIBATCHING_UTILS_H_
+40
View File
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/cumsum.cu
* @brief Cumsum operators implementation on CUDA.
*/
#include <cub/cub.cuh>
#include "./common.h"
namespace graphbolt {
namespace ops {
torch::Tensor ExclusiveCumSum(torch::Tensor input) {
auto result = torch::empty_like(input);
AT_DISPATCH_INTEGRAL_TYPES(input.scalar_type(), "ExclusiveCumSum", ([&] {
CUB_CALL(
DeviceScan::ExclusiveSum,
input.data_ptr<scalar_t>(),
result.data_ptr<scalar_t>(), input.size(0));
}));
return result;
}
} // namespace ops
} // namespace graphbolt
+145
View File
@@ -0,0 +1,145 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/expand_indptr.cu
* @brief ExpandIndptr operator implementation on CUDA.
*/
#include <thrust/iterator/constant_iterator.h>
#include <thrust/iterator/counting_iterator.h>
#include <thrust/iterator/transform_iterator.h>
#include <cub/cub.cuh>
#include <limits>
#include "./common.h"
namespace graphbolt {
namespace ops {
template <typename indices_t, typename nodes_t>
struct RepeatIndex {
const nodes_t* nodes;
__host__ __device__ auto operator()(indices_t i) {
return thrust::make_constant_iterator(nodes ? nodes[i] : i);
}
};
template <typename indices_t, typename nodes_t>
struct IotaIndex {
const nodes_t* nodes;
__host__ __device__ auto operator()(indices_t i) {
return thrust::make_counting_iterator(nodes ? nodes[i] : 0);
}
};
template <typename indptr_t, typename indices_t>
struct OutputBufferIndexer {
const indptr_t* indptr;
indices_t* buffer;
__host__ __device__ auto operator()(int64_t i) { return buffer + indptr[i]; }
};
template <typename indptr_t>
struct AdjacentDifference {
const indptr_t* indptr;
__host__ __device__ auto operator()(int64_t i) {
return indptr[i + 1] - indptr[i];
}
};
torch::Tensor ExpandIndptrImpl(
torch::Tensor indptr, torch::ScalarType dtype,
torch::optional<torch::Tensor> nodes, torch::optional<int64_t> output_size,
const bool is_edge_ids_variant) {
if (!output_size.has_value()) {
output_size = AT_DISPATCH_INTEGRAL_TYPES(
indptr.scalar_type(), "ExpandIndptrIndptr[-1]", ([&]() -> int64_t {
auto indptr_ptr = indptr.data_ptr<scalar_t>();
auto output_size = cuda::CopyScalar{indptr_ptr + indptr.size(0) - 1};
return static_cast<scalar_t>(output_size);
}));
}
auto csc_rows =
torch::empty(output_size.value(), indptr.options().dtype(dtype));
AT_DISPATCH_INTEGRAL_TYPES(
indptr.scalar_type(), "ExpandIndptrIndptr", ([&] {
using indptr_t = scalar_t;
auto indptr_ptr = indptr.data_ptr<indptr_t>();
AT_DISPATCH_INTEGRAL_TYPES(
dtype, "ExpandIndptrIndices", ([&] {
using indices_t = scalar_t;
auto csc_rows_ptr = csc_rows.data_ptr<indices_t>();
auto nodes_dtype = nodes ? nodes.value().scalar_type() : dtype;
AT_DISPATCH_INTEGRAL_TYPES(
nodes_dtype, "ExpandIndptrNodes", ([&] {
using nodes_t = scalar_t;
auto nodes_ptr =
nodes ? nodes.value().data_ptr<nodes_t>() : nullptr;
thrust::counting_iterator<int64_t> iota(0);
auto output_buffer = thrust::make_transform_iterator(
iota, OutputBufferIndexer<indptr_t, indices_t>{
indptr_ptr, csc_rows_ptr});
auto buffer_sizes = thrust::make_transform_iterator(
iota, AdjacentDifference<indptr_t>{indptr_ptr});
const auto num_rows = indptr.size(0) - 1;
constexpr int64_t max_copy_at_once =
std::numeric_limits<int32_t>::max();
if (is_edge_ids_variant) {
auto input_buffer = thrust::make_transform_iterator(
iota, IotaIndex<indices_t, nodes_t>{nodes_ptr});
for (int64_t i = 0; i < num_rows; i += max_copy_at_once) {
CUB_CALL(
DeviceCopy::Batched, input_buffer + i,
output_buffer + i, buffer_sizes + i,
std::min(num_rows - i, max_copy_at_once));
}
} else {
auto input_buffer = thrust::make_transform_iterator(
iota, RepeatIndex<indices_t, nodes_t>{nodes_ptr});
for (int64_t i = 0; i < num_rows; i += max_copy_at_once) {
CUB_CALL(
DeviceCopy::Batched, input_buffer + i,
output_buffer + i, buffer_sizes + i,
std::min(num_rows - i, max_copy_at_once));
}
}
}));
}));
}));
return csc_rows;
}
torch::Tensor ExpandIndptrImpl(
torch::Tensor indptr, torch::ScalarType dtype,
torch::optional<torch::Tensor> nodes,
torch::optional<int64_t> output_size) {
return ExpandIndptrImpl(indptr, dtype, nodes, output_size, false);
}
torch::Tensor IndptrEdgeIdsImpl(
torch::Tensor indptr, torch::ScalarType dtype,
torch::optional<torch::Tensor> offset,
torch::optional<int64_t> output_size) {
return ExpandIndptrImpl(indptr, dtype, offset, output_size, true);
}
} // namespace ops
} // namespace graphbolt
+133
View File
@@ -0,0 +1,133 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/gpu_cache.cu
* @brief GPUCache implementation on CUDA.
*/
#include <numeric>
#include "../common.h"
#include "./gpu_cache.h"
namespace graphbolt {
namespace cuda {
GpuCache::GpuCache(const std::vector<int64_t> &shape, torch::ScalarType dtype) {
TORCH_CHECK(shape.size() >= 2, "Shape must at least have 2 dimensions.");
const auto num_items = shape[0];
TORCH_CHECK(
num_items > 0, "The capacity of GpuCache needs to be a positive.");
const int64_t num_feats =
std::accumulate(shape.begin() + 1, shape.end(), 1ll, std::multiplies<>());
const int element_size =
torch::empty(1, torch::TensorOptions().dtype(dtype)).element_size();
num_bytes_ = num_feats * element_size;
num_float_feats_ = (num_bytes_ + sizeof(float) - 1) / sizeof(float);
cache_ = std::make_unique<gpu_cache_t>(
(num_items + bucket_size - 1) / bucket_size, num_float_feats_);
shape_ = shape;
shape_[0] = -1;
dtype_ = dtype;
device_id_ = cuda::GetCurrentStream().device_index();
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor> GpuCache::Query(
torch::Tensor keys) {
TORCH_CHECK(keys.device().is_cuda(), "Keys should be on a CUDA device.");
TORCH_CHECK(
keys.device().index() == device_id_,
"Keys should be on the correct CUDA device.");
TORCH_CHECK(keys.sizes().size() == 1, "Keys should be a 1D tensor.");
keys = keys.to(torch::kLong);
auto values = torch::empty(
{keys.size(0), num_float_feats_}, keys.options().dtype(torch::kFloat));
auto missing_index =
torch::empty(keys.size(0), keys.options().dtype(torch::kLong));
auto missing_keys =
torch::empty(keys.size(0), keys.options().dtype(torch::kLong));
auto allocator = cuda::GetAllocator();
auto missing_len_device = allocator.AllocateStorage<size_t>(1);
cache_->Query(
reinterpret_cast<const key_t *>(keys.data_ptr()), keys.size(0),
values.data_ptr<float>(),
reinterpret_cast<uint64_t *>(missing_index.data_ptr()),
reinterpret_cast<key_t *>(missing_keys.data_ptr()),
missing_len_device.get(), cuda::GetCurrentStream());
values = values.view(torch::kByte)
.slice(1, 0, num_bytes_)
.view(dtype_)
.view(shape_);
cuda::CopyScalar<size_t> missing_len(missing_len_device.get());
missing_index = missing_index.slice(0, 0, static_cast<size_t>(missing_len));
missing_keys = missing_keys.slice(0, 0, static_cast<size_t>(missing_len));
return std::make_tuple(values, missing_index, missing_keys);
}
c10::intrusive_ptr<Future<std::vector<torch::Tensor>>> GpuCache::QueryAsync(
torch::Tensor keys) {
return async(
[=] {
auto [values, missing_index, missing_keys] = Query(keys);
return std::vector{values, missing_index, missing_keys};
},
true);
}
void GpuCache::Replace(torch::Tensor keys, torch::Tensor values) {
TORCH_CHECK(keys.device().is_cuda(), "Keys should be on a CUDA device.");
TORCH_CHECK(
keys.device().index() == device_id_,
"Keys should be on the correct CUDA device.");
TORCH_CHECK(values.device().is_cuda(), "Keys should be on a CUDA device.");
TORCH_CHECK(
values.device().index() == device_id_,
"Values should be on the correct CUDA device.");
TORCH_CHECK(
keys.size(0) == values.size(0),
"The first dimensions of keys and values must match.");
TORCH_CHECK(
std::equal(shape_.begin() + 1, shape_.end(), values.sizes().begin() + 1),
"Values should have the correct dimensions.");
TORCH_CHECK(
values.scalar_type() == dtype_, "Values should have the correct dtype.");
if (keys.numel() == 0) return;
keys = keys.to(torch::kLong);
torch::Tensor float_values;
if (num_bytes_ % sizeof(float) != 0) {
float_values = torch::empty(
{values.size(0), num_float_feats_},
values.options().dtype(torch::kFloat));
float_values.view(torch::kByte)
.slice(1, 0, num_bytes_)
.copy_(values.view(torch::kByte).view({values.size(0), -1}));
} else {
float_values = values.view(torch::kByte)
.view({values.size(0), -1})
.view(torch::kFloat)
.contiguous();
}
cache_->Replace(
reinterpret_cast<const key_t *>(keys.data_ptr()), keys.size(0),
float_values.data_ptr<float>(), cuda::GetCurrentStream());
}
c10::intrusive_ptr<GpuCache> GpuCache::Create(
const std::vector<int64_t> &shape, torch::ScalarType dtype) {
return c10::make_intrusive<GpuCache>(shape, dtype);
}
} // namespace cuda
} // namespace graphbolt
+83
View File
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/gpu_cache.h
* @brief Header file of HugeCTR gpu_cache wrapper.
*/
#ifndef GRAPHBOLT_GPU_CACHE_H_
#define GRAPHBOLT_GPU_CACHE_H_
#include <graphbolt/async.h>
#include <torch/custom_class.h>
#include <torch/torch.h>
#include <limits>
#include <nv_gpu_cache.hpp>
namespace graphbolt {
namespace cuda {
class GpuCache : public torch::CustomClassHolder {
using key_t = long long;
constexpr static int set_associativity = 2;
constexpr static int WARP_SIZE = 32;
constexpr static int bucket_size = WARP_SIZE * set_associativity;
using gpu_cache_t = ::gpu_cache::gpu_cache<
key_t, uint64_t, std::numeric_limits<key_t>::max(), set_associativity,
WARP_SIZE>;
public:
/**
* @brief Constructor for the GpuCache struct.
*
* @param shape The shape of the GPU cache.
* @param dtype The datatype of items to be stored.
*/
GpuCache(const std::vector<int64_t>& shape, torch::ScalarType dtype);
GpuCache() = default;
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor> Query(
torch::Tensor keys);
c10::intrusive_ptr<Future<std::vector<torch::Tensor>>> QueryAsync(
torch::Tensor keys);
void Replace(torch::Tensor keys, torch::Tensor values);
static c10::intrusive_ptr<GpuCache> Create(
const std::vector<int64_t>& shape, torch::ScalarType dtype);
private:
std::vector<int64_t> shape_;
torch::ScalarType dtype_;
std::unique_ptr<gpu_cache_t> cache_;
int64_t num_bytes_;
int64_t num_float_feats_;
torch::DeviceIndex device_id_;
};
// The cu file in HugeCTR gpu cache uses unsigned int and long long.
// Changing to int64_t results in a mismatch of template arguments.
static_assert(
sizeof(long long) == sizeof(int64_t),
"long long and int64_t needs to have the same size."); // NOLINT
} // namespace cuda
} // namespace graphbolt
#endif // GRAPHBOLT_GPU_CACHE_H_
@@ -0,0 +1,546 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/gpu_graph_cache.cu
* @brief GPU graph cache implementation on CUDA.
*/
#include <graphbolt/cuda_ops.h>
#include <thrust/gather.h>
#include <thrust/transform.h>
#include <cstddef>
#include <cub/cub.cuh>
#include <cuco/static_map.cuh>
#include <cuda/std/atomic>
#include <cuda/stream_ref>
#include <limits>
#include <numeric>
#include <type_traits>
#include "../common.h"
#include "../utils.h"
#include "./gpu_graph_cache.h"
namespace graphbolt {
namespace cuda {
namespace {
constexpr int cg_size = 1;
template <typename index_t>
using probing_t =
cuco::linear_probing<cg_size, cuco::default_hash_function<index_t>>;
template <typename index_t>
using allocator_t = cuda::CUDAWorkspaceAllocator<cuco::pair<index_t, index_t>>;
template <typename index_t>
using map_t = cuco::static_map<
index_t, index_t, cuco::extent<int64_t>, ::cuda::thread_scope_device,
thrust::equal_to<index_t>, probing_t<index_t>, allocator_t<index_t>>;
template <typename index_t, typename map_t>
__global__ void _Insert(
const int64_t num_nodes, const index_t num_existing, const index_t* seeds,
const index_t* missing_indices, const index_t* indices, map_t map) {
int64_t i = blockIdx.x * blockDim.x + threadIdx.x;
const int stride = gridDim.x * blockDim.x;
while (i < num_nodes) {
const auto key = seeds[missing_indices[indices[i]]];
auto slot = map.find(key);
slot->second = num_existing + i;
i += stride;
}
}
/**
* @brief For node ids not in the cache, it keeps their access count inside
* a hash table as (v, -c) where v is the node id and c is the access count.
* When c == -threshold, it means that v will be inserted into the cache
* during the call to the replace method. Once v is inserted into the cache,
* c is assigned to a nonnegative value and indicates the local id of vertex
* v in the cache.
*
* @param num_nodes The number of node ids.
* @param seeds The node ids the cache is being queried with.
* @param positions Holds the values found in the hash table.
* @param map The hash table holding (v, -c) or (v, local_id).
*
*/
template <typename index_t, typename map_t>
__global__ void _QueryAndIncrement(
const int64_t num_nodes, const index_t* seeds, index_t* positions,
map_t map) {
int64_t i = blockIdx.x * blockDim.x + threadIdx.x;
const int stride = gridDim.x * blockDim.x;
while (i < num_nodes) {
const auto key = seeds[i];
constexpr index_t minusONE = -1;
auto [slot, is_new_key] = map.insert_and_find(cuco::pair{key, minusONE});
int64_t position = -1;
if (!is_new_key) {
auto ref = ::cuda::atomic_ref<index_t, ::cuda::thread_scope_device>{
slot->second};
position = ref.load(::cuda::memory_order_relaxed);
if (position < 0) {
position = ref.fetch_add(-1, ::cuda::memory_order_relaxed) - 1;
}
}
positions[i] = position;
i += stride;
}
}
constexpr int kIntBlockSize = 512;
} // namespace
c10::intrusive_ptr<GpuGraphCache> GpuGraphCache::Create(
const int64_t num_edges, const int64_t threshold,
torch::ScalarType indptr_dtype, std::vector<torch::ScalarType> dtypes,
bool has_original_edge_ids) {
return c10::make_intrusive<GpuGraphCache>(
num_edges, threshold, indptr_dtype, dtypes, has_original_edge_ids);
}
GpuGraphCache::GpuGraphCache(
const int64_t num_edges, const int64_t threshold,
torch::ScalarType indptr_dtype, std::vector<torch::ScalarType> dtypes,
bool has_original_edge_ids) {
const int64_t initial_node_capacity = 1024;
AT_DISPATCH_INDEX_TYPES(
dtypes.at(0), "GpuGraphCache::GpuGraphCache", ([&] {
auto map_temp = map_t<index_t>{
initial_node_capacity,
kDoubleLoadFactor,
cuco::empty_key{static_cast<index_t>(-1)},
cuco::empty_value{std::numeric_limits<index_t>::lowest()},
{},
probing_t<index_t>{},
{},
{},
allocator_t<index_t>{},
::cuda::stream_ref{cuda::GetCurrentStream()}};
map_ = new map_t<index_t>{std::move(map_temp)};
}));
C10_CUDA_KERNEL_LAUNCH_CHECK(); // Check the map constructor's success.
const auto options = torch::TensorOptions().device(c10::DeviceType::CUDA);
TORCH_CHECK(threshold > 0, "Threshold should be a position integer.");
threshold_ = threshold;
device_id_ = cuda::GetCurrentStream().device_index();
map_size_ = 0;
num_nodes_ = 0;
num_edges_ = 0;
indptr_ =
torch::zeros(initial_node_capacity + 1, options.dtype(indptr_dtype));
if (!has_original_edge_ids) {
offset_ = torch::empty(indptr_.size(0) - 1, indptr_.options());
}
for (auto dtype : dtypes) {
cached_edge_tensors_.push_back(
torch::empty(num_edges, options.dtype(dtype)));
}
}
GpuGraphCache::~GpuGraphCache() {
AT_DISPATCH_INDEX_TYPES(
cached_edge_tensors_.at(0).scalar_type(), "GpuGraphCache::GpuGraphCache",
([&] { delete reinterpret_cast<map_t<index_t>*>(map_); }));
}
std::tuple<torch::Tensor, torch::Tensor, int64_t, int64_t> GpuGraphCache::Query(
torch::Tensor seeds) {
TORCH_CHECK(seeds.device().is_cuda(), "Seeds should be on a CUDA device.");
TORCH_CHECK(
seeds.device().index() == device_id_,
"Seeds should be on the correct CUDA device.");
TORCH_CHECK(seeds.sizes().size() == 1, "Keys should be a 1D tensor.");
std::lock_guard lock(mtx_);
auto allocator = cuda::GetAllocator();
auto index_dtype = cached_edge_tensors_.at(0).scalar_type();
const dim3 block(kIntBlockSize);
const dim3 grid((seeds.size(0) + kIntBlockSize - 1) / kIntBlockSize);
return AT_DISPATCH_INDEX_TYPES(
index_dtype, "GpuGraphCache::Query", ([&] {
auto map = reinterpret_cast<map_t<index_t>*>(map_);
while ((
map_size_ + seeds.size(0) >= map->capacity() * kDoubleLoadFactor)) {
map->rehash_async(
map->capacity() * kIntGrowthFactor,
::cuda::stream_ref{cuda::GetCurrentStream()});
}
auto positions = torch::empty_like(seeds);
CUDA_KERNEL_CALL(
_QueryAndIncrement, grid, block, 0,
static_cast<int64_t>(seeds.size(0)), seeds.data_ptr<index_t>(),
positions.data_ptr<index_t>(), map->ref(cuco::insert_and_find));
auto num_threshold_new_hit =
allocator.AllocateStorage<thrust::tuple<int64_t, int64_t, int64_t>>(
1);
// Since threshold_ is a class member, we want the lambda functions
// below to only capture this particular variable by reassigning it to a
// local variable.
const auto threshold = -threshold_;
auto is_threshold_new_hit = thrust::make_transform_iterator(
positions.data_ptr<index_t>(), [=] __host__ __device__(index_t x) {
int64_t is_threshold = x == threshold;
int64_t is_new = x == -1;
int64_t is_hit = x >= 0;
return thrust::make_tuple(is_threshold, is_new, is_hit);
});
CUB_CALL(
DeviceReduce::Reduce, is_threshold_new_hit,
num_threshold_new_hit.get(), positions.size(0),
[] __host__ __device__(
const thrust::tuple<int64_t, int64_t, int64_t>& a,
const thrust::tuple<int64_t, int64_t, int64_t>& b) {
return thrust::make_tuple(
thrust::get<0>(a) + thrust::get<0>(b),
thrust::get<1>(a) + thrust::get<1>(b),
thrust::get<2>(a) + thrust::get<2>(b));
},
thrust::tuple<int64_t, int64_t, int64_t>{});
CopyScalar num_threshold_new_hit_cpu{num_threshold_new_hit.get()};
thrust::counting_iterator<index_t> iota{0};
auto position_and_index =
thrust::make_zip_iterator(positions.data_ptr<index_t>(), iota);
auto output_positions = torch::empty_like(seeds);
auto output_indices = torch::empty_like(seeds);
auto output_position_and_index = thrust::make_zip_iterator(
output_positions.data_ptr<index_t>(),
output_indices.data_ptr<index_t>());
CUB_CALL(
DevicePartition::If, position_and_index, output_position_and_index,
cub::DiscardOutputIterator{}, seeds.size(0),
[] __device__(thrust::tuple<index_t, index_t> & x) {
return thrust::get<0>(x) >= 0;
});
const auto [num_threshold, num_new, num_hit] =
static_cast<thrust::tuple<int64_t, int64_t, int64_t>>(
num_threshold_new_hit_cpu);
map_size_ += num_new;
return std::make_tuple(
output_indices, output_positions, num_hit, num_threshold);
}));
}
c10::intrusive_ptr<
Future<std::tuple<torch::Tensor, torch::Tensor, int64_t, int64_t>>>
GpuGraphCache::QueryAsync(torch::Tensor seeds) {
return async([=] { return Query(seeds); }, true);
}
std::tuple<torch::Tensor, std::vector<torch::Tensor>> GpuGraphCache::Replace(
torch::Tensor seeds, torch::Tensor indices, torch::Tensor positions,
int64_t num_hit, int64_t num_threshold, torch::Tensor indptr,
std::vector<torch::Tensor> edge_tensors) {
const auto with_edge_ids = offset_.has_value();
// The last element of edge_tensors has the edge ids.
const auto num_tensors = edge_tensors.size() - with_edge_ids;
TORCH_CHECK(
num_tensors == cached_edge_tensors_.size(),
"Same number of tensors need to be passed!");
const auto num_nodes = seeds.size(0);
TORCH_CHECK(
indptr.size(0) == num_nodes - num_hit + 1,
"(indptr.size(0) == seeds.size(0) - num_hit + 1) failed.");
std::lock_guard lock(mtx_);
const int64_t num_buffers = num_nodes * num_tensors;
auto allocator = cuda::GetAllocator();
auto index_dtype = cached_edge_tensors_.at(0).scalar_type();
return AT_DISPATCH_INDEX_TYPES(
index_dtype, "GpuGraphCache::Replace", ([&] {
using indices_t = index_t;
return AT_DISPATCH_INDEX_TYPES(
indptr_.scalar_type(), "GpuGraphCache::Replace::copy_prep", ([&] {
using indptr_t = index_t;
static_assert(
sizeof(int64_t) == sizeof(void*),
"Pointers have to be 64-bit.");
static_assert(
sizeof(std::byte) == 1, "Byte needs to have a size of 1.");
auto cache_missing_dtype = torch::empty(
// Below, we use this storage to store a tuple of 4 elements,
// since each element is 64-bit, we need 4x int64 storage.
4 * num_tensors, c10::TensorOptions()
.dtype(torch::kInt64)
.pinned_memory(true));
auto cache_missing_dtype_ptr =
reinterpret_cast<::cuda::std::tuple<
std::byte*, std::byte*, int64_t, int64_t>*>(
cache_missing_dtype.data_ptr());
int64_t total_size = 0;
for (size_t i = 0; i < num_tensors; i++) {
TORCH_CHECK(
cached_edge_tensors_[i].scalar_type() ==
edge_tensors[i].scalar_type(),
"The dtypes of edge tensors must match.");
if (i > 0) {
TORCH_CHECK(
edge_tensors[i - 1].size(0) == edge_tensors[i].size(0),
"The missing edge tensors should have identical size.");
}
const int64_t element_size = edge_tensors[i].element_size();
cache_missing_dtype_ptr[i] = {
reinterpret_cast<std::byte*>(
cached_edge_tensors_[i].data_ptr()),
reinterpret_cast<std::byte*>(edge_tensors[i].data_ptr()),
element_size, total_size};
total_size += element_size;
}
auto cache_missing_dtype_dev = allocator.AllocateStorage<
::cuda::std::tuple<std::byte*, std::byte*, int64_t, int64_t>>(
num_tensors);
THRUST_CALL(
copy_n, cache_missing_dtype_ptr, num_tensors,
cache_missing_dtype_dev.get());
auto input = allocator.AllocateStorage<std::byte*>(num_buffers);
auto input_size =
allocator.AllocateStorage<size_t>(num_buffers + 1);
torch::optional<torch::Tensor> edge_id_offsets;
if (with_edge_ids) {
edge_id_offsets = torch::empty(
num_nodes,
seeds.options().dtype(offset_.value().scalar_type()));
}
const auto cache_missing_dtype_dev_ptr =
cache_missing_dtype_dev.get();
const auto indices_ptr = indices.data_ptr<indices_t>();
const auto positions_ptr = positions.data_ptr<indices_t>();
const auto input_ptr = input.get();
const auto input_size_ptr = input_size.get();
const auto edge_id_offsets_ptr =
edge_id_offsets ? edge_id_offsets->data_ptr<indptr_t>()
: nullptr;
const auto cache_indptr = indptr_.data_ptr<indptr_t>();
const auto missing_indptr = indptr.data_ptr<indptr_t>();
const auto cache_offset =
offset_ ? offset_->data_ptr<indptr_t>() : nullptr;
const auto missing_edge_ids =
edge_id_offsets ? edge_tensors.back().data_ptr<indptr_t>()
: nullptr;
CUB_CALL(DeviceFor::Bulk, num_buffers, [=] __device__(int64_t i) {
const auto tensor_idx = i / num_nodes;
const auto idx = i % num_nodes;
const auto pos = positions_ptr[idx];
const auto original_idx = indices_ptr[idx];
const auto [cache_ptr, missing_ptr, size, cum_size] =
cache_missing_dtype_dev_ptr[tensor_idx];
const auto is_cached = pos >= 0;
const auto offset = is_cached ? cache_indptr[pos]
: missing_indptr[idx - num_hit];
const auto offset_end = is_cached
? cache_indptr[pos + 1]
: missing_indptr[idx - num_hit + 1];
const auto out_idx = tensor_idx * num_nodes + original_idx;
input_ptr[out_idx] =
(is_cached ? cache_ptr : missing_ptr) + offset * size;
input_size_ptr[out_idx] = size * (offset_end - offset);
if (edge_id_offsets_ptr && i < num_nodes) {
const auto edge_id =
is_cached ? cache_offset[pos] : missing_edge_ids[offset];
edge_id_offsets_ptr[out_idx] = edge_id;
}
});
auto output_indptr = torch::empty(
num_nodes + 1, seeds.options().dtype(indptr_.scalar_type()));
auto output_indptr_ptr = output_indptr.data_ptr<indptr_t>();
const auto element_size =
::cuda::std::get<2>(cache_missing_dtype_ptr[0]);
auto input_indegree = thrust::make_transform_iterator(
input_size_ptr, [=] __host__ __device__(size_t x) {
return x / element_size;
});
CUB_CALL(
DeviceScan::ExclusiveSum, input_indegree, output_indptr_ptr,
num_nodes + 1);
CopyScalar output_size{output_indptr_ptr + num_nodes};
if (num_threshold > 0) {
// Insert the vertices whose access count equal threshold.
auto missing_positions = positions.slice(0, num_hit);
auto missing_indices = indices.slice(0, num_hit);
thrust::counting_iterator<indices_t> iota{0};
auto threshold = -threshold_;
auto is_threshold = thrust::make_transform_iterator(
missing_positions.data_ptr<indices_t>(),
[=] __host__ __device__(indices_t x) {
return x == threshold;
});
auto output_indices =
torch::empty(num_threshold, seeds.options());
CUB_CALL(
DeviceSelect::Flagged, iota, is_threshold,
output_indices.data_ptr<indices_t>(),
cub::DiscardOutputIterator{}, missing_positions.size(0));
auto [in_degree, sliced_indptr] =
ops::SliceCSCIndptr(indptr, output_indices);
while (num_nodes_ + num_threshold >= indptr_.size(0)) {
auto new_indptr = torch::empty(
indptr_.size(0) * kIntGrowthFactor, indptr_.options());
new_indptr.slice(0, 0, indptr_.size(0)) = indptr_;
indptr_ = new_indptr;
if (offset_) {
auto new_offset =
torch::empty(indptr_.size(0) - 1, offset_->options());
new_offset.slice(0, 0, offset_->size(0)) = *offset_;
offset_ = new_offset;
}
}
torch::Tensor sindptr;
bool enough_space;
torch::optional<int64_t> cached_output_size;
for (size_t i = 0; i < num_tensors; i++) {
torch::Tensor sindices;
std::tie(sindptr, sindices) = ops::IndexSelectCSCImpl(
in_degree, sliced_indptr, edge_tensors[i], output_indices,
indptr.size(0) - 2, cached_output_size);
cached_output_size = sindices.size(0);
enough_space = num_edges_ + *cached_output_size <=
cached_edge_tensors_[i].size(0);
if (enough_space) {
cached_edge_tensors_[i].slice(
0, num_edges_, num_edges_ + *cached_output_size) =
sindices;
} else
break;
}
if (enough_space) {
auto num_edges = num_edges_;
if (offset_) {
auto transform_input_it = thrust::make_zip_iterator(
sindptr.data_ptr<indptr_t>() + 1,
sliced_indptr.data_ptr<indptr_t>());
auto transform_output_it = thrust::make_zip_iterator(
indptr_.data_ptr<indptr_t>() + num_nodes_ + 1,
offset_->data_ptr<indptr_t>() + num_nodes_);
THRUST_CALL(
transform, transform_input_it,
transform_input_it + sindptr.size(0) - 1,
transform_output_it,
[=] __host__ __device__(
const thrust::tuple<indptr_t, indptr_t>& x) {
return thrust::make_tuple(
thrust::get<0>(x) + num_edges,
missing_edge_ids[thrust::get<1>(x)]);
});
} else {
THRUST_CALL(
transform, sindptr.data_ptr<indptr_t>() + 1,
sindptr.data_ptr<indptr_t>() + sindptr.size(0),
indptr_.data_ptr<indptr_t>() + num_nodes_ + 1,
[=] __host__ __device__(const indptr_t& x) {
return x + num_edges;
});
}
auto map = reinterpret_cast<map_t<indices_t>*>(map_);
const dim3 block(kIntBlockSize);
const dim3 grid(
(num_threshold + kIntBlockSize - 1) / kIntBlockSize);
CUDA_KERNEL_CALL(
_Insert, grid, block, 0, output_indices.size(0),
static_cast<indices_t>(num_nodes_),
seeds.data_ptr<indices_t>(),
missing_indices.data_ptr<indices_t>(),
output_indices.data_ptr<indices_t>(),
map->ref(cuco::find));
num_edges_ += *cached_output_size;
num_nodes_ += num_threshold;
}
}
constexpr int alignment = 128;
const auto output_allocation_count =
(static_cast<indptr_t>(output_size) + alignment - 1) /
alignment * alignment;
auto output_allocation = torch::empty(
output_allocation_count * total_size,
seeds.options().dtype(torch::kInt8));
const auto output_allocation_ptr =
output_allocation.data_ptr<int8_t>();
std::vector<torch::Tensor> output_edge_tensors;
for (size_t i = 0; i < num_tensors; i++) {
const auto cum_size =
::cuda::std::get<3>(cache_missing_dtype_ptr[i]);
output_edge_tensors.push_back(
output_allocation
.slice(0, cum_size * output_allocation_count)
.view(edge_tensors[i].scalar_type())
.slice(0, 0, static_cast<indptr_t>(output_size)));
}
if (edge_id_offsets) {
// Append the edge ids as the last element of the output.
output_edge_tensors.push_back(ops::IndptrEdgeIdsImpl(
output_indptr, output_indptr.scalar_type(),
*edge_id_offsets,
static_cast<int64_t>(static_cast<indptr_t>(output_size))));
}
{
thrust::counting_iterator<int64_t> iota{0};
auto output_buffer_it = thrust::make_transform_iterator(
iota, [=] __host__ __device__(int64_t i) {
const auto tensor_idx = i / num_nodes;
const auto idx = i % num_nodes;
const auto offset = output_indptr_ptr[idx];
const auto [_0, _1, size, cum_size] =
cache_missing_dtype_dev_ptr[tensor_idx];
return output_allocation_ptr +
cum_size * output_allocation_count + offset * size;
});
constexpr int64_t max_copy_at_once =
std::numeric_limits<int32_t>::max();
for (int64_t i = 0; i < num_buffers; i += max_copy_at_once) {
CUB_CALL(
DeviceMemcpy::Batched, input.get() + i,
output_buffer_it + i, input_size_ptr + i,
std::min(num_buffers - i, max_copy_at_once));
}
}
return std::make_tuple(output_indptr, output_edge_tensors);
}));
}));
}
c10::intrusive_ptr<
Future<std::tuple<torch::Tensor, std::vector<torch::Tensor>>>>
GpuGraphCache::ReplaceAsync(
torch::Tensor seeds, torch::Tensor indices, torch::Tensor positions,
int64_t num_hit, int64_t num_threshold, torch::Tensor indptr,
std::vector<torch::Tensor> edge_tensors) {
return async(
[=] {
return Replace(
seeds, indices, positions, num_hit, num_threshold, indptr,
edge_tensors);
},
true);
}
} // namespace cuda
} // namespace graphbolt
@@ -0,0 +1,136 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/gpu_graph_cache.h
* @brief Header file of GPU graph cache.
*/
#ifndef GRAPHBOLT_GPU_GRAPH_CACHE_H_
#define GRAPHBOLT_GPU_GRAPH_CACHE_H_
#include <graphbolt/async.h>
#include <torch/custom_class.h>
#include <torch/torch.h>
#include <mutex>
namespace graphbolt {
namespace cuda {
class GpuGraphCache : public torch::CustomClassHolder {
// The load factor of the constructed hash table.
static constexpr double kDoubleLoadFactor = 0.8;
// The growth factor of the hash table and the dynamically sized indptr
// tensor.
static constexpr int kIntGrowthFactor = 2;
public:
/**
* @brief Constructor for the GpuGraphCache struct.
*
* @param num_edges The edge capacity of GPU cache.
* @param threshold The access threshold before a vertex neighborhood is
* cached.
* @param indptr_dtype The node id datatype.
* @param dtypes The dtypes of the edge tensors to be cached. dtypes[0] is
* reserved for the indices edge tensor holding node ids.
* @param has_original_edge_ids Whether the graph to be cached has original
* edge ids.
*/
GpuGraphCache(
const int64_t num_edges, const int64_t threshold,
torch::ScalarType indptr_dtype, std::vector<torch::ScalarType> dtypes,
bool has_original_edge_ids);
GpuGraphCache() = default;
~GpuGraphCache();
/**
* @brief Queries the cache. Returns tensors indicating which elements are
* missing.
*
* @param seeds The node ids to query the cache with.
*
* @return
* (torch::Tensor, torch::Tensor, int64_t, int64_t) index, position,
* number of cache hits and number of ids that will enter the cache.
*/
std::tuple<torch::Tensor, torch::Tensor, int64_t, int64_t> Query(
torch::Tensor seeds);
c10::intrusive_ptr<
Future<std::tuple<torch::Tensor, torch::Tensor, int64_t, int64_t>>>
QueryAsync(torch::Tensor seeds);
/**
* @brief After the graph structure for the missing node ids are fetched, it
* inserts the node ids which passes the threshold and returns the final
* output graph structure, combining the information in the cache with the
* graph structure for the missing node ids.
*
* @param seeds The node ids that the cache was queried with.
* @param indices seeds[indices[:num_hit]] gives us the node ids that were
* found in the cache
* @param positions positions[:num_hit] gives where the node ids can be found
* in the cache.
* @param num_hit The number of seeds that are already in the cache.
* @param num_threshold The number of seeds among the missing node ids that
* will be inserted into the cache.
* @param indptr The indptr for the missing seeds fetched from remote.
* @param edge_tensors The edge tensors for the missing seeds. The last
* element of edge_tensors is treated as the edge ids tensor with
* indptr_dtype.
*
* @return (torch::Tensor, std::vector<torch::Tensor>) The final indptr and
* edge_tensors, directly corresponding to the seeds tensor.
*/
std::tuple<torch::Tensor, std::vector<torch::Tensor>> Replace(
torch::Tensor seeds, torch::Tensor indices, torch::Tensor positions,
int64_t num_hit, int64_t num_threshold, torch::Tensor indptr,
std::vector<torch::Tensor> edge_tensors);
c10::intrusive_ptr<
Future<std::tuple<torch::Tensor, std::vector<torch::Tensor>>>>
ReplaceAsync(
torch::Tensor seeds, torch::Tensor indices, torch::Tensor positions,
int64_t num_hit, int64_t num_threshold, torch::Tensor indptr,
std::vector<torch::Tensor> edge_tensors);
static c10::intrusive_ptr<GpuGraphCache> Create(
const int64_t num_edges, const int64_t threshold,
torch::ScalarType indptr_dtype, std::vector<torch::ScalarType> dtypes,
bool has_original_edge_ids);
private:
void* map_; // pointer to the hash table.
int64_t threshold_; // A positive threshold value.
torch::DeviceIndex device_id_; // Which GPU the cache resides in.
int64_t map_size_; // The number of nodes inside the hash table.
int64_t num_nodes_; // The number of cached nodes in the cache.
int64_t num_edges_; // The number of cached edges in the cache.
torch::Tensor indptr_; // The cached graph structure indptr tensor.
torch::optional<torch::Tensor>
offset_; // The original graph's sliced_indptr tensor.
std::vector<torch::Tensor> cached_edge_tensors_; // The cached graph
// structure edge tensors.
std::mutex mtx_; // Protects the data structure and makes it threadsafe.
};
} // namespace cuda
} // namespace graphbolt
#endif // GRAPHBOLT_GPU_CACHE_H_
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/unique_and_compact.h
* @brief Unique and compact operator utilities on CUDA using hash table.
*/
#ifndef GRAPHBOLT_CUDA_UNIQUE_AND_COMPACT_H_
#define GRAPHBOLT_CUDA_UNIQUE_AND_COMPACT_H_
#include <torch/script.h>
#include <vector>
namespace graphbolt {
namespace ops {
std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>
UniqueAndCompactBatchedHashMapBased(
const std::vector<torch::Tensor>& src_ids,
const std::vector<torch::Tensor>& dst_ids,
const std::vector<torch::Tensor>& unique_dst_ids, const int64_t rank,
const int64_t world_size);
} // namespace ops
} // namespace graphbolt
#endif // GRAPHBOLT_CUDA_UNIQUE_AND_COMPACT_H_
@@ -0,0 +1,335 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/unique_and_compact_map.cu
* @brief Unique and compact operator implementation on CUDA using hash table.
*/
#include <graphbolt/cuda_ops.h>
#include <thrust/iterator/reverse_iterator.h>
#include <thrust/iterator/tabulate_output_iterator.h>
#include <thrust/iterator/transform_iterator.h>
#include <thrust/iterator/transform_output_iterator.h>
#include <cub/cub.cuh>
#include <cuco/static_map.cuh>
#include <cuda/functional>
#include <cuda/std/atomic>
#include <cuda/std/utility>
#include <cuda/stream_ref>
#include <limits>
#include <numeric>
#include "../common.h"
#include "../cooperative_minibatching_utils.cuh"
#include "../cooperative_minibatching_utils.h"
#include "../utils.h"
#include "./unique_and_compact.h"
namespace graphbolt {
namespace ops {
// Support graphs with up to 2^kNodeIdBits nodes.
constexpr int kNodeIdBits = 40;
template <typename index_t, typename map_t>
__global__ void _InsertAndSetMinBatched(
const int64_t num_edges, const int32_t* const indexes, index_t** pointers,
const int64_t* const offsets, map_t map) {
int64_t i = blockIdx.x * blockDim.x + threadIdx.x;
const int stride = gridDim.x * blockDim.x;
while (i < num_edges) {
const auto tensor_index = indexes[i];
const auto tensor_offset = i - offsets[tensor_index];
const int64_t node_id = pointers[tensor_index][tensor_offset];
const int64_t batch_index = tensor_index / 2;
const int64_t key = node_id | (batch_index << kNodeIdBits);
auto [slot, is_new_key] = map.insert_and_find(cuco::pair{key, i});
if (!is_new_key) {
auto ref = ::cuda::atomic_ref<int64_t, ::cuda::thread_scope_device>{
slot->second};
ref.fetch_min(i, ::cuda::memory_order_relaxed);
}
i += stride;
}
}
template <typename index_t, typename map_t>
__global__ void _MapIdsBatched(
const int num_batches, const int64_t num_edges,
const int32_t* const indexes, index_t** pointers,
const int64_t* const offsets, const int64_t* const unique_ids_offsets,
const index_t* const index, map_t map, index_t* mapped_ids) {
int64_t i = blockIdx.x * blockDim.x + threadIdx.x;
const int stride = gridDim.x * blockDim.x;
while (i < num_edges) {
const auto tensor_index = indexes[i];
int64_t batch_index;
if (tensor_index >= 2 * num_batches) {
batch_index = tensor_index - 2 * num_batches;
} else if (tensor_index & 1) {
batch_index = tensor_index / 2;
} else {
batch_index = -1;
}
// Only map src or dst ids.
if (batch_index >= 0) {
const auto tensor_offset = i - offsets[tensor_index];
const int64_t node_id = pointers[tensor_index][tensor_offset];
const int64_t key = node_id | (batch_index << kNodeIdBits);
auto slot = map.find(key);
auto new_id = slot->second;
if (index) {
new_id = index[new_id];
} else {
new_id -= unique_ids_offsets[batch_index];
}
mapped_ids[i] = new_id;
}
i += stride;
}
}
std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>
UniqueAndCompactBatchedHashMapBased(
const std::vector<torch::Tensor>& src_ids,
const std::vector<torch::Tensor>& dst_ids,
const std::vector<torch::Tensor>& unique_dst_ids, const int64_t rank,
const int64_t world_size) {
TORCH_CHECK(
rank < world_size, "rank needs to be smaller than the world_size.");
TORCH_CHECK(world_size <= std::numeric_limits<uint32_t>::max());
auto allocator = cuda::GetAllocator();
auto stream = cuda::GetCurrentStream();
auto scalar_type = src_ids.at(0).scalar_type();
constexpr int BLOCK_SIZE = 512;
const auto num_batches = src_ids.size();
static_assert(
sizeof(std::ptrdiff_t) == sizeof(int64_t),
"Need to be compiled on a 64-bit system.");
constexpr int batch_id_bits = sizeof(int64_t) * 8 - 1 - kNodeIdBits;
TORCH_CHECK(
num_batches <= (1 << batch_id_bits),
"UniqueAndCompactBatched supports a batch size of up to ",
1 << batch_id_bits);
return AT_DISPATCH_INDEX_TYPES(
scalar_type, "unique_and_compact", ([&] {
// For 2 batches of inputs, stores the input tensor pointers in the
// unique_dst, src, unique_dst, src, dst, dst order. Since there are
// 3 * num_batches input tensors, we need the first 3 * num_batches to
// store the input tensor pointers. Then, we store offsets in the rest
// of the 3 * num_batches + 1 space as if they were stored contiguously.
auto pointers_and_offsets = torch::empty(
6 * num_batches + 1,
c10::TensorOptions().dtype(torch::kInt64).pinned_memory(true));
// Points to the input tensor pointers.
auto pointers_ptr =
reinterpret_cast<index_t**>(pointers_and_offsets.data_ptr());
// Points to the input tensor storage logical offsets.
auto offsets_ptr =
pointers_and_offsets.data_ptr<int64_t>() + 3 * num_batches;
for (std::size_t i = 0; i < num_batches; i++) {
pointers_ptr[2 * i] = unique_dst_ids.at(i).data_ptr<index_t>();
offsets_ptr[2 * i] = unique_dst_ids[i].size(0);
pointers_ptr[2 * i + 1] = src_ids.at(i).data_ptr<index_t>();
offsets_ptr[2 * i + 1] = src_ids[i].size(0);
pointers_ptr[2 * num_batches + i] = dst_ids.at(i).data_ptr<index_t>();
offsets_ptr[2 * num_batches + i] = dst_ids[i].size(0);
}
// Finish computing the offsets by taking a cumulative sum.
std::exclusive_scan(
offsets_ptr, offsets_ptr + 3 * num_batches + 1, offsets_ptr, 0ll);
// Device version of the tensors defined above. We store the information
// initially on the CPU, which are later copied to the device.
auto pointers_and_offsets_dev = torch::empty(
pointers_and_offsets.size(0),
src_ids[0].options().dtype(pointers_and_offsets.scalar_type()));
auto offsets_dev = pointers_and_offsets_dev.slice(0, 3 * num_batches);
auto pointers_dev_ptr =
reinterpret_cast<index_t**>(pointers_and_offsets_dev.data_ptr());
auto offsets_dev_ptr = offsets_dev.data_ptr<int64_t>();
CUDA_CALL(cudaMemcpyAsync(
pointers_dev_ptr, pointers_ptr,
sizeof(int64_t) * pointers_and_offsets.size(0),
cudaMemcpyHostToDevice, stream));
auto indexes = ExpandIndptrImpl(
offsets_dev, torch::kInt32, torch::nullopt,
offsets_ptr[3 * num_batches]);
cuco::static_map map{
offsets_ptr[2 * num_batches],
0.5, // load_factor
cuco::empty_key{static_cast<int64_t>(-1)},
cuco::empty_value{static_cast<int64_t>(-1)},
{},
cuco::linear_probing<1, cuco::default_hash_function<int64_t>>{},
{},
{},
cuda::CUDAWorkspaceAllocator<cuco::pair<int64_t, int64_t>>{},
::cuda::stream_ref{stream},
};
C10_CUDA_KERNEL_LAUNCH_CHECK(); // Check the map constructor's success.
const dim3 block(BLOCK_SIZE);
const dim3 grid(
(offsets_ptr[2 * num_batches] + BLOCK_SIZE - 1) / BLOCK_SIZE);
CUDA_KERNEL_CALL(
_InsertAndSetMinBatched, grid, block, 0,
offsets_ptr[2 * num_batches], indexes.data_ptr<int32_t>(),
pointers_dev_ptr, offsets_dev_ptr, map.ref(cuco::insert_and_find));
cub::ArgIndexInputIterator index_it(indexes.data_ptr<int32_t>());
auto input_it = thrust::make_transform_iterator(
index_it,
::cuda::proclaim_return_type<
::cuda::std::tuple<int64_t*, index_t, int32_t, bool>>(
[=, map = map.ref(cuco::find)] __device__(auto it)
-> ::cuda::std::tuple<int64_t*, index_t, int32_t, bool> {
const auto i = it.key;
const auto tensor_index = it.value;
const auto tensor_offset = i - offsets_dev_ptr[tensor_index];
const int64_t node_id =
pointers_dev_ptr[tensor_index][tensor_offset];
const auto batch_index = tensor_index / 2;
const int64_t key =
node_id |
(static_cast<int64_t>(batch_index) << kNodeIdBits);
const auto batch_offset = offsets_dev_ptr[batch_index * 2];
auto slot = map.find(key);
const auto valid = slot->second == i;
return {&slot->second, node_id, batch_index, valid};
}));
torch::optional<torch::Tensor> part_ids;
if (world_size > 1) {
part_ids = torch::empty(
offsets_ptr[2 * num_batches],
src_ids[0].options().dtype(cuda::kPartDType));
}
auto unique_ids =
torch::empty(offsets_ptr[2 * num_batches], src_ids[0].options());
auto unique_ids_offsets_dev = torch::full(
num_batches + 1, std::numeric_limits<int64_t>::max(),
src_ids[0].options().dtype(torch::kInt64));
auto unique_ids_offsets_dev_ptr =
unique_ids_offsets_dev.data_ptr<int64_t>();
auto output_it = thrust::make_tabulate_output_iterator(
::cuda::proclaim_return_type<void>(
[=, unique_ids_ptr = unique_ids.data_ptr<index_t>(),
part_ids_ptr =
part_ids ? part_ids->data_ptr<cuda::part_t>() : nullptr,
rank = static_cast<uint32_t>(rank),
world_size = static_cast<uint32_t>(
world_size)] __device__(const int64_t i, const auto& t) {
*::cuda::std::get<0>(t) = i;
const auto node_id = ::cuda::std::get<1>(t);
unique_ids_ptr[i] = node_id;
if (part_ids_ptr) {
part_ids_ptr[i] =
cuda::rank_assignment(node_id, rank, world_size);
}
const auto batch_index = ::cuda::std::get<2>(t);
auto ref =
::cuda::atomic_ref<int64_t, ::cuda::thread_scope_device>{
unique_ids_offsets_dev_ptr[batch_index]};
ref.fetch_min(i, ::cuda::memory_order_relaxed);
}));
CUB_CALL(
DeviceSelect::If, input_it, output_it,
unique_ids_offsets_dev_ptr + num_batches,
offsets_ptr[2 * num_batches],
::cuda::proclaim_return_type<bool>([] __device__(const auto& t) {
return ::cuda::std::get<3>(t);
}));
auto unique_ids_offsets = torch::empty(
num_batches + 1,
c10::TensorOptions().dtype(torch::kInt64).pinned_memory(true));
{
auto unique_ids_offsets_dev2 =
torch::empty_like(unique_ids_offsets_dev);
CUB_CALL(
DeviceScan::InclusiveScan,
thrust::make_reverse_iterator(
num_batches + 1 + unique_ids_offsets_dev_ptr),
thrust::make_reverse_iterator(
num_batches + 1 +
thrust::make_transform_output_iterator(
thrust::make_zip_iterator(
unique_ids_offsets_dev2.data_ptr<int64_t>(),
unique_ids_offsets.data_ptr<int64_t>()),
::cuda::proclaim_return_type<
thrust::tuple<int64_t, int64_t>>(
[=] __device__(const auto x) {
return thrust::make_tuple(x, x);
}))),
cub::Min{}, num_batches + 1);
unique_ids_offsets_dev = unique_ids_offsets_dev2;
unique_ids_offsets_dev_ptr =
unique_ids_offsets_dev.data_ptr<int64_t>();
}
at::cuda::CUDAEvent unique_ids_offsets_event;
unique_ids_offsets_event.record();
torch::optional<torch::Tensor> index;
if (part_ids) {
unique_ids_offsets_event.synchronize();
const auto num_unique =
unique_ids_offsets.data_ptr<int64_t>()[num_batches];
unique_ids = unique_ids.slice(0, 0, num_unique);
part_ids = part_ids->slice(0, 0, num_unique);
std::tie(
unique_ids, index, unique_ids_offsets, unique_ids_offsets_event) =
cuda::RankSortImpl(
unique_ids, *part_ids, unique_ids_offsets_dev, world_size);
}
auto mapped_ids =
torch::empty(offsets_ptr[3 * num_batches], unique_ids.options());
CUDA_KERNEL_CALL(
_MapIdsBatched, grid, block, 0, num_batches,
offsets_ptr[3 * num_batches], indexes.data_ptr<int32_t>(),
pointers_dev_ptr, offsets_dev_ptr, unique_ids_offsets_dev_ptr,
index ? index->data_ptr<index_t>() : nullptr, map.ref(cuco::find),
mapped_ids.data_ptr<index_t>());
std::vector<std::tuple<
torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>
results;
unique_ids_offsets_event.synchronize();
auto unique_ids_offsets_ptr = unique_ids_offsets.data_ptr<int64_t>();
for (int64_t i = 0; i < num_batches; i++) {
results.emplace_back(
unique_ids.slice(
0, unique_ids_offsets_ptr[i * world_size],
unique_ids_offsets_ptr[(i + 1) * world_size]),
mapped_ids.slice(
0, offsets_ptr[2 * i + 1], offsets_ptr[2 * i + 2]),
mapped_ids.slice(
0, offsets_ptr[2 * num_batches + i],
offsets_ptr[2 * num_batches + i + 1]),
unique_ids_offsets.slice(
0, i * world_size, (i + 1) * world_size + 1));
}
return results;
}));
}
} // namespace ops
} // namespace graphbolt
+50
View File
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/gather.cu
* @brief Gather operators implementation on CUDA.
*/
#include <thrust/gather.h>
#include "./common.h"
namespace graphbolt {
namespace ops {
torch::Tensor Gather(
torch::Tensor input, torch::Tensor index,
torch::optional<torch::ScalarType> dtype) {
if (!dtype.has_value()) dtype = input.scalar_type();
auto output = torch::empty(index.sizes(), index.options().dtype(*dtype));
AT_DISPATCH_INDEX_TYPES(
index.scalar_type(), "GatherIndexType", ([&] {
AT_DISPATCH_INTEGRAL_TYPES(
input.scalar_type(), "GatherInputType", ([&] {
using input_t = scalar_t;
AT_DISPATCH_INTEGRAL_TYPES(*dtype, "GatherOutputType", ([&] {
using output_t = scalar_t;
THRUST_CALL(
gather, index.data_ptr<index_t>(),
index.data_ptr<index_t>() + index.size(0),
input.data_ptr<input_t>(), output.data_ptr<output_t>());
}));
}));
}));
return output;
}
} // namespace ops
} // namespace graphbolt
+335
View File
@@ -0,0 +1,335 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/index_select_csc_impl.cu
* @brief Index select csc operator implementation on CUDA.
*/
#include <c10/core/ScalarType.h>
#include <graphbolt/cuda_ops.h>
#include <thrust/iterator/counting_iterator.h>
#include <thrust/iterator/transform_iterator.h>
#include <thrust/iterator/zip_iterator.h>
#include <cstdint>
#include <cub/cub.cuh>
#include <numeric>
#include "./common.h"
#include "./max_uva_threads.h"
#include "./utils.h"
namespace graphbolt {
namespace ops {
constexpr int BLOCK_SIZE = CUDA_MAX_NUM_THREADS;
// Given the in_degree array and a permutation, returns in_degree of the output
// and the permuted and modified in_degree of the input. The modified in_degree
// is modified so that there is slack to be able to align as needed.
template <typename indptr_t, typename indices_t>
struct AlignmentFunc {
static_assert(GPU_CACHE_LINE_SIZE % sizeof(indices_t) == 0);
const indptr_t* in_degree;
const int64_t* perm;
int64_t num_nodes;
__host__ __device__ auto operator()(int64_t row) {
constexpr int num_elements = GPU_CACHE_LINE_SIZE / sizeof(indices_t);
return thrust::make_tuple(
in_degree[row],
// A single cache line has num_elements items, we add num_elements - 1
// to ensure there is enough slack to move forward or backward by
// num_elements - 1 items if the performed access is not aligned.
static_cast<indptr_t>(
in_degree[perm ? perm[row % num_nodes] : row] + num_elements - 1));
}
};
template <typename indptr_t, typename indices_t, typename coo_rows_t>
__global__ void _CopyIndicesAlignedKernel(
const indptr_t edge_count, const indptr_t* const indptr,
const indptr_t* const output_indptr,
const indptr_t* const output_indptr_aligned, const indices_t* const indices,
const coo_rows_t* const coo_aligned_rows, indices_t* const output_indices,
const int64_t* const perm) {
indptr_t idx = static_cast<indptr_t>(blockIdx.x) * blockDim.x + threadIdx.x;
const int stride_x = gridDim.x * blockDim.x;
while (idx < edge_count) {
const auto permuted_row_pos = coo_aligned_rows[idx];
const auto row_pos = perm ? perm[permuted_row_pos] : permuted_row_pos;
const auto out_row = output_indptr[row_pos];
const auto d = output_indptr[row_pos + 1] - out_row;
const int offset = (reinterpret_cast<std::uintptr_t>(
indices + indptr[row_pos] -
output_indptr_aligned[permuted_row_pos]) %
GPU_CACHE_LINE_SIZE) /
sizeof(indices_t);
const auto rofs = idx - output_indptr_aligned[permuted_row_pos] - offset;
if (rofs >= 0 && rofs < d) {
const auto in_idx = indptr[row_pos] + rofs;
assert(
reinterpret_cast<std::uintptr_t>(indices + in_idx - idx) %
GPU_CACHE_LINE_SIZE ==
0);
const auto u = indices[in_idx];
output_indices[out_row + rofs] = u;
}
idx += stride_x;
}
}
struct PairSum {
template <typename indptr_t>
__host__ __device__ auto operator()(
const thrust::tuple<indptr_t, indptr_t> a,
const thrust::tuple<indptr_t, indptr_t> b) {
return thrust::make_tuple(
thrust::get<0>(a) + thrust::get<0>(b),
thrust::get<1>(a) + thrust::get<1>(b));
};
};
template <typename indptr_t, typename indices_t>
std::tuple<torch::Tensor, torch::Tensor> UVAIndexSelectCSCCopyIndices(
torch::Tensor indices, const int64_t num_nodes,
const indptr_t* const in_degree, const indptr_t* const sliced_indptr,
const int64_t* const perm, torch::TensorOptions options,
torch::ScalarType indptr_scalar_type,
torch::optional<int64_t> output_size) {
auto allocator = cuda::GetAllocator();
thrust::counting_iterator<int64_t> iota(0);
// Output indptr for the slice indexed by nodes.
auto output_indptr =
torch::empty(num_nodes + 1, options.dtype(indptr_scalar_type));
auto output_indptr_aligned =
torch::empty(num_nodes + 1, options.dtype(indptr_scalar_type));
auto output_indptr_aligned_ptr = output_indptr_aligned.data_ptr<indptr_t>();
{
// Returns the actual and modified_indegree as a pair, the
// latter overestimates the actual indegree for alignment
// purposes.
auto modified_in_degree = thrust::make_transform_iterator(
iota, AlignmentFunc<indptr_t, indices_t>{in_degree, perm, num_nodes});
auto output_indptr_pair = thrust::make_zip_iterator(
output_indptr.data_ptr<indptr_t>(), output_indptr_aligned_ptr);
thrust::tuple<indptr_t, indptr_t> zero_value{};
// Compute the prefix sum over actual and modified indegrees.
CUB_CALL(
DeviceScan::ExclusiveScan, modified_in_degree, output_indptr_pair,
PairSum{}, zero_value, num_nodes + 1);
}
// Copy the actual total number of edges.
if (!output_size.has_value()) {
auto edge_count =
cuda::CopyScalar{output_indptr.data_ptr<indptr_t>() + num_nodes};
output_size = static_cast<indptr_t>(edge_count);
}
// Copy the modified number of edges.
auto edge_count_aligned_ =
cuda::CopyScalar{output_indptr_aligned_ptr + num_nodes};
const int64_t edge_count_aligned = static_cast<indptr_t>(edge_count_aligned_);
// Allocate output array with actual number of edges.
torch::Tensor output_indices =
torch::empty(output_size.value(), options.dtype(indices.scalar_type()));
const dim3 block(BLOCK_SIZE);
const dim3 grid(
(std::min(edge_count_aligned, cuda::max_uva_threads.value_or(1 << 20)) +
BLOCK_SIZE - 1) /
BLOCK_SIZE);
// Find the smallest integer type to store the coo_aligned_rows tensor.
const int num_bits = cuda::NumberOfBits(num_nodes);
std::array<int, 4> type_bits = {8, 15, 31, 63};
const auto type_index =
std::lower_bound(type_bits.begin(), type_bits.end(), num_bits) -
type_bits.begin();
std::array<torch::ScalarType, 5> types = {
torch::kByte, torch::kInt16, torch::kInt32, torch::kLong, torch::kLong};
auto coo_dtype = types[type_index];
auto coo_aligned_rows = ExpandIndptrImpl(
output_indptr_aligned, coo_dtype, torch::nullopt, edge_count_aligned);
AT_DISPATCH_INTEGRAL_TYPES(
coo_dtype, "UVAIndexSelectCSCCopyIndicesCOO", ([&] {
using coo_rows_t = scalar_t;
// Perform the actual copying, of the indices array into
// output_indices in an aligned manner.
CUDA_KERNEL_CALL(
_CopyIndicesAlignedKernel, grid, block, 0,
static_cast<indptr_t>(edge_count_aligned_), sliced_indptr,
output_indptr.data_ptr<indptr_t>(), output_indptr_aligned_ptr,
reinterpret_cast<indices_t*>(indices.data_ptr()),
coo_aligned_rows.data_ptr<coo_rows_t>(),
reinterpret_cast<indices_t*>(output_indices.data_ptr()), perm);
}));
return {output_indptr, output_indices};
}
std::tuple<torch::Tensor, torch::Tensor> UVAIndexSelectCSCImpl(
torch::Tensor in_degree, torch::Tensor sliced_indptr, torch::Tensor indices,
torch::Tensor nodes, int num_bits, torch::optional<int64_t> output_size) {
// Sorting nodes so that accesses over PCI-e are more regular.
const auto sorted_idx = Sort(nodes, num_bits).second;
const int64_t num_nodes = nodes.size(0);
return AT_DISPATCH_INTEGRAL_TYPES(
sliced_indptr.scalar_type(), "UVAIndexSelectCSCIndptr", ([&] {
using indptr_t = scalar_t;
return GRAPHBOLT_DISPATCH_ELEMENT_SIZES(
indices.element_size(), "UVAIndexSelectCSCCopyIndices", ([&] {
return UVAIndexSelectCSCCopyIndices<indptr_t, element_size_t>(
indices, num_nodes, in_degree.data_ptr<indptr_t>(),
sliced_indptr.data_ptr<indptr_t>(),
sorted_idx.data_ptr<int64_t>(), nodes.options(),
sliced_indptr.scalar_type(), output_size);
}));
}));
}
template <typename indptr_t, typename indices_t>
struct IteratorFunc {
indptr_t* indptr;
indices_t* indices;
__host__ __device__ auto operator()(int64_t i) { return indices + indptr[i]; }
};
template <typename indptr_t, typename indices_t>
struct ConvertToBytes {
const indptr_t* in_degree;
__host__ __device__ indptr_t operator()(int64_t i) {
return in_degree[i] * sizeof(indices_t);
}
};
template <typename indptr_t, typename indices_t>
void IndexSelectCSCCopyIndices(
const int64_t num_nodes, indices_t* const indices,
indptr_t* const sliced_indptr, const indptr_t* const in_degree,
indptr_t* const output_indptr, indices_t* const output_indices) {
thrust::counting_iterator<int64_t> iota(0);
auto input_buffer_it = thrust::make_transform_iterator(
iota, IteratorFunc<indptr_t, indices_t>{sliced_indptr, indices});
auto output_buffer_it = thrust::make_transform_iterator(
iota, IteratorFunc<indptr_t, indices_t>{output_indptr, output_indices});
auto buffer_sizes = thrust::make_transform_iterator(
iota, ConvertToBytes<indptr_t, indices_t>{in_degree});
constexpr int64_t max_copy_at_once = std::numeric_limits<int32_t>::max();
// Performs the copy from indices into output_indices.
for (int64_t i = 0; i < num_nodes; i += max_copy_at_once) {
CUB_CALL(
DeviceMemcpy::Batched, input_buffer_it + i, output_buffer_it + i,
buffer_sizes + i, std::min(num_nodes - i, max_copy_at_once));
}
}
std::tuple<torch::Tensor, torch::Tensor> DeviceIndexSelectCSCImpl(
torch::Tensor in_degree, torch::Tensor sliced_indptr, torch::Tensor indices,
torch::TensorOptions options, torch::optional<int64_t> output_size) {
const int64_t num_nodes = sliced_indptr.size(0);
return AT_DISPATCH_INTEGRAL_TYPES(
sliced_indptr.scalar_type(), "IndexSelectCSCIndptr", ([&] {
using indptr_t = scalar_t;
auto in_degree_ptr = in_degree.data_ptr<indptr_t>();
auto sliced_indptr_ptr = sliced_indptr.data_ptr<indptr_t>();
// Output indptr for the slice indexed by nodes.
torch::Tensor output_indptr = torch::empty(
num_nodes + 1, options.dtype(sliced_indptr.scalar_type()));
// Compute the output indptr, output_indptr.
CUB_CALL(
DeviceScan::ExclusiveSum, in_degree_ptr,
output_indptr.data_ptr<indptr_t>(), num_nodes + 1);
// Number of edges being copied.
if (!output_size.has_value()) {
auto edge_count =
cuda::CopyScalar{output_indptr.data_ptr<indptr_t>() + num_nodes};
output_size = static_cast<indptr_t>(edge_count);
}
// Allocate output array of size number of copied edges.
torch::Tensor output_indices = torch::empty(
output_size.value(), options.dtype(indices.scalar_type()));
GRAPHBOLT_DISPATCH_ELEMENT_SIZES(
indices.element_size(), "IndexSelectCSCCopyIndices", ([&] {
using indices_t = element_size_t;
IndexSelectCSCCopyIndices<indptr_t, indices_t>(
num_nodes, reinterpret_cast<indices_t*>(indices.data_ptr()),
sliced_indptr_ptr, in_degree_ptr,
output_indptr.data_ptr<indptr_t>(),
reinterpret_cast<indices_t*>(output_indices.data_ptr()));
}));
return std::make_tuple(output_indptr, output_indices);
}));
}
std::tuple<torch::Tensor, torch::Tensor> IndexSelectCSCImpl(
torch::Tensor in_degree, torch::Tensor sliced_indptr, torch::Tensor indices,
torch::Tensor nodes, int64_t nodes_max,
torch::optional<int64_t> output_size) {
if (indices.is_pinned()) {
int num_bits = cuda::NumberOfBits(nodes_max + 1);
return UVAIndexSelectCSCImpl(
in_degree, sliced_indptr, indices, nodes, num_bits, output_size);
} else {
return DeviceIndexSelectCSCImpl(
in_degree, sliced_indptr, indices, nodes.options(), output_size);
}
}
std::tuple<torch::Tensor, torch::Tensor> IndexSelectCSCImpl(
torch::Tensor indptr, torch::Tensor indices, torch::Tensor nodes,
torch::optional<int64_t> output_size) {
auto [in_degree, sliced_indptr] = SliceCSCIndptr(indptr, nodes);
return IndexSelectCSCImpl(
in_degree, sliced_indptr, indices, nodes, indptr.size(0) - 2,
output_size);
}
std::tuple<torch::Tensor, std::vector<torch::Tensor>> IndexSelectCSCBatchedImpl(
torch::Tensor indptr, std::vector<torch::Tensor> indices_list,
torch::Tensor nodes, bool with_edge_ids,
torch::optional<int64_t> output_size) {
auto [in_degree, sliced_indptr] = SliceCSCIndptr(indptr, nodes);
std::vector<torch::Tensor> results;
results.reserve(indices_list.size());
torch::Tensor output_indptr;
for (auto& indices : indices_list) {
torch::Tensor output_indices;
std::tie(output_indptr, output_indices) = IndexSelectCSCImpl(
in_degree, sliced_indptr, indices, nodes, indptr.size(0) - 2,
output_size);
if (!output_size.has_value()) output_size = output_indices.size(0);
TORCH_CHECK(*output_size == output_indices.size(0));
results.push_back(output_indices);
}
if (with_edge_ids) {
results.push_back(IndptrEdgeIdsImpl(
output_indptr, sliced_indptr.scalar_type(), sliced_indptr,
output_size));
}
return {output_indptr, results};
}
} // namespace ops
} // namespace graphbolt
+208
View File
@@ -0,0 +1,208 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/index_select_impl.cu
* @brief Index select operator implementation on CUDA.
*/
#include <c10/core/ScalarType.h>
#include <graphbolt/cuda_ops.h>
#include <numeric>
#include "./common.h"
#include "./max_uva_threads.h"
#include "./utils.h"
namespace graphbolt {
namespace ops {
/** @brief Index select operator implementation for feature size 1. */
template <typename DType, typename IdType>
__global__ void IndexSelectSingleKernel(
const DType* input, const int64_t input_len, const IdType* index,
const int64_t output_len, DType* output,
const int64_t* permutation = nullptr) {
int64_t out_row_index = blockIdx.x * blockDim.x + threadIdx.x;
int stride = gridDim.x * blockDim.x;
while (out_row_index < output_len) {
assert(index[out_row_index] >= 0 && index[out_row_index] < input_len);
const auto out_row =
permutation ? permutation[out_row_index] : out_row_index;
output[out_row] = input[index[out_row_index]];
out_row_index += stride;
}
}
/**
* @brief Index select operator implementation for feature size > 1.
*/
template <typename DType, typename IdType>
__global__ void IndexSelectMultiKernel(
const DType* const input, const int64_t input_len,
const int64_t feature_size, const IdType* const index,
const int64_t output_len, DType* const output,
const int64_t* permutation = nullptr) {
int64_t out_row_index = blockIdx.x * blockDim.y + threadIdx.y;
const int64_t stride = blockDim.y * gridDim.x;
while (out_row_index < output_len) {
int64_t column = threadIdx.x;
const int64_t in_row = index[out_row_index];
assert(in_row >= 0 && in_row < input_len);
const auto out_row =
permutation ? permutation[out_row_index] : out_row_index;
while (column < feature_size) {
output[out_row * feature_size + column] =
input[in_row * feature_size + column];
column += blockDim.x;
}
out_row_index += stride;
}
}
/**
* @brief Index select operator implementation for feature size > 1.
*
* @note This is a cross-device access version of IndexSelectMultiKernel. Since
* the memory access over PCIe is more sensitive to the data access aligment
* (cacheline), we need a separate version here.
*/
template <typename DType, typename IdType>
__global__ void IndexSelectMultiKernelAligned(
const DType* const input, const int64_t input_len,
const int64_t feature_size, const IdType* const index,
const int64_t output_len, DType* const output,
const int64_t* permutation = nullptr) {
int64_t out_row_index = blockIdx.x * blockDim.y + threadIdx.y;
const int64_t stride = blockDim.y * gridDim.x;
while (out_row_index < output_len) {
int64_t col = threadIdx.x;
const int64_t in_row = index[out_row_index];
assert(in_row >= 0 && in_row < input_len);
const int64_t idx_offset =
((uint64_t)(&input[in_row * feature_size]) % GPU_CACHE_LINE_SIZE) /
sizeof(DType);
col = col - idx_offset;
const auto out_row =
permutation ? permutation[out_row_index] : out_row_index;
while (col < feature_size) {
if (col >= 0)
output[out_row * feature_size + col] =
input[in_row * feature_size + col];
col += blockDim.x;
}
out_row_index += stride;
}
}
template <typename DType, typename IdType>
torch::Tensor UVAIndexSelectImpl_(torch::Tensor input, torch::Tensor index) {
const int64_t input_len = input.size(0);
const int64_t return_len = index.size(0);
const int64_t original_feature_size = std::accumulate(
input.sizes().begin() + 1, input.sizes().end(), 1ll, std::multiplies<>());
const auto aligned_feature_size =
input.element_size() * original_feature_size / sizeof(DType);
torch::Tensor ret = torch::empty(
{return_len, original_feature_size}, torch::TensorOptions()
.dtype(input.dtype())
.device(c10::DeviceType::CUDA));
DType* input_ptr = reinterpret_cast<DType*>(input.data_ptr());
DType* ret_ptr = reinterpret_cast<DType*>(ret.data_ptr());
// Sort the index to improve the memory access pattern.
torch::Tensor sorted_index, permutation;
std::tie(sorted_index, permutation) =
Sort(index, cuda::NumberOfBits(input_len));
const IdType* index_sorted_ptr = sorted_index.data_ptr<IdType>();
const int64_t* permutation_ptr = permutation.data_ptr<int64_t>();
if (aligned_feature_size == 1) {
// Use a single thread to process each output row to avoid wasting threads.
const int num_threads = cuda::FindNumThreads(return_len);
const int num_blocks =
(std::min(return_len, cuda::max_uva_threads.value_or(1 << 20)) +
num_threads - 1) /
num_threads;
CUDA_KERNEL_CALL(
IndexSelectSingleKernel, num_blocks, num_threads, 0, input_ptr,
input_len, index_sorted_ptr, return_len, ret_ptr, permutation_ptr);
} else {
constexpr int BLOCK_SIZE = CUDA_MAX_NUM_THREADS;
dim3 block(BLOCK_SIZE, 1);
while (static_cast<int64_t>(block.x) >= 2 * aligned_feature_size) {
block.x >>= 1;
block.y <<= 1;
}
const dim3 grid(std::min(
(return_len + block.y - 1) / block.y,
cuda::max_uva_threads.value_or(1 << 20) / BLOCK_SIZE));
if (aligned_feature_size * sizeof(DType) <= GPU_CACHE_LINE_SIZE) {
// When feature size is smaller than GPU cache line size, use unaligned
// version for less SM usage, which is more resource efficient.
CUDA_KERNEL_CALL(
IndexSelectMultiKernel, grid, block, 0, input_ptr, input_len,
aligned_feature_size, index_sorted_ptr, return_len, ret_ptr,
permutation_ptr);
} else {
// Use aligned version to improve the memory access pattern.
CUDA_KERNEL_CALL(
IndexSelectMultiKernelAligned, grid, block, 0, input_ptr, input_len,
aligned_feature_size, index_sorted_ptr, return_len, ret_ptr,
permutation_ptr);
}
}
auto return_shape = std::vector<int64_t>({return_len});
return_shape.insert(
return_shape.end(), input.sizes().begin() + 1, input.sizes().end());
ret = ret.reshape(return_shape);
return ret;
}
/**
* @brief UVA index select operator implementation on CUDA.
*
* All basic torch types are supported for input.
* The supporting index types are: int, int64_t.
*/
torch::Tensor UVAIndexSelectImpl(torch::Tensor input, torch::Tensor index) {
return AT_DISPATCH_INDEX_TYPES(
index.scalar_type(), "UVAIndexSelectImpl", ([&] {
const auto ptr = (size_t)input.data_ptr();
const int64_t feature_size = std::accumulate(
input.sizes().begin() + 1, input.sizes().end(), 1ll,
std::multiplies<>());
// We perform the copy with datatype of size powers of 2, and the
// maximum data type we use has 16 bytes. We check the alignment of the
// pointer and the feature dimensionality to determine the largest
// type to use for the copy to minimize the number of CUDA threads used.
// Alignment denotes the maximum suitable alignment and datatype size
// for the copies.
const int aligned_access_size =
std::gcd(16, std::gcd(ptr, input.element_size() * feature_size));
return GRAPHBOLT_DISPATCH_ELEMENT_SIZES(
aligned_access_size, "UVAIndexSelectImplElementSize", ([&] {
return UVAIndexSelectImpl_<element_size_t, index_t>(input, index);
}));
}));
}
} // namespace ops
} // namespace graphbolt
+51
View File
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/insubgraph.cu
* @brief InSubgraph operator implementation on CUDA.
*/
#include <graphbolt/cuda_ops.h>
#include <graphbolt/cuda_sampling_ops.h>
#include "./common.h"
namespace graphbolt {
namespace ops {
c10::intrusive_ptr<sampling::FusedSampledSubgraph> InSubgraph(
torch::Tensor indptr, torch::Tensor indices, torch::Tensor nodes,
torch::optional<torch::Tensor> type_per_edge) {
auto [in_degree, sliced_indptr] = SliceCSCIndptr(indptr, nodes);
auto [output_indptr, output_indices] = IndexSelectCSCImpl(
in_degree, sliced_indptr, indices, nodes, indptr.size(0) - 2);
const int64_t num_edges = output_indices.size(0);
torch::optional<torch::Tensor> output_type_per_edge;
if (type_per_edge) {
output_type_per_edge = std::get<1>(IndexSelectCSCImpl(
in_degree, sliced_indptr, type_per_edge.value(), nodes,
indptr.size(0) - 2, num_edges));
}
auto edge_ids = IndptrEdgeIdsImpl(
output_indptr, sliced_indptr.scalar_type(), sliced_indptr, num_edges);
return c10::make_intrusive<sampling::FusedSampledSubgraph>(
output_indptr, output_indices, edge_ids, nodes, torch::nullopt,
output_type_per_edge);
}
} // namespace ops
} // namespace graphbolt
+68
View File
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/isin.cu
* @brief IsIn operator implementation on CUDA.
*/
#include <graphbolt/cuda_ops.h>
#include <thrust/binary_search.h>
#include <cub/cub.cuh>
#include "./common.h"
namespace graphbolt {
namespace ops {
torch::Tensor IsIn(torch::Tensor elements, torch::Tensor test_elements) {
auto sorted_test_elements = Sort<false>(test_elements);
auto result = torch::empty_like(elements, torch::kBool);
AT_DISPATCH_INTEGRAL_TYPES(
elements.scalar_type(), "IsInOperation", ([&] {
THRUST_CALL(
binary_search, sorted_test_elements.data_ptr<scalar_t>(),
sorted_test_elements.data_ptr<scalar_t>() +
sorted_test_elements.size(0),
elements.data_ptr<scalar_t>(),
elements.data_ptr<scalar_t>() + elements.size(0),
result.data_ptr<bool>());
}));
return result;
}
torch::Tensor Nonzero(torch::Tensor mask, bool logical_not) {
thrust::counting_iterator<int64_t> iota(0);
auto result = torch::empty_like(mask, torch::kInt64);
auto mask_ptr = mask.data_ptr<bool>();
auto result_ptr = result.data_ptr<int64_t>();
auto allocator = cuda::GetAllocator();
auto num_copied = allocator.AllocateStorage<int64_t>(1);
if (logical_not) {
CUB_CALL(
DeviceSelect::FlaggedIf, iota, mask_ptr, result_ptr, num_copied.get(),
mask.numel(), thrust::logical_not<bool>{});
} else {
CUB_CALL(
DeviceSelect::Flagged, iota, mask_ptr, result_ptr, num_copied.get(),
mask.numel());
}
cuda::CopyScalar num_copied_cpu(num_copied.get());
return result.slice(0, 0, static_cast<int64_t>(num_copied_cpu));
}
} // namespace ops
} // namespace graphbolt
+28
View File
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/max_uva_threads.cc
* @brief Max uva threads variable setter function.
*/
#include "./max_uva_threads.h"
namespace graphbolt {
namespace cuda {
void set_max_uva_threads(int64_t count) { max_uva_threads = count; }
} // namespace cuda
} // namespace graphbolt
+37
View File
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/max_uva_threads.h
* @brief Max uva threads variable declaration.
*/
#ifndef GRAPHBOLT_MAX_UVA_THREADS_H_
#define GRAPHBOLT_MAX_UVA_THREADS_H_
#include <cstdint>
#include <optional>
namespace graphbolt {
namespace cuda {
/** @brief Set a limit on the number of CUDA threads for UVA accesses. */
inline std::optional<int64_t> max_uva_threads;
void set_max_uva_threads(int64_t count);
} // namespace cuda
} // namespace graphbolt
#endif // GRAPHBOLT_MAX_UVA_THREADS_H_
+807
View File
@@ -0,0 +1,807 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/index_select_impl.cu
* @brief Index select operator implementation on CUDA.
*/
#include <c10/core/ScalarType.h>
#include <curand_kernel.h>
#include <graphbolt/continuous_seed.h>
#include <graphbolt/cuda_ops.h>
#include <graphbolt/cuda_sampling_ops.h>
#include <thrust/copy.h>
#include <thrust/gather.h>
#include <thrust/iterator/counting_iterator.h>
#include <thrust/iterator/transform_iterator.h>
#include <thrust/iterator/transform_output_iterator.h>
#include <algorithm>
#include <array>
#include <cub/cub.cuh>
#if __CUDA_ARCH__ >= 700
#include <cuda/atomic>
#endif // __CUDA_ARCH__ >= 700
#include <limits>
#include <numeric>
#include <type_traits>
#include "../macro.h"
#include "../random.h"
#include "../utils.h"
#include "./common.h"
#include "./utils.h"
namespace graphbolt {
namespace ops {
constexpr int BLOCK_SIZE = 128;
inline __device__ int64_t AtomicMax(int64_t* const address, const int64_t val) {
// To match the type of "::atomicCAS", ignore lint warning.
using Type = unsigned long long int; // NOLINT
static_assert(sizeof(Type) == sizeof(*address), "Type width must match");
return atomicMax(reinterpret_cast<Type*>(address), static_cast<Type>(val));
}
inline __device__ int32_t AtomicMax(int32_t* const address, const int32_t val) {
// To match the type of "::atomicCAS", ignore lint warning.
using Type = int; // NOLINT
static_assert(sizeof(Type) == sizeof(*address), "Type width must match");
return atomicMax(reinterpret_cast<Type*>(address), static_cast<Type>(val));
}
/**
* @brief Performs neighbor sampling and fills the edge_ids array with
* original edge ids if sliced_indptr is valid. If not, then it fills the edge
* ids array with numbers upto the node degree.
*/
template <typename indptr_t, typename indices_t>
__global__ void _ComputeRandomsNS(
const int64_t num_edges, const indptr_t* const sliced_indptr,
const indptr_t* const sub_indptr, const indptr_t* const output_indptr,
const indices_t* const csr_rows, const uint64_t random_seed,
indptr_t* edge_ids) {
int64_t i = blockIdx.x * blockDim.x + threadIdx.x;
const int stride = gridDim.x * blockDim.x;
curandStatePhilox4_32_10_t rng;
curand_init(random_seed, i, 0, &rng);
while (i < num_edges) {
const auto row_position = csr_rows[i];
const auto row_offset = i - sub_indptr[row_position];
const auto output_offset = output_indptr[row_position];
const auto fanout = output_indptr[row_position + 1] - output_offset;
const auto rnd =
row_offset < fanout ? row_offset : curand(&rng) % (row_offset + 1);
if (rnd < fanout) {
const indptr_t edge_id =
row_offset + (sliced_indptr ? sliced_indptr[row_position] : 0);
#if __CUDA_ARCH__ >= 700
::cuda::atomic_ref<indptr_t, ::cuda::thread_scope_device> a(
edge_ids[output_offset + rnd]);
a.fetch_max(edge_id, ::cuda::std::memory_order_relaxed);
#else
AtomicMax(edge_ids + output_offset + rnd, edge_id);
#endif // __CUDA_ARCH__
}
i += stride;
}
}
/**
* @brief Fills the random_arr with random numbers and the edge_ids array with
* original edge ids. When random_arr is sorted along with edge_ids, the first
* fanout elements of each row gives us the sampled edges.
*/
template <
typename float_t, typename indptr_t, typename indices_t, typename weights_t,
typename edge_id_t>
__global__ void _ComputeRandoms(
const int64_t num_edges, const indptr_t* const sliced_indptr,
const indptr_t* const sub_indptr, const indices_t* const csr_rows,
const weights_t* const sliced_weights, const indices_t* const indices,
const continuous_seed random_seed, float_t* random_arr,
edge_id_t* edge_ids) {
int64_t i = blockIdx.x * blockDim.x + threadIdx.x;
const int stride = gridDim.x * blockDim.x;
const auto labor = indices != nullptr;
const float_t inf =
static_cast<float_t>(std::numeric_limits<float>::infinity());
while (i < num_edges) {
const auto row_position = csr_rows[i];
const auto row_offset = i - sub_indptr[row_position];
const auto in_idx = sliced_indptr[row_position] + row_offset;
const auto rnd = random_seed.uniform(labor ? indices[in_idx] : i);
const auto prob =
sliced_weights ? sliced_weights[i] : static_cast<weights_t>(1);
const auto exp_rnd = -__logf(rnd);
const float_t adjusted_rnd =
prob > 0 ? static_cast<float_t>(exp_rnd / prob) : inf;
random_arr[i] = adjusted_rnd;
edge_ids[i] = row_offset;
i += stride;
}
}
struct IsPositive {
template <typename probs_t>
__host__ __device__ auto operator()(probs_t x) {
return x > 0;
}
};
template <typename indptr_t>
struct MinInDegreeFanout {
const indptr_t* in_degree;
const int64_t* fanouts;
size_t num_fanouts;
__host__ __device__ auto operator()(int64_t i) {
return static_cast<indptr_t>(
min(static_cast<int64_t>(in_degree[i]), fanouts[i % num_fanouts]));
}
};
template <typename indptr_t, typename indices_t>
struct IteratorFunc {
indptr_t* indptr;
indices_t* indices;
__host__ __device__ auto operator()(int64_t i) { return indices + indptr[i]; }
};
template <typename indptr_t>
struct AddOffset {
indptr_t offset;
template <typename edge_id_t>
__host__ __device__ indptr_t operator()(edge_id_t x) {
return x + offset;
}
};
template <typename indptr_t, typename indices_t>
struct IteratorFuncAddOffset {
indptr_t* indptr;
indptr_t* sliced_indptr;
indices_t* indices;
__host__ __device__ auto operator()(int64_t i) {
return thrust::transform_output_iterator{
indices + indptr[i], AddOffset<indptr_t>{sliced_indptr[i]}};
}
};
template <typename indptr_t, typename in_degree_iterator_t>
struct SegmentEndFunc {
indptr_t* indptr;
in_degree_iterator_t in_degree;
__host__ __device__ auto operator()(int64_t i) {
return indptr[i] + in_degree[i];
}
};
c10::intrusive_ptr<sampling::FusedSampledSubgraph> SampleNeighbors(
torch::Tensor indptr, torch::Tensor indices,
torch::optional<torch::Tensor> seeds,
torch::optional<std::vector<int64_t>> seed_offsets,
const std::vector<int64_t>& fanouts, bool replace, bool layer,
bool returning_indices_is_optional,
torch::optional<torch::Tensor> type_per_edge,
torch::optional<torch::Tensor> probs_or_mask,
torch::optional<torch::Tensor> node_type_offset,
torch::optional<torch::Dict<std::string, int64_t>> node_type_to_id,
torch::optional<torch::Dict<std::string, int64_t>> edge_type_to_id,
torch::optional<torch::Tensor> random_seed_tensor, float seed2_contribution,
// Optional temporal sampling arguments begin.
torch::optional<torch::Tensor> seeds_timestamp,
torch::optional<torch::Tensor> seeds_pre_time_window,
torch::optional<torch::Tensor> node_timestamp,
torch::optional<torch::Tensor> edge_timestamp
// Optional temporal sampling arguments end.
) {
// When seed_offsets.has_value() in the hetero case, we compute the output of
// sample_neighbors _convert_to_sampled_subgraph in a fused manner so that
// _convert_to_sampled_subgraph only has to perform slices over the returned
// indptr and indices tensors to form CSC outputs for each edge type.
TORCH_CHECK(!replace, "Sampling with replacement is not supported yet!");
// Assume that indptr, indices, seeds, type_per_edge and probs_or_mask
// are all resident on the GPU. If not, it is better to first extract them
// before calling this function.
auto allocator = cuda::GetAllocator();
auto num_rows =
seeds.has_value() ? seeds.value().size(0) : indptr.size(0) - 1;
auto fanouts_pinned = torch::empty(
fanouts.size(),
c10::TensorOptions().dtype(torch::kLong).pinned_memory(true));
auto fanouts_pinned_ptr = fanouts_pinned.data_ptr<int64_t>();
for (size_t i = 0; i < fanouts.size(); i++) {
fanouts_pinned_ptr[i] =
fanouts[i] >= 0 ? fanouts[i] : std::numeric_limits<int64_t>::max();
}
// Finally, copy the adjusted fanout values to the device memory.
auto fanouts_device = allocator.AllocateStorage<int64_t>(fanouts.size());
CUDA_CALL(cudaMemcpyAsync(
fanouts_device.get(), fanouts_pinned_ptr,
sizeof(int64_t) * fanouts.size(), cudaMemcpyHostToDevice,
cuda::GetCurrentStream()));
auto in_degree_and_sliced_indptr = SliceCSCIndptr(indptr, seeds);
auto in_degree = std::get<0>(in_degree_and_sliced_indptr);
auto sliced_indptr = std::get<1>(in_degree_and_sliced_indptr);
const auto homo_in_degree = in_degree;
const auto homo_sliced_indptr = sliced_indptr;
auto max_in_degree = torch::empty(
1,
c10::TensorOptions().dtype(in_degree.scalar_type()).pinned_memory(true));
AT_DISPATCH_INDEX_TYPES(
indptr.scalar_type(), "SampleNeighborsMaxInDegree", ([&] {
CUB_CALL(
DeviceReduce::Max, in_degree.data_ptr<index_t>(),
max_in_degree.data_ptr<index_t>(), num_rows);
}));
// Protect access to max_in_degree with a CUDAEvent
at::cuda::CUDAEvent max_in_degree_event;
max_in_degree_event.record();
torch::optional<int64_t> num_edges;
torch::Tensor sub_indptr;
if (!seeds.has_value()) {
num_edges = indices.size(0);
sub_indptr = indptr;
}
torch::optional<torch::Tensor> sliced_probs_or_mask;
if (probs_or_mask.has_value()) {
if (seeds.has_value()) {
torch::Tensor sliced_probs_or_mask_tensor;
std::tie(sub_indptr, sliced_probs_or_mask_tensor) = IndexSelectCSCImpl(
in_degree, sliced_indptr, probs_or_mask.value(), seeds.value(),
indptr.size(0) - 2, num_edges);
sliced_probs_or_mask = sliced_probs_or_mask_tensor;
num_edges = sliced_probs_or_mask_tensor.size(0);
} else {
sliced_probs_or_mask = probs_or_mask;
}
}
if (fanouts.size() > 1) {
torch::Tensor sliced_type_per_edge;
if (seeds.has_value()) {
std::tie(sub_indptr, sliced_type_per_edge) = IndexSelectCSCImpl(
in_degree, sliced_indptr, type_per_edge.value(), seeds.value(),
indptr.size(0) - 2, num_edges);
} else {
sliced_type_per_edge = type_per_edge.value();
}
std::tie(sub_indptr, in_degree, sliced_indptr) = SliceCSCIndptrHetero(
sub_indptr, sliced_type_per_edge, sliced_indptr, fanouts.size());
num_rows = sliced_indptr.size(0);
num_edges = sliced_type_per_edge.size(0);
}
// If sub_indptr was not computed in the two code blocks above:
if (seeds.has_value() && !probs_or_mask.has_value() && fanouts.size() <= 1) {
sub_indptr = ExclusiveCumSum(in_degree);
}
torch::optional<torch::Tensor> homo_coo_rows;
if (seeds_timestamp.has_value()) {
// Temporal sampling is enabled.
const auto homo_sub_indptr =
fanouts.size() > 1 ? ExclusiveCumSum(homo_in_degree) : sub_indptr;
homo_coo_rows = ExpandIndptrImpl(
homo_sub_indptr, indices.scalar_type(), torch::nullopt, num_edges);
num_edges = homo_coo_rows->size(0);
const auto is_probs_initialized = sliced_probs_or_mask.has_value();
if (!is_probs_initialized) {
sliced_probs_or_mask =
torch::empty(*num_edges, sub_indptr.options().dtype(torch::kBool));
}
GRAPHBOLT_DISPATCH_ALL_TYPES(
sliced_probs_or_mask->scalar_type(),
"SampleNeighborsTemporalProbsOrMask", ([&] {
const scalar_t* input_probs_ptr =
is_probs_initialized ? sliced_probs_or_mask->data_ptr<scalar_t>()
: nullptr;
auto output_probs_ptr = sliced_probs_or_mask->data_ptr<scalar_t>();
using timestamp_t = int64_t;
const auto seeds_timestamp_ptr =
seeds_timestamp->data_ptr<timestamp_t>();
const timestamp_t* seeds_pre_time_window_ptr =
seeds_pre_time_window.has_value()
? seeds_pre_time_window->data_ptr<timestamp_t>()
: nullptr;
const timestamp_t* node_timestamp_ptr =
node_timestamp.has_value()
? node_timestamp->data_ptr<timestamp_t>()
: nullptr;
const timestamp_t* edge_timestamp_ptr =
edge_timestamp.has_value()
? edge_timestamp->data_ptr<timestamp_t>()
: nullptr;
AT_DISPATCH_INDEX_TYPES(
homo_coo_rows->scalar_type(),
"SampleNeighborsTemporalMaskIndices", ([&] {
const auto coo_rows_ptr = homo_coo_rows->data_ptr<index_t>();
const auto indices_ptr = indices.data_ptr<index_t>();
AT_DISPATCH_INDEX_TYPES(
homo_sliced_indptr.scalar_type(),
"SampleNeighborsTemporalMaskIndptr", ([&] {
const auto sliced_indptr_data =
homo_sliced_indptr.data_ptr<index_t>();
const auto sub_indptr_data =
homo_sub_indptr.data_ptr<index_t>();
CUB_CALL(
DeviceFor::Bulk, *num_edges,
[=] __device__(int64_t i) {
const auto row = coo_rows_ptr[i];
const auto seed_timestamp =
seeds_timestamp_ptr[row];
const auto row_offset = i - sub_indptr_data[row];
const auto in_idx =
sliced_indptr_data[row] + row_offset;
bool mask = true;
if (node_timestamp_ptr) {
const auto index = indices_ptr[in_idx];
const auto neighbor_timestamp =
node_timestamp_ptr[index];
mask &= neighbor_timestamp < seed_timestamp;
if (seeds_pre_time_window_ptr) {
mask &= neighbor_timestamp >
seed_timestamp -
seeds_pre_time_window_ptr[row];
}
}
if (edge_timestamp_ptr) {
const auto edge_timestamp =
edge_timestamp_ptr[in_idx];
mask &= edge_timestamp < seed_timestamp;
if (seeds_pre_time_window_ptr) {
mask &= edge_timestamp >
seed_timestamp -
seeds_pre_time_window_ptr[row];
}
}
const scalar_t prob = input_probs_ptr
? input_probs_ptr[i]
: scalar_t{1};
output_probs_ptr[i] =
prob * static_cast<scalar_t>(mask);
});
}));
}));
}));
}
const continuous_seed random_seed = [&] {
if (random_seed_tensor.has_value()) {
return continuous_seed(random_seed_tensor.value(), seed2_contribution);
} else {
return continuous_seed{RandomEngine::ThreadLocal()->RandInt(
static_cast<int64_t>(0), std::numeric_limits<int64_t>::max())};
}
}();
auto output_indptr = torch::empty_like(sub_indptr);
torch::Tensor picked_eids;
torch::optional<torch::Tensor> output_indices;
AT_DISPATCH_INDEX_TYPES(
indptr.scalar_type(), "SampleNeighborsIndptr", ([&] {
using indptr_t = index_t;
if (sliced_probs_or_mask.has_value()) {
// Count nonzero probs into in_degree.
GRAPHBOLT_DISPATCH_ALL_TYPES(
sliced_probs_or_mask->scalar_type(),
"SampleNeighborsPositiveProbs", ([&] {
using probs_t = scalar_t;
auto is_nonzero = thrust::make_transform_iterator(
sliced_probs_or_mask->data_ptr<probs_t>(), IsPositive{});
CUB_CALL(
DeviceSegmentedReduce::Sum, is_nonzero,
in_degree.data_ptr<indptr_t>(), num_rows,
sub_indptr.data_ptr<indptr_t>(),
sub_indptr.data_ptr<indptr_t>() + 1);
}));
}
thrust::counting_iterator<int64_t> iota(0);
auto sampled_degree = thrust::make_transform_iterator(
iota, MinInDegreeFanout<indptr_t>{
in_degree.data_ptr<indptr_t>(), fanouts_device.get(),
fanouts.size()});
// Compute output_indptr.
CUB_CALL(
DeviceScan::ExclusiveSum, sampled_degree,
output_indptr.data_ptr<indptr_t>(), num_rows + 1);
auto num_sampled_edges =
cuda::CopyScalar{output_indptr.data_ptr<indptr_t>() + num_rows};
// This operation is placed after num_sampled_edges copy is started to
// hide the latency of copy synchronization later.
torch::Tensor coo_rows;
if (!homo_coo_rows.has_value() || fanouts.size() > 1) {
coo_rows = ExpandIndptrImpl(
sub_indptr, indices.scalar_type(), torch::nullopt, num_edges);
num_edges = coo_rows.size(0);
} else {
coo_rows = *homo_coo_rows;
}
// Find the smallest integer type to store the edge id offsets. We synch
// the CUDAEvent so that the access is safe.
auto compute_num_bits = [&] {
max_in_degree_event.synchronize();
return cuda::NumberOfBits(max_in_degree.data_ptr<indptr_t>()[0]);
};
if (layer || sliced_probs_or_mask.has_value()) {
const int num_bits = compute_num_bits();
std::array<int, 4> type_bits = {8, 16, 32, 64};
const auto type_index =
std::lower_bound(type_bits.begin(), type_bits.end(), num_bits) -
type_bits.begin();
std::array<torch::ScalarType, 5> types = {
torch::kByte, torch::kInt16, torch::kInt32, torch::kLong,
torch::kLong};
auto edge_id_dtype = types[type_index];
AT_DISPATCH_INTEGRAL_TYPES(
edge_id_dtype, "SampleNeighborsEdgeIDs", ([&] {
using edge_id_t = std::make_unsigned_t<scalar_t>;
TORCH_CHECK(
num_bits <= sizeof(edge_id_t) * 8,
"Selected edge_id_t must be capable of storing edge_ids.");
// Using bfloat16 for random numbers works just as reliably as
// float32 and provides around 30% speedup.
using rnd_t = nv_bfloat16;
auto randoms =
allocator.AllocateStorage<rnd_t>(num_edges.value());
auto randoms_sorted =
allocator.AllocateStorage<rnd_t>(num_edges.value());
auto edge_id_segments =
allocator.AllocateStorage<edge_id_t>(num_edges.value());
auto sorted_edge_id_segments =
allocator.AllocateStorage<edge_id_t>(num_edges.value());
AT_DISPATCH_INDEX_TYPES(
indices.scalar_type(), "SampleNeighborsIndices", ([&] {
using indices_t = index_t;
auto probs_or_mask_scalar_type = torch::kFloat32;
if (sliced_probs_or_mask.has_value()) {
probs_or_mask_scalar_type =
sliced_probs_or_mask->scalar_type();
}
GRAPHBOLT_DISPATCH_ALL_TYPES(
probs_or_mask_scalar_type, "SampleNeighborsProbs",
([&] {
using probs_t = scalar_t;
probs_t* sliced_probs_ptr = nullptr;
if (sliced_probs_or_mask.has_value()) {
sliced_probs_ptr =
sliced_probs_or_mask->data_ptr<probs_t>();
}
const indices_t* indices_ptr =
layer ? indices.data_ptr<indices_t>() : nullptr;
const dim3 block(BLOCK_SIZE);
const dim3 grid(
(num_edges.value() + BLOCK_SIZE - 1) /
BLOCK_SIZE);
// Compute row and random number pairs.
CUDA_KERNEL_CALL(
_ComputeRandoms, grid, block, 0,
num_edges.value(),
sliced_indptr.data_ptr<indptr_t>(),
sub_indptr.data_ptr<indptr_t>(),
coo_rows.data_ptr<indices_t>(),
sliced_probs_ptr, indices_ptr, random_seed,
randoms.get(), edge_id_segments.get());
}));
}));
// Sort the random numbers along with edge ids, after
// sorting the first fanout elements of each row will
// give us the sampled edges.
CUB_CALL(
DeviceSegmentedSort::SortPairs, randoms.get(),
randoms_sorted.get(), edge_id_segments.get(),
sorted_edge_id_segments.get(), num_edges.value(), num_rows,
sub_indptr.data_ptr<indptr_t>(),
sub_indptr.data_ptr<indptr_t>() + 1);
picked_eids = torch::empty(
static_cast<indptr_t>(num_sampled_edges),
sub_indptr.options());
// Need to sort the sampled edges only when fanouts.size() == 1
// since multiple fanout sampling case is automatically going to
// be sorted.
if (type_per_edge && fanouts.size() == 1) {
// Ensuring sort result still ends up in
// sorted_edge_id_segments
std::swap(edge_id_segments, sorted_edge_id_segments);
auto sampled_segment_end_it = thrust::make_transform_iterator(
iota,
SegmentEndFunc<indptr_t, decltype(sampled_degree)>{
sub_indptr.data_ptr<indptr_t>(), sampled_degree});
CUB_CALL(
DeviceSegmentedSort::SortKeys, edge_id_segments.get(),
sorted_edge_id_segments.get(), picked_eids.size(0),
num_rows, sub_indptr.data_ptr<indptr_t>(),
sampled_segment_end_it);
}
auto input_buffer_it = thrust::make_transform_iterator(
iota, IteratorFunc<indptr_t, edge_id_t>{
sub_indptr.data_ptr<indptr_t>(),
sorted_edge_id_segments.get()});
auto output_buffer_it = thrust::make_transform_iterator(
iota, IteratorFuncAddOffset<indptr_t, indptr_t>{
output_indptr.data_ptr<indptr_t>(),
sliced_indptr.data_ptr<indptr_t>(),
picked_eids.data_ptr<indptr_t>()});
constexpr int64_t max_copy_at_once =
std::numeric_limits<int32_t>::max();
// Copy the sampled edge ids into picked_eids tensor.
for (int64_t i = 0; i < num_rows; i += max_copy_at_once) {
CUB_CALL(
DeviceCopy::Batched, input_buffer_it + i,
output_buffer_it + i, sampled_degree + i,
std::min(num_rows - i, max_copy_at_once));
}
}));
} else { // Non-weighted neighbor sampling.
picked_eids = torch::zeros(num_edges.value(), sub_indptr.options());
const auto sort_needed = type_per_edge && fanouts.size() == 1;
const auto sliced_indptr_ptr =
sort_needed ? nullptr : sliced_indptr.data_ptr<indptr_t>();
const dim3 block(BLOCK_SIZE);
const dim3 grid(
(std::min(num_edges.value(), static_cast<int64_t>(1 << 20)) +
BLOCK_SIZE - 1) /
BLOCK_SIZE);
AT_DISPATCH_INDEX_TYPES(
indices.scalar_type(), "SampleNeighborsIndices", ([&] {
using indices_t = index_t;
// Compute row and random number pairs.
CUDA_KERNEL_CALL(
_ComputeRandomsNS, grid, block, 0, num_edges.value(),
sliced_indptr_ptr, sub_indptr.data_ptr<indptr_t>(),
output_indptr.data_ptr<indptr_t>(),
coo_rows.data_ptr<indices_t>(), random_seed.get_seed(0),
picked_eids.data_ptr<indptr_t>());
}));
picked_eids =
picked_eids.slice(0, 0, static_cast<indptr_t>(num_sampled_edges));
// Need to sort the sampled edges only when fanouts.size() == 1
// since multiple fanout sampling case is automatically going to
// be sorted.
if (sort_needed) {
const int num_bits = compute_num_bits();
std::array<int, 4> type_bits = {8, 15, 31, 63};
const auto type_index =
std::lower_bound(type_bits.begin(), type_bits.end(), num_bits) -
type_bits.begin();
std::array<torch::ScalarType, 5> types = {
torch::kByte, torch::kInt16, torch::kInt32, torch::kLong,
torch::kLong};
auto edge_id_dtype = types[type_index];
AT_DISPATCH_INTEGRAL_TYPES(
edge_id_dtype, "SampleNeighborsEdgeIDs", ([&] {
using edge_id_t = scalar_t;
TORCH_CHECK(
num_bits <= sizeof(edge_id_t) * 8,
"Selected edge_id_t must be capable of storing "
"edge_ids.");
auto picked_offsets = picked_eids.to(edge_id_dtype);
auto sorted_offsets = torch::empty_like(picked_offsets);
CUB_CALL(
DeviceSegmentedSort::SortKeys,
picked_offsets.data_ptr<edge_id_t>(),
sorted_offsets.data_ptr<edge_id_t>(), picked_eids.size(0),
num_rows, output_indptr.data_ptr<indptr_t>(),
output_indptr.data_ptr<indptr_t>() + 1);
auto edge_id_offsets = ExpandIndptrImpl(
output_indptr, picked_eids.scalar_type(), sliced_indptr,
picked_eids.size(0));
picked_eids = sorted_offsets.to(picked_eids.scalar_type()) +
edge_id_offsets;
}));
}
}
if (!returning_indices_is_optional || utils::is_on_gpu(indices)) {
output_indices = Gather(indices, picked_eids);
}
}));
torch::optional<torch::Tensor> output_type_per_edge;
torch::optional<torch::Tensor> edge_offsets;
if (type_per_edge && seed_offsets) {
const int64_t num_etypes =
edge_type_to_id.has_value() ? edge_type_to_id->size() : 1;
// If we performed homogenous sampling on hetero graph, we have to look at
// type_per_edge of sampled edges and determine the offsets of different
// sampled etypes and convert to fused hetero indptr representation.
if (fanouts.size() == 1) {
output_type_per_edge = Gather(*type_per_edge, picked_eids);
torch::Tensor output_in_degree, sliced_output_indptr;
sliced_output_indptr =
output_indptr.slice(0, 0, output_indptr.size(0) - 1);
std::tie(output_indptr, output_in_degree, sliced_output_indptr) =
SliceCSCIndptrHetero(
output_indptr, output_type_per_edge.value(), sliced_output_indptr,
num_etypes);
// We use num_rows to hold num_seeds * num_etypes. So, it needs to be
// updated when sampling with a single fanout value when the graph is
// heterogenous.
num_rows = sliced_output_indptr.size(0);
}
// Here, we check what are the dst node types for the given seeds so that
// we can compute the output indptr space later.
std::vector<int64_t> etype_id_to_dst_ntype_id(num_etypes);
// Here, we check what are the src node types for the given seeds so that
// we can subtract source node offset from indices later.
auto etype_id_to_src_ntype_id = torch::empty(
2 * num_etypes,
c10::TensorOptions().dtype(torch::kLong).pinned_memory(true));
auto etype_id_to_src_ntype_id_ptr =
etype_id_to_src_ntype_id.data_ptr<int64_t>();
for (auto& etype_and_id : edge_type_to_id.value()) {
auto etype = etype_and_id.key();
auto id = etype_and_id.value();
auto [src_type, dst_type] = utils::parse_src_dst_ntype_from_etype(etype);
etype_id_to_dst_ntype_id[id] = node_type_to_id->at(dst_type);
etype_id_to_src_ntype_id_ptr[2 * id] =
etype_id_to_src_ntype_id_ptr[2 * id + 1] =
node_type_to_id->at(src_type);
}
auto indices_offsets_device = torch::empty(
etype_id_to_src_ntype_id.size(0),
picked_eids.options().dtype(torch::kLong));
AT_DISPATCH_INDEX_TYPES(
node_type_offset->scalar_type(), "SampleNeighborsNodeTypeOffset", ([&] {
THRUST_CALL(
gather, etype_id_to_src_ntype_id_ptr,
etype_id_to_src_ntype_id_ptr + etype_id_to_src_ntype_id.size(0),
node_type_offset->data_ptr<index_t>(),
indices_offsets_device.data_ptr<int64_t>());
}));
// For each edge type, we compute the start and end offsets to index into
// indptr to form the final output_indptr.
auto indptr_offsets = torch::empty(
num_etypes * 2,
c10::TensorOptions().dtype(torch::kLong).pinned_memory(true));
auto indptr_offsets_ptr = indptr_offsets.data_ptr<int64_t>();
// We compute the indptr offsets here, right now, output_indptr is of size
// # seeds * num_etypes + 1. We can simply take slices to get correct output
// indptr. The final output_indptr is same as current indptr except that
// some intermediate values are removed to change the node ids space from
// all of the seed vertices to the node id space of the dst node type of
// each edge type.
for (int i = 0; i < num_etypes; i++) {
indptr_offsets_ptr[2 * i] = num_rows / num_etypes * i +
seed_offsets->at(etype_id_to_dst_ntype_id[i]);
indptr_offsets_ptr[2 * i + 1] =
num_rows / num_etypes * i +
seed_offsets->at(etype_id_to_dst_ntype_id[i] + 1);
}
auto permutation = torch::arange(
0, num_rows * num_etypes, num_etypes, output_indptr.options());
permutation =
permutation.remainder(num_rows) + permutation.div(num_rows, "floor");
// This permutation, when applied sorts the sampled edges with respect to
// edge types.
auto [output_in_degree, sliced_output_indptr] =
SliceCSCIndptr(output_indptr, permutation);
std::tie(output_indptr, picked_eids) = IndexSelectCSCImpl(
output_in_degree, sliced_output_indptr, picked_eids, permutation,
num_rows - 1, picked_eids.size(0));
edge_offsets = torch::empty(
num_etypes * 2, c10::TensorOptions()
.dtype(output_indptr.scalar_type())
.pinned_memory(true));
auto edge_offsets_device =
torch::empty(num_etypes * 2, output_indptr.options());
at::cuda::CUDAEvent edge_offsets_event;
AT_DISPATCH_INDEX_TYPES(
indptr.scalar_type(), "SampleNeighborsEdgeOffsets", ([&] {
auto edge_offsets_pinned_device_pair =
thrust::make_transform_output_iterator(
thrust::make_zip_iterator(
edge_offsets->data_ptr<index_t>(),
edge_offsets_device.data_ptr<index_t>()),
[=] __device__(index_t x) {
return thrust::make_tuple(x, x);
});
THRUST_CALL(
gather, indptr_offsets_ptr,
indptr_offsets_ptr + indptr_offsets.size(0),
output_indptr.data_ptr<index_t>(),
edge_offsets_pinned_device_pair);
}));
edge_offsets_event.record();
if (output_indices.has_value()) {
auto indices_offset_subtract = ExpandIndptrImpl(
edge_offsets_device, indices.scalar_type(), indices_offsets_device,
output_indices->size(0));
// The output_indices is permuted here.
std::tie(output_indptr, output_indices) = IndexSelectCSCImpl(
output_in_degree, sliced_output_indptr, *output_indices, permutation,
num_rows - 1, output_indices->size(0));
*output_indices -= indices_offset_subtract;
}
auto output_indptr_offsets = torch::empty(
num_etypes * 2,
c10::TensorOptions().dtype(torch::kLong).pinned_memory(true));
auto output_indptr_offsets_ptr = output_indptr_offsets.data_ptr<int64_t>();
std::vector<torch::Tensor> indptr_list;
for (int i = 0; i < num_etypes; i++) {
indptr_list.push_back(output_indptr.slice(
0, indptr_offsets_ptr[2 * i], indptr_offsets_ptr[2 * i + 1] + 1));
output_indptr_offsets_ptr[2 * i] =
i == 0 ? 0 : output_indptr_offsets_ptr[2 * i - 1];
output_indptr_offsets_ptr[2 * i + 1] =
output_indptr_offsets_ptr[2 * i] + indptr_list.back().size(0);
}
auto output_indptr_offsets_device = torch::empty(
output_indptr_offsets.size(0),
output_indptr.options().dtype(torch::kLong));
THRUST_CALL(
copy_n, output_indptr_offsets_ptr, output_indptr_offsets.size(0),
output_indptr_offsets_device.data_ptr<int64_t>());
// We form the final output indptr by concatenating pieces for different
// edge types.
output_indptr = torch::cat(indptr_list);
auto indptr_offset_subtract = ExpandIndptrImpl(
output_indptr_offsets_device, indptr.scalar_type(), edge_offsets_device,
output_indptr.size(0));
output_indptr -= indptr_offset_subtract;
edge_offsets_event.synchronize();
// We read the edge_offsets here, they are in pairs but we don't need it to
// be in pairs. So we remove the duplicate information from it and turn it
// into a real offsets array.
AT_DISPATCH_INDEX_TYPES(
indptr.scalar_type(), "SampleNeighborsEdgeOffsetsCheck", ([&] {
auto edge_offsets_ptr = edge_offsets->data_ptr<index_t>();
TORCH_CHECK(edge_offsets_ptr[0] == 0, "edge_offsets is incorrect.");
for (int i = 1; i < num_etypes; i++) {
TORCH_CHECK(
edge_offsets_ptr[2 * i - 1] == edge_offsets_ptr[2 * i],
"edge_offsets is incorrect.");
}
TORCH_CHECK(
edge_offsets_ptr[2 * num_etypes - 1] == picked_eids.size(0),
"edge_offsets is incorrect.");
for (int i = 0; i < num_etypes; i++) {
edge_offsets_ptr[i + 1] = edge_offsets_ptr[2 * i + 1];
}
}));
edge_offsets = edge_offsets->slice(0, 0, num_etypes + 1);
} else {
// Convert output_indptr back to homo by discarding intermediate offsets.
output_indptr =
output_indptr.slice(0, 0, output_indptr.size(0), fanouts.size());
if (type_per_edge)
output_type_per_edge = Gather(*type_per_edge, picked_eids);
}
return c10::make_intrusive<sampling::FusedSampledSubgraph>(
output_indptr, output_indices, picked_eids, seeds, torch::nullopt,
output_type_per_edge, edge_offsets);
}
} // namespace ops
} // namespace graphbolt
+153
View File
@@ -0,0 +1,153 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/sampling_utils.cu
* @brief Sampling utility function implementations on CUDA.
*/
#include <thrust/for_each.h>
#include <thrust/iterator/counting_iterator.h>
#include <cub/cub.cuh>
#include "./common.h"
#include "./utils.h"
namespace graphbolt {
namespace ops {
// Given rows and indptr, computes:
// inrow_indptr[i] = indptr[rows[i]];
// in_degree[i] = indptr[rows[i] + 1] - indptr[rows[i]];
template <typename indptr_t, typename nodes_t>
struct SliceFunc {
const nodes_t* rows;
const indptr_t* indptr;
indptr_t* in_degree;
indptr_t* inrow_indptr;
__host__ __device__ auto operator()(int64_t tIdx) {
const auto out_row = rows[tIdx];
const auto indptr_val = indptr[out_row];
const auto degree = indptr[out_row + 1] - indptr_val;
in_degree[tIdx] = degree;
inrow_indptr[tIdx] = indptr_val;
}
};
// Returns (indptr[nodes + 1] - indptr[nodes], indptr[nodes])
std::tuple<torch::Tensor, torch::Tensor> SliceCSCIndptr(
torch::Tensor indptr, torch::optional<torch::Tensor> nodes_optional) {
if (nodes_optional.has_value()) {
auto nodes = nodes_optional.value();
const int64_t num_nodes = nodes.size(0);
// Read indptr only once in case it is pinned and access is slow.
auto sliced_indptr =
torch::empty(num_nodes, nodes.options().dtype(indptr.scalar_type()));
// compute in-degrees
auto in_degree = torch::empty(
num_nodes + 1, nodes.options().dtype(indptr.scalar_type()));
thrust::counting_iterator<int64_t> iota(0);
AT_DISPATCH_INTEGRAL_TYPES(
indptr.scalar_type(), "IndexSelectCSCIndptr", ([&] {
using indptr_t = scalar_t;
AT_DISPATCH_INDEX_TYPES(
nodes.scalar_type(), "IndexSelectCSCNodes", ([&] {
using nodes_t = index_t;
THRUST_CALL(
for_each, iota, iota + num_nodes,
SliceFunc<indptr_t, nodes_t>{
nodes.data_ptr<nodes_t>(), indptr.data_ptr<indptr_t>(),
in_degree.data_ptr<indptr_t>(),
sliced_indptr.data_ptr<indptr_t>()});
}));
}));
return {in_degree, sliced_indptr};
} else {
const int64_t num_nodes = indptr.size(0) - 1;
auto sliced_indptr = indptr.slice(0, 0, num_nodes);
auto in_degree = torch::empty(
num_nodes + 2, indptr.options().dtype(indptr.scalar_type()));
AT_DISPATCH_INTEGRAL_TYPES(
indptr.scalar_type(), "IndexSelectCSCIndptr", ([&] {
using indptr_t = scalar_t;
CUB_CALL(
DeviceAdjacentDifference::SubtractLeftCopy,
indptr.data_ptr<indptr_t>(), in_degree.data_ptr<indptr_t>(),
num_nodes + 1, cub::Difference{});
}));
in_degree = in_degree.slice(0, 1);
return {in_degree, sliced_indptr};
}
}
template <typename indptr_t, typename etype_t>
struct EdgeTypeSearch {
const indptr_t* sub_indptr;
const indptr_t* sliced_indptr;
const etype_t* etypes;
int64_t num_fanouts;
int64_t num_rows;
indptr_t* new_sub_indptr;
indptr_t* new_sliced_indptr;
__host__ __device__ auto operator()(int64_t i) {
const auto homo_i = i / num_fanouts;
const auto indptr_i = sub_indptr[homo_i];
const auto degree = sub_indptr[homo_i + 1] - indptr_i;
const etype_t etype = i % num_fanouts;
auto offset = cub::LowerBound(etypes + indptr_i, degree, etype);
new_sub_indptr[i] = indptr_i + offset;
new_sliced_indptr[i] = sliced_indptr[homo_i] + offset;
if (i == num_rows - 1) new_sub_indptr[num_rows] = indptr_i + degree;
}
};
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor> SliceCSCIndptrHetero(
torch::Tensor sub_indptr, torch::Tensor etypes, torch::Tensor sliced_indptr,
int64_t num_fanouts) {
auto num_rows = (sub_indptr.size(0) - 1) * num_fanouts;
auto new_sub_indptr = torch::empty(num_rows + 1, sub_indptr.options());
auto new_indegree = torch::empty(num_rows + 2, sub_indptr.options());
auto new_sliced_indptr = torch::empty(num_rows, sliced_indptr.options());
thrust::counting_iterator<int64_t> iota(0);
AT_DISPATCH_INTEGRAL_TYPES(
sub_indptr.scalar_type(), "SliceCSCIndptrHeteroIndptr", ([&] {
using indptr_t = scalar_t;
AT_DISPATCH_INTEGRAL_TYPES(
etypes.scalar_type(), "SliceCSCIndptrHeteroTypePerEdge", ([&] {
using etype_t = scalar_t;
THRUST_CALL(
for_each, iota, iota + num_rows,
EdgeTypeSearch<indptr_t, etype_t>{
sub_indptr.data_ptr<indptr_t>(),
sliced_indptr.data_ptr<indptr_t>(),
etypes.data_ptr<etype_t>(), num_fanouts, num_rows,
new_sub_indptr.data_ptr<indptr_t>(),
new_sliced_indptr.data_ptr<indptr_t>()});
}));
CUB_CALL(
DeviceAdjacentDifference::SubtractLeftCopy,
new_sub_indptr.data_ptr<indptr_t>(),
new_indegree.data_ptr<indptr_t>(), num_rows + 1, cub::Difference{});
}));
// Discard the first element of the SubtractLeftCopy result and ensure that
// new_indegree tensor has size num_rows + 1 so that its ExclusiveCumSum is
// directly equivalent to new_sub_indptr.
// Equivalent to new_indegree = new_indegree[1:] in Python.
new_indegree = new_indegree.slice(0, 1);
return {new_sub_indptr, new_indegree, new_sliced_indptr};
}
} // namespace ops
} // namespace graphbolt
+78
View File
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/sort_impl.cu
* @brief Sort implementation on CUDA.
*/
#include <c10/core/ScalarType.h>
#include <cub/cub.cuh>
#include "./common.h"
#include "./utils.h"
namespace graphbolt {
namespace ops {
template <bool return_original_positions, typename scalar_t>
std::conditional_t<
return_original_positions, std::pair<torch::Tensor, torch::Tensor>,
torch::Tensor>
Sort(const scalar_t* input_keys, int64_t num_items, int num_bits) {
const auto options = torch::TensorOptions().device(c10::DeviceType::CUDA);
constexpr c10::ScalarType dtype = c10::CppTypeToScalarType<scalar_t>::value;
auto sorted_array = torch::empty(num_items, options.dtype(dtype));
auto sorted_keys = sorted_array.data_ptr<scalar_t>();
if (num_bits == 0) {
num_bits = sizeof(scalar_t) * 8;
}
if constexpr (return_original_positions) {
// We utilize int64_t for the values array. (torch::kLong == int64_t)
auto original_idx = torch::arange(num_items, options.dtype(torch::kLong));
auto sorted_idx = torch::empty_like(original_idx);
const int64_t* input_values = original_idx.data_ptr<int64_t>();
int64_t* sorted_values = sorted_idx.data_ptr<int64_t>();
CUB_CALL(
DeviceRadixSort::SortPairs, input_keys, sorted_keys, input_values,
sorted_values, num_items, 0, num_bits);
return std::make_pair(sorted_array, sorted_idx);
} else {
CUB_CALL(
DeviceRadixSort::SortKeys, input_keys, sorted_keys, num_items, 0,
num_bits);
return sorted_array;
}
}
template <bool return_original_positions>
std::conditional_t<
return_original_positions, std::pair<torch::Tensor, torch::Tensor>,
torch::Tensor>
Sort(torch::Tensor input, int num_bits) {
return AT_DISPATCH_INTEGRAL_TYPES(input.scalar_type(), "SortImpl", ([&] {
return Sort<return_original_positions>(
input.data_ptr<scalar_t>(),
input.size(0), num_bits);
}));
}
template torch::Tensor Sort<false>(torch::Tensor input, int num_bits);
template std::pair<torch::Tensor, torch::Tensor> Sort<true>(
torch::Tensor input, int num_bits);
} // namespace ops
} // namespace graphbolt
@@ -0,0 +1,323 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file cuda/unique_and_compact_impl.cu
* @brief Unique and compact operator implementation on CUDA.
*/
#include <graphbolt/cuda_ops.h>
#include <thrust/binary_search.h>
#include <thrust/functional.h>
#include <thrust/gather.h>
#include <thrust/logical.h>
#include <cub/cub.cuh>
#include <mutex>
#include <type_traits>
#include <unordered_map>
#include "./common.h"
#include "./extension/unique_and_compact.h"
#include "./utils.h"
namespace graphbolt {
namespace ops {
template <typename scalar_t>
struct EqualityFunc {
const scalar_t* sorted_order;
const scalar_t* found_locations;
const scalar_t* searched_items;
__host__ __device__ auto operator()(int64_t i) {
return sorted_order[found_locations[i]] == searched_items[i];
}
};
#define DefineCubReductionFunction(cub_reduce_fn, name) \
template <typename scalar_iterator_t> \
auto name(const scalar_iterator_t input, int64_t size) { \
using scalar_t = std::remove_reference_t<decltype(input[0])>; \
cuda::CopyScalar<scalar_t> result; \
CUB_CALL(cub_reduce_fn, input, result.get(), size); \
return result; \
}
DefineCubReductionFunction(DeviceReduce::Max, Max);
DefineCubReductionFunction(DeviceReduce::Min, Min);
std::vector<std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>>
UniqueAndCompactBatchedSortBased(
const std::vector<torch::Tensor>& src_ids,
const std::vector<torch::Tensor>& dst_ids,
const std::vector<torch::Tensor>& unique_dst_ids, int num_bits = 0) {
auto allocator = cuda::GetAllocator();
auto stream = cuda::GetCurrentStream();
auto scalar_type = src_ids.at(0).scalar_type();
return AT_DISPATCH_INDEX_TYPES(
scalar_type, "unique_and_compact", ([&] {
std::vector<index_t*> src_ids_ptr, dst_ids_ptr, unique_dst_ids_ptr;
for (std::size_t i = 0; i < src_ids.size(); i++) {
src_ids_ptr.emplace_back(src_ids[i].data_ptr<index_t>());
dst_ids_ptr.emplace_back(dst_ids[i].data_ptr<index_t>());
unique_dst_ids_ptr.emplace_back(
unique_dst_ids[i].data_ptr<index_t>());
}
// If num_bits is not given, compute maximum vertex ids to compute
// num_bits later to speedup the expensive sort operations.
std::vector<cuda::CopyScalar<index_t>> max_id_src;
std::vector<cuda::CopyScalar<index_t>> max_id_dst;
for (std::size_t i = 0; num_bits == 0 && i < src_ids.size(); i++) {
max_id_src.emplace_back(Max(src_ids_ptr[i], src_ids[i].size(0)));
max_id_dst.emplace_back(
Max(unique_dst_ids_ptr[i], unique_dst_ids[i].size(0)));
}
// Sort the unique_dst_ids tensor.
std::vector<torch::Tensor> sorted_unique_dst_ids;
std::vector<index_t*> sorted_unique_dst_ids_ptr;
for (std::size_t i = 0; i < unique_dst_ids.size(); i++) {
sorted_unique_dst_ids.emplace_back(Sort<false>(
unique_dst_ids_ptr[i], unique_dst_ids[i].size(0), num_bits));
sorted_unique_dst_ids_ptr.emplace_back(
sorted_unique_dst_ids[i].data_ptr<index_t>());
}
// Mark dst nodes in the src_ids tensor.
std::vector<decltype(allocator.AllocateStorage<bool>(0))> is_dst;
for (std::size_t i = 0; i < src_ids.size(); i++) {
is_dst.emplace_back(
allocator.AllocateStorage<bool>(src_ids[i].size(0)));
THRUST_CALL(
binary_search, sorted_unique_dst_ids_ptr[i],
sorted_unique_dst_ids_ptr[i] + unique_dst_ids[i].size(0),
src_ids_ptr[i], src_ids_ptr[i] + src_ids[i].size(0),
is_dst[i].get());
}
// Filter the non-dst nodes in the src_ids tensor, hence only_src.
std::vector<torch::Tensor> only_src;
{
std::vector<cuda::CopyScalar<int64_t>> only_src_size;
for (std::size_t i = 0; i < src_ids.size(); i++) {
only_src.emplace_back(torch::empty(
src_ids[i].size(0), sorted_unique_dst_ids[i].options()));
auto is_src = thrust::make_transform_iterator(
is_dst[i].get(), thrust::logical_not<bool>{});
only_src_size.emplace_back(cuda::CopyScalar<int64_t>{});
CUB_CALL(
DeviceSelect::Flagged, src_ids_ptr[i], is_src,
only_src[i].data_ptr<index_t>(), only_src_size[i].get(),
src_ids[i].size(0));
}
stream.synchronize();
for (std::size_t i = 0; i < only_src.size(); i++) {
only_src[i] =
only_src[i].slice(0, 0, static_cast<int64_t>(only_src_size[i]));
}
}
// The code block above synchronizes, ensuring safe access to
// max_id_src and max_id_dst.
if (num_bits == 0) {
index_t max_id = 0;
for (std::size_t i = 0; i < max_id_src.size(); i++) {
max_id = std::max(max_id, static_cast<index_t>(max_id_src[i]));
max_id = std::max(max_id, static_cast<index_t>(max_id_dst[i]));
}
num_bits = cuda::NumberOfBits(1ll + max_id);
}
// Sort the only_src tensor so that we can unique it later.
std::vector<torch::Tensor> sorted_only_src;
for (auto& only_src_i : only_src) {
sorted_only_src.emplace_back(Sort<false>(
only_src_i.data_ptr<index_t>(), only_src_i.size(0), num_bits));
}
std::vector<torch::Tensor> unique_only_src;
std::vector<index_t*> unique_only_src_ptr;
std::vector<cuda::CopyScalar<int64_t>> unique_only_src_size;
for (std::size_t i = 0; i < src_ids.size(); i++) {
// Compute the unique operation on the only_src tensor.
unique_only_src.emplace_back(
torch::empty(only_src[i].size(0), src_ids[i].options()));
unique_only_src_ptr.emplace_back(
unique_only_src[i].data_ptr<index_t>());
unique_only_src_size.emplace_back(cuda::CopyScalar<int64_t>{});
CUB_CALL(
DeviceSelect::Unique, sorted_only_src[i].data_ptr<index_t>(),
unique_only_src_ptr[i], unique_only_src_size[i].get(),
only_src[i].size(0));
}
stream.synchronize();
for (std::size_t i = 0; i < unique_only_src.size(); i++) {
unique_only_src[i] = unique_only_src[i].slice(
0, 0, static_cast<int64_t>(unique_only_src_size[i]));
}
std::vector<torch::Tensor> real_order;
for (std::size_t i = 0; i < unique_dst_ids.size(); i++) {
real_order.emplace_back(
torch::cat({unique_dst_ids[i], unique_only_src[i]}));
}
// Sort here so that binary search can be used to lookup new_ids.
std::vector<torch::Tensor> sorted_order, new_ids;
std::vector<index_t*> sorted_order_ptr;
std::vector<int64_t*> new_ids_ptr;
for (std::size_t i = 0; i < real_order.size(); i++) {
auto [sorted_order_i, new_ids_i] = Sort(real_order[i], num_bits);
sorted_order_ptr.emplace_back(sorted_order_i.data_ptr<index_t>());
new_ids_ptr.emplace_back(new_ids_i.data_ptr<int64_t>());
sorted_order.emplace_back(std::move(sorted_order_i));
new_ids.emplace_back(std::move(new_ids_i));
}
// Holds the found locations of the src and dst ids in the
// sorted_order. Later is used to lookup the new ids of the src_ids
// and dst_ids tensors.
std::vector<decltype(allocator.AllocateStorage<index_t>(0))>
new_dst_ids_loc;
for (std::size_t i = 0; i < sorted_order.size(); i++) {
new_dst_ids_loc.emplace_back(
allocator.AllocateStorage<index_t>(dst_ids[i].size(0)));
THRUST_CALL(
lower_bound, sorted_order_ptr[i],
sorted_order_ptr[i] + sorted_order[i].size(0), dst_ids_ptr[i],
dst_ids_ptr[i] + dst_ids[i].size(0), new_dst_ids_loc[i].get());
}
std::vector<cuda::CopyScalar<bool>> all_exist;
at::cuda::CUDAEvent all_exist_event;
bool should_record = false;
// Check if unique_dst_ids includes all dst_ids.
for (std::size_t i = 0; i < dst_ids.size(); i++) {
if (dst_ids[i].size(0) > 0) {
thrust::counting_iterator<int64_t> iota(0);
auto equal_it = thrust::make_transform_iterator(
iota, EqualityFunc<index_t>{
sorted_order_ptr[i], new_dst_ids_loc[i].get(),
dst_ids_ptr[i]});
all_exist.emplace_back(Min(equal_it, dst_ids[i].size(0)));
should_record = true;
} else {
all_exist.emplace_back(cuda::CopyScalar<bool>{});
}
}
if (should_record) all_exist_event.record();
std::vector<decltype(allocator.AllocateStorage<index_t>(0))>
new_src_ids_loc;
for (std::size_t i = 0; i < sorted_order.size(); i++) {
new_src_ids_loc.emplace_back(
allocator.AllocateStorage<index_t>(src_ids[i].size(0)));
THRUST_CALL(
lower_bound, sorted_order_ptr[i],
sorted_order_ptr[i] + sorted_order[i].size(0), src_ids_ptr[i],
src_ids_ptr[i] + src_ids[i].size(0), new_src_ids_loc[i].get());
}
// Finally, lookup the new compact ids of the src and dst tensors
// via gather operations.
std::vector<torch::Tensor> new_src_ids;
for (std::size_t i = 0; i < src_ids.size(); i++) {
new_src_ids.emplace_back(torch::empty_like(src_ids[i]));
THRUST_CALL(
gather, new_src_ids_loc[i].get(),
new_src_ids_loc[i].get() + src_ids[i].size(0),
new_ids[i].data_ptr<int64_t>(),
new_src_ids[i].data_ptr<index_t>());
}
// Perform check before we gather for the dst indices.
for (std::size_t i = 0; i < dst_ids.size(); i++) {
if (dst_ids[i].size(0) > 0) {
if (should_record) {
all_exist_event.synchronize();
should_record = false;
}
if (!static_cast<bool>(all_exist[i])) {
throw std::out_of_range("Some ids not found.");
}
}
}
std::vector<torch::Tensor> new_dst_ids;
for (std::size_t i = 0; i < dst_ids.size(); i++) {
new_dst_ids.emplace_back(torch::empty_like(dst_ids[i]));
THRUST_CALL(
gather, new_dst_ids_loc[i].get(),
new_dst_ids_loc[i].get() + dst_ids[i].size(0),
new_ids[i].data_ptr<int64_t>(),
new_dst_ids[i].data_ptr<index_t>());
}
std::vector<std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>>
results;
for (std::size_t i = 0; i < src_ids.size(); i++) {
results.emplace_back(
std::move(real_order[i]), std::move(new_src_ids[i]),
std::move(new_dst_ids[i]));
}
return results;
}));
}
std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>
UniqueAndCompactBatched(
const std::vector<torch::Tensor>& src_ids,
const std::vector<torch::Tensor>& dst_ids,
const std::vector<torch::Tensor>& unique_dst_ids, const int64_t rank,
const int64_t world_size) {
if (cuda::compute_capability() >= 70) {
// Utilizes a hash table based implementation, the mapped id of a vertex
// will be monotonically increasing as the first occurrence index of it in
// torch.cat([unique_dst_ids, src_ids]). Thus, it is deterministic.
return UniqueAndCompactBatchedHashMapBased(
src_ids, dst_ids, unique_dst_ids, rank, world_size);
}
TORCH_CHECK(
world_size <= 1,
"Cooperative Minibatching (arXiv:2310.12403) is not supported on "
"pre-Volta generation GPUs.");
// Utilizes a sort based algorithm, the mapped id of a vertex part of the
// src_ids but not part of the unique_dst_ids will be monotonically increasing
// as the actual vertex id increases. Thus, it is deterministic.
auto results3 =
UniqueAndCompactBatchedSortBased(src_ids, dst_ids, unique_dst_ids);
std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>
results4;
auto offsets = torch::zeros(
2 * results3.size(),
c10::TensorOptions().dtype(torch::kInt64).pinned_memory(true));
for (const auto& [a, b, c] : results3) {
auto d = offsets.slice(0, 0, 2);
d.data_ptr<int64_t>()[1] = a.size(0);
results4.emplace_back(a, b, c, d);
offsets = offsets.slice(0, 2);
}
return results4;
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
UniqueAndCompact(
const torch::Tensor src_ids, const torch::Tensor dst_ids,
const torch::Tensor unique_dst_ids, const int64_t rank,
const int64_t world_size) {
return UniqueAndCompactBatched(
{src_ids}, {dst_ids}, {unique_dst_ids}, rank, world_size)[0];
}
} // namespace ops
} // namespace graphbolt
+67
View File
@@ -0,0 +1,67 @@
/**
* Copyright (c) 2023 by Contributors
*
* @file utils.h
* @brief CUDA utilities.
*/
#ifndef GRAPHBOLT_CUDA_UTILS_H_
#define GRAPHBOLT_CUDA_UTILS_H_
// The cache line size of GPU.
constexpr int GPU_CACHE_LINE_SIZE = 128;
// The max number of threads per block.
constexpr int CUDA_MAX_NUM_THREADS = 1024;
namespace graphbolt {
namespace cuda {
/**
* @brief Returns the compute capability of the cuda device, e.g. 70 for Volta.
*/
inline int compute_capability(
int device = cuda::GetCurrentStream().device_index()) {
int sm_version;
CUDA_RUNTIME_CHECK(cub::SmVersion(sm_version, device));
return sm_version / 10;
};
/**
* @brief Calculate the number of threads needed given the size of the dimension
* to be processed.
*
* It finds the largest power of two that is less than or equal to the minimum
* of size and CUDA_MAX_NUM_THREADS.
*/
inline int FindNumThreads(int size) {
int ret = 1;
while ((ret << 1) <= std::min(size, CUDA_MAX_NUM_THREADS)) {
ret <<= 1;
}
return ret;
}
/**
* @brief Calculate the smallest number of bits needed to represent a given
* range of integers [0, range).
*/
template <typename T>
int NumberOfBits(const T& range) {
if (range <= 1) {
// ranges of 0 or 1 require no bits to store
return 0;
}
int bits = 1;
const auto urange = static_cast<std::make_unsigned_t<T>>(range);
while (bits < static_cast<int>(sizeof(T) * 8) && (1ull << bits) < urange) {
++bits;
}
return bits;
}
} // namespace cuda
} // namespace graphbolt
#endif // GRAPHBOLT_CUDA_UTILS_H_
+75
View File
@@ -0,0 +1,75 @@
/**
* Copyright (c) 2023 by Contributors
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* @file expand_indptr.cc
* @brief ExpandIndptr operators.
*/
#include <graphbolt/cuda_ops.h>
#include <torch/autograd.h>
#include "./macro.h"
#include "./utils.h"
namespace graphbolt {
namespace ops {
torch::Tensor ExpandIndptr(
torch::Tensor indptr, torch::ScalarType dtype,
torch::optional<torch::Tensor> node_ids,
torch::optional<int64_t> output_size) {
if (utils::is_on_gpu(indptr) &&
(!node_ids.has_value() || utils::is_on_gpu(node_ids.value()))) {
GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(c10::DeviceType::CUDA, "ExpandIndptr", {
return ExpandIndptrImpl(indptr, dtype, node_ids, output_size);
});
}
if (!node_ids.has_value()) {
return torch::repeat_interleave(indptr.diff(), output_size).to(dtype);
}
return node_ids.value().to(dtype).repeat_interleave(
indptr.diff(), 0, output_size);
}
torch::Tensor IndptrEdgeIds(
torch::Tensor indptr, torch::ScalarType dtype,
torch::optional<torch::Tensor> offset,
torch::optional<int64_t> output_size) {
if (utils::is_on_gpu(indptr) &&
(!offset.has_value() || utils::is_on_gpu(offset.value()))) {
GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(
c10::DeviceType::CUDA, "IndptrEdgeIds",
{ return IndptrEdgeIdsImpl(indptr, dtype, offset, output_size); });
}
TORCH_CHECK(false, "CPU implementation of IndptrEdgeIds is not available.");
}
TORCH_LIBRARY_IMPL(graphbolt, CPU, m) {
m.impl("expand_indptr", &ExpandIndptr);
}
#ifdef GRAPHBOLT_USE_CUDA
TORCH_LIBRARY_IMPL(graphbolt, CUDA, m) {
m.impl("expand_indptr", &ExpandIndptrImpl);
}
#endif
TORCH_LIBRARY_IMPL(graphbolt, Autograd, m) {
m.impl("expand_indptr", torch::autograd::autogradNotImplementedFallback());
}
TORCH_LIBRARY_IMPL(graphbolt, CPU, m) {
m.impl("indptr_edge_ids", &IndptrEdgeIds);
}
#ifdef GRAPHBOLT_USE_CUDA
TORCH_LIBRARY_IMPL(graphbolt, CUDA, m) {
m.impl("indptr_edge_ids", &IndptrEdgeIdsImpl);
}
#endif
TORCH_LIBRARY_IMPL(graphbolt, Autograd, m) {
m.impl("indptr_edge_ids", torch::autograd::autogradNotImplementedFallback());
}
} // namespace ops
} // namespace graphbolt
+55
View File
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2023 by Contributors
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* @file expand_indptr.h
* @brief ExpandIndptr operators.
*/
#ifndef GRAPHBOLT_EXPAND_INDPTR_H_
#define GRAPHBOLT_EXPAND_INDPTR_H_
#include <torch/script.h>
namespace graphbolt {
namespace ops {
/**
* @brief ExpandIndptr implements conversion from a given indptr offset
* tensor to a COO format tensor. If node_ids is not given, it is assumed to be
* equal to torch::arange(indptr.size(0) - 1, dtype=dtype).
*
* @param indptr The indptr offset tensor.
* @param dtype The dtype of the returned output tensor.
* @param node_ids 1D tensor represents the node ids.
* @param output_size Optional, value of indptr[-1]. Passing it eliminates CPU
* GPU synchronization.
*
* @return The resulting tensor.
*/
torch::Tensor ExpandIndptr(
torch::Tensor indptr, torch::ScalarType dtype,
torch::optional<torch::Tensor> node_ids = torch::nullopt,
torch::optional<int64_t> output_size = torch::nullopt);
/**
* @brief IndptrEdgeIdsImpl implements conversion from a given indptr offset
* tensor to a COO edge ids tensor. For a given indptr [0, 2, 5, 7] and offset
* tensor [0, 100, 200], the output will be [0, 1, 100, 101, 102, 201, 202]. If
* offset was not provided, the output would be [0, 1, 0, 1, 2, 0, 1].
*
* @param indptr The indptr offset tensor.
* @param dtype The dtype of the returned output tensor.
* @param offset The offset tensor.
* @param output_size Optional value of indptr[-1]. Passing it eliminates CPU
* GPU synchronization.
*
* @return The resulting tensor.
*/
torch::Tensor IndptrEdgeIds(
torch::Tensor indptr, torch::ScalarType dtype,
torch::optional<torch::Tensor> offset,
torch::optional<int64_t> output_size);
} // namespace ops
} // namespace graphbolt
#endif // GRAPHBOLT_EXPAND_INDPTR_H_
+101
View File
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file feature_cache.cc
* @brief Feature cache implementation on the CPU.
*/
#include "./feature_cache.h"
#include "./index_select.h"
#include "./utils.h"
namespace graphbolt {
namespace storage {
constexpr int kIntGrainSize = 64;
FeatureCache::FeatureCache(
const std::vector<int64_t>& shape, torch::ScalarType dtype, bool pin_memory)
: tensor_(torch::empty(
shape, c10::TensorOptions().dtype(dtype).pinned_memory(pin_memory))) {
}
torch::Tensor FeatureCache::Query(
torch::Tensor positions, torch::Tensor indices, int64_t size) {
const bool pin_memory =
utils::is_pinned(positions) || utils::is_pinned(indices);
std::vector<int64_t> output_shape{
tensor_.sizes().begin(), tensor_.sizes().end()};
output_shape[0] = size;
auto values =
torch::empty(output_shape, tensor_.options().pinned_memory(pin_memory));
const auto row_bytes = values.slice(0, 0, 1).numel() * values.element_size();
auto values_ptr = reinterpret_cast<std::byte*>(values.data_ptr());
const auto tensor_ptr = reinterpret_cast<std::byte*>(tensor_.data_ptr());
const auto positions_ptr = positions.data_ptr<int64_t>();
const auto indices_ptr = indices.data_ptr<int64_t>();
graphbolt::parallel_for_each(
0, positions.size(0), kIntGrainSize, [&](const int64_t i) {
std::memcpy(
values_ptr + indices_ptr[i] * row_bytes,
tensor_ptr + positions_ptr[i] * row_bytes, row_bytes);
});
return values;
}
c10::intrusive_ptr<Future<torch::Tensor>> FeatureCache::QueryAsync(
torch::Tensor positions, torch::Tensor indices, int64_t size) {
return async([=] { return Query(positions, indices, size); });
}
torch::Tensor FeatureCache::IndexSelect(torch::Tensor positions) {
return ops::IndexSelect(tensor_, positions);
}
void FeatureCache::Replace(torch::Tensor positions, torch::Tensor values) {
TORCH_CHECK(positions.size(0) == values.size(0));
if (values.numel() == 0) return;
const auto row_bytes = values.slice(0, 0, 1).numel() * values.element_size();
TORCH_CHECK(
row_bytes == tensor_.slice(0, 0, 1).numel() * tensor_.element_size(),
"The # bytes of a single row should match the cache's.");
auto values_ptr = reinterpret_cast<std::byte*>(values.data_ptr());
const auto tensor_ptr = reinterpret_cast<std::byte*>(tensor_.data_ptr());
const auto positions_ptr = positions.data_ptr<int64_t>();
graphbolt::parallel_for_each(
0, positions.size(0), kIntGrainSize, [&](const int64_t i) {
const auto position = positions_ptr[i];
if (position >= 0) {
std::memcpy(
tensor_ptr + position * row_bytes, values_ptr + i * row_bytes,
row_bytes);
}
});
}
c10::intrusive_ptr<Future<void>> FeatureCache::ReplaceAsync(
torch::Tensor positions, torch::Tensor values) {
return async([=] { return Replace(positions, values); });
}
c10::intrusive_ptr<FeatureCache> FeatureCache::Create(
const std::vector<int64_t>& shape, torch::ScalarType dtype,
bool pin_memory) {
return c10::make_intrusive<FeatureCache>(shape, dtype, pin_memory);
}
} // namespace storage
} // namespace graphbolt
+101
View File
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file feature_cache.h
* @brief Feature cache implementation on the CPU.
*/
#ifndef GRAPHBOLT_FEATURE_CACHE_H_
#define GRAPHBOLT_FEATURE_CACHE_H_
#include <graphbolt/async.h>
#include <torch/custom_class.h>
#include <torch/torch.h>
#include <vector>
namespace graphbolt {
namespace storage {
struct FeatureCache : public torch::CustomClassHolder {
/**
* @brief Constructor for the FeatureCache struct.
*
* @param shape The shape of the cache.
* @param dtype The dtype of elements stored in the cache.
* @param pin_memory Whether to pin the memory of the cache storage tensor.
*/
FeatureCache(
const std::vector<int64_t>& shape, torch::ScalarType dtype,
bool pin_memory);
bool IsPinned() const { return tensor_.is_pinned(); }
int64_t NumBytes() const { return tensor_.numel() * tensor_.element_size(); }
/**
* @brief The cache query function. Allocates an empty tensor `values` with
* size as the first dimension and runs
* values[indices[:positions.size(0)]] = cache_tensor[positions] before
* returning it.
*
* @param positions The positions of the queried items.
* @param indices The indices of the queried items among the original keys.
* Only the first portion corresponding to the provided positions tensor is
* used, e.g. indices[:positions.size(0)].
* @param size The size of the original keys, hence the first dimension of
* the output shape.
*
* @return The values tensor is returned. Its memory is pinned if pin_memory
* is true.
*/
torch::Tensor Query(
torch::Tensor positions, torch::Tensor indices, int64_t size);
c10::intrusive_ptr<Future<torch::Tensor>> QueryAsync(
torch::Tensor positions, torch::Tensor indices, int64_t size);
/**
* @brief The cache tensor index_select returns cache_tensor[positions].
*
* @param positions The positions of the queried items.
*
* @return The values tensor is returned on the same device as positions.
*/
torch::Tensor IndexSelect(torch::Tensor positions);
/**
* @brief The cache replace function.
*
* @param positions The positions to replace in the cache.
* @param values The values to be inserted into the cache.
*/
void Replace(torch::Tensor positions, torch::Tensor values);
c10::intrusive_ptr<Future<void>> ReplaceAsync(
torch::Tensor positions, torch::Tensor values);
static c10::intrusive_ptr<FeatureCache> Create(
const std::vector<int64_t>& shape, torch::ScalarType dtype,
bool pin_memory);
private:
torch::Tensor tensor_;
};
} // namespace storage
} // namespace graphbolt
#endif // GRAPHBOLT_FEATURE_CACHE_H_
File diff suppressed because it is too large Load Diff
+225
View File
@@ -0,0 +1,225 @@
/**
* Copyright (c) 2023 by Contributors
* @file index_select.cc
* @brief Index select operators.
*/
#include "./index_select.h"
#include <graphbolt/cuda_ops.h>
#include <graphbolt/fused_csc_sampling_graph.h>
#include <cstring>
#include <numeric>
#include "./macro.h"
#include "./utils.h"
namespace graphbolt {
namespace ops {
constexpr int kIntGrainSize = 64;
torch::Tensor IndexSelect(torch::Tensor input, torch::Tensor index) {
if (utils::is_on_gpu(index)) {
if (input.is_pinned()) {
GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(
c10::DeviceType::CUDA, "UVAIndexSelect",
{ return UVAIndexSelectImpl(input, index); });
} else {
return torch::index_select(input, 0, index);
}
}
auto output_shape = input.sizes().vec();
output_shape[0] = index.numel();
auto result = torch::empty(
output_shape, index.options()
.dtype(input.dtype())
.pinned_memory(utils::is_pinned(index)));
auto result_ptr = reinterpret_cast<std::byte*>(result.data_ptr());
const auto input_ptr = reinterpret_cast<std::byte*>(input.data_ptr());
const auto row_bytes = input.slice(0, 0, 1).numel() * input.element_size();
const auto stride = input.stride(0) * input.element_size();
const auto num_input_rows = input.size(0);
AT_DISPATCH_INDEX_TYPES(
index.scalar_type(), "IndexSelect::index::scalar_type()", ([&] {
const auto index_ptr = index.data_ptr<index_t>();
graphbolt::parallel_for(
0, index.size(0), kIntGrainSize, [&](int64_t begin, int64_t end) {
for (int64_t i = begin; i < end; i++) {
auto idx = index_ptr[i];
if (idx < 0) idx += num_input_rows;
if (idx < 0 || idx >= num_input_rows) {
// Throw IndexError via torch.
idx += input[num_input_rows].item<index_t>();
}
std::memcpy(
result_ptr + i * row_bytes, input_ptr + idx * stride,
row_bytes);
}
});
}));
return result;
}
c10::intrusive_ptr<Future<torch::Tensor>> IndexSelectAsync(
torch::Tensor input, torch::Tensor index) {
TORCH_CHECK(!utils::is_on_gpu(index) && !utils::is_on_gpu(input));
return async([=] { return IndexSelect(input, index); });
}
c10::intrusive_ptr<Future<torch::Tensor>> ScatterAsync(
torch::Tensor input, torch::Tensor index, torch::Tensor src) {
TORCH_CHECK(
!utils::is_on_gpu(input) && !utils::is_on_gpu(index) &&
!utils::is_on_gpu(src));
TORCH_CHECK(index.sizes().size() == 1, "index tensor needs to be 1d.");
for (size_t i = 1; i < input.sizes().size(); i++) {
TORCH_CHECK(
input.size(i) == src.size(i),
"dimension mismatch between input and src at ", i,
"th dimension: ", input.size(i), " != ", src.size(i), ".");
}
return async([=] {
const auto row_bytes = src.slice(0, 0, 1).numel() * src.element_size();
const auto src_ptr = reinterpret_cast<std::byte*>(src.data_ptr());
auto input_ptr = reinterpret_cast<std::byte*>(input.data_ptr());
AT_DISPATCH_INDEX_TYPES(
index.scalar_type(), "ScatterAsync::index::scalar_type()", ([&] {
const auto index_ptr = index.data_ptr<index_t>();
graphbolt::parallel_for(
0, index.size(0), kIntGrainSize, [&](int64_t begin, int64_t end) {
for (int64_t i = begin; i < end; i++) {
std::memcpy(
input_ptr + index_ptr[i] * row_bytes,
src_ptr + i * row_bytes, row_bytes);
}
});
}));
return input;
});
}
std::tuple<torch::Tensor, torch::Tensor> IndexSelectCSC(
torch::Tensor indptr, torch::Tensor indices, torch::Tensor nodes,
torch::optional<int64_t> output_size) {
TORCH_CHECK(
indices.sizes().size() == 1, "IndexSelectCSC only supports 1d tensors");
if (utils::is_on_gpu(nodes) && utils::is_accessible_from_gpu(indptr) &&
utils::is_accessible_from_gpu(indices)) {
GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(
c10::DeviceType::CUDA, "IndexSelectCSCImpl",
{ return IndexSelectCSCImpl(indptr, indices, nodes, output_size); });
}
auto [output_indptr, results] = IndexSelectCSCBatched(
indptr, std::vector{indices}, nodes, false, output_size);
return std::make_tuple(output_indptr, results.at(0));
}
std::tuple<torch::Tensor, std::vector<torch::Tensor>> IndexSelectCSCBatched(
torch::Tensor indptr, std::vector<torch::Tensor> indices_list,
torch::Tensor nodes, bool with_edge_ids,
torch::optional<int64_t> output_size) {
for (auto& indices : indices_list) {
TORCH_CHECK(
indices.sizes().size() == 1,
"IndexSelectCSCBatched only supports 1d tensors");
}
if (utils::is_on_gpu(nodes) && utils::is_accessible_from_gpu(indptr) &&
utils::are_accessible_from_gpu(indices_list)) {
GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(
c10::DeviceType::CUDA, "IndexSelectCSCImpl", {
return IndexSelectCSCBatchedImpl(
indptr, indices_list, nodes, with_edge_ids, output_size);
});
}
constexpr int kDefaultGrainSize = 128;
const auto num_nodes = nodes.size(0);
torch::Tensor output_indptr = torch::empty(
{num_nodes + 1}, nodes.options().dtype(indptr.scalar_type()));
std::vector<torch::Tensor> results;
torch::optional<torch::Tensor> edge_ids;
AT_DISPATCH_INDEX_TYPES(
indptr.scalar_type(), "IndexSelectCSCBatched::indptr", ([&] {
using indptr_t = index_t;
const auto indptr_data = indptr.data_ptr<indptr_t>();
auto out_indptr_data = output_indptr.data_ptr<indptr_t>();
out_indptr_data[0] = 0;
AT_DISPATCH_INDEX_TYPES(
nodes.scalar_type(), "IndexSelectCSCBatched::nodes", ([&] {
const auto nodes_data = nodes.data_ptr<index_t>();
torch::parallel_for(
0, num_nodes, kDefaultGrainSize,
[&](int64_t begin, int64_t end) {
for (int64_t i = begin; i < end; i++) {
const auto node_id = nodes_data[i];
const auto degree =
indptr_data[node_id + 1] - indptr_data[node_id];
out_indptr_data[i + 1] = degree;
}
});
output_indptr = output_indptr.cumsum(0, indptr.scalar_type());
out_indptr_data = output_indptr.data_ptr<indptr_t>();
TORCH_CHECK(
!output_size.has_value() ||
out_indptr_data[num_nodes] == *output_size,
"An incorrect output_size argument was provided.");
output_size = out_indptr_data[num_nodes];
for (const auto& indices : indices_list) {
results.push_back(torch::empty(
*output_size,
nodes.options().dtype(indices.scalar_type())));
}
if (with_edge_ids) {
edge_ids = torch::empty(
*output_size, nodes.options().dtype(indptr.scalar_type()));
}
torch::parallel_for(
0, num_nodes, kDefaultGrainSize,
[&](int64_t begin, int64_t end) {
for (int64_t i = begin; i < end; i++) {
const auto output_offset = out_indptr_data[i];
const auto numel = out_indptr_data[i + 1] - output_offset;
const auto input_offset = indptr_data[nodes_data[i]];
for (size_t tensor_id = 0;
tensor_id < indices_list.size(); tensor_id++) {
auto output = reinterpret_cast<std::byte*>(
results[tensor_id].data_ptr());
const auto input = reinterpret_cast<std::byte*>(
indices_list[tensor_id].data_ptr());
const auto element_size =
indices_list[tensor_id].element_size();
std::memcpy(
output + output_offset * element_size,
input + input_offset * element_size,
element_size * numel);
}
if (edge_ids.has_value()) {
auto output = edge_ids->data_ptr<indptr_t>();
std::iota(
output + output_offset,
output + output_offset + numel, input_offset);
}
}
});
}));
}));
if (edge_ids) results.push_back(*edge_ids);
return std::make_tuple(output_indptr, results);
}
c10::intrusive_ptr<
Future<std::tuple<torch::Tensor, std::vector<torch::Tensor>>>>
IndexSelectCSCBatchedAsync(
torch::Tensor indptr, std::vector<torch::Tensor> indices_list,
torch::Tensor nodes, bool with_edge_ids,
torch::optional<int64_t> output_size) {
return async(
[=] {
return IndexSelectCSCBatched(
indptr, indices_list, nodes, with_edge_ids, output_size);
},
utils::is_on_gpu(nodes));
}
} // namespace ops
} // namespace graphbolt
+105
View File
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2023 by Contributors
* @file index_select.h
* @brief Index select operators.
*/
#ifndef GRAPHBOLT_INDEX_SELECT_H_
#define GRAPHBOLT_INDEX_SELECT_H_
#include <graphbolt/async.h>
#include <torch/script.h>
namespace graphbolt {
namespace ops {
/**
* @brief Select columns for a sparse matrix in a CSC format according to nodes
* tensor.
*
* NOTE:
* 1. The shape of all tensors must be 1-D.
* 2. If indices is on pinned memory and nodes is on pinned memory or GPU
* memory, then UVAIndexSelectCSCImpl will be called. If indices is on GPU
* memory, then IndexSelectCSCImpl will be called. Otherwise,
* FusedCSCSamplingGraph::InSubgraph will be called.
*
* @param indptr Indptr tensor containing offsets with shape (N,).
* @param indices Indices tensor with edge information of shape (indptr[N],).
* @param nodes Nodes tensor with shape (M,).
* @param output_size The total number of edges being copied.
* @return (torch::Tensor, torch::Tensor) Output indptr and indices tensors of
* shapes (M + 1,) and ((indptr[nodes + 1] - indptr[nodes]).sum(),).
*/
std::tuple<torch::Tensor, torch::Tensor> IndexSelectCSC(
torch::Tensor indptr, torch::Tensor indices, torch::Tensor nodes,
torch::optional<int64_t> output_size = torch::nullopt);
/**
* @brief Select rows from input tensor according to index tensor.
*
* NOTE:
* 1. The shape of input tensor can be multi-dimensional, but the index tensor
* must be 1-D.
* 2. If input is on pinned memory and index is on pinned memory or GPU memory,
* then UVAIndexSelectImpl will be called. Otherwise, torch::index_select will
* be called.
*
* @param input Input tensor with shape (N, ...).
* @param index Index tensor with shape (M,).
* @return torch::Tensor Output tensor with shape (M, ...).
*/
torch::Tensor IndexSelect(torch::Tensor input, torch::Tensor index);
/**
* @brief The async version of IndexSelect, available for only CPU tensors.
*
* @return Returns a future containing a torch::Tensor.
*/
c10::intrusive_ptr<Future<torch::Tensor>> IndexSelectAsync(
torch::Tensor input, torch::Tensor index);
/**
* @brief The async version of operation input[index] = src.
* @param input The input tensor.
* @param index The index tensor into input.
* @param src The src tensor being assigned into input.
*
* @return Returns a future containing input, a torch::Tensor.
*/
c10::intrusive_ptr<Future<torch::Tensor>> ScatterAsync(
torch::Tensor input, torch::Tensor index, torch::Tensor src);
/**
* @brief Select columns for a sparse matrix in a CSC format according to nodes
* tensor.
*
* NOTE: The shape of all tensors must be 1-D.
*
* @param indptr Indptr tensor containing offsets with shape (N,).
* @param indices_list Vector of indices tensor with edge information of shape
* (indptr[N],).
* @param nodes Nodes tensor with shape (M,).
* @param with_edge_ids Whether to return edge ids tensor corresponding to
* sliced edges as the last element of the output.
* @param output_size The total number of edges being copied.
*
* @return (torch::Tensor, std::vector<torch::Tensor>) Output indptr and vector
* of indices tensors of shapes (M + 1,) and ((indptr[nodes + 1] -
* indptr[nodes]).sum(),).
*/
std::tuple<torch::Tensor, std::vector<torch::Tensor>> IndexSelectCSCBatched(
torch::Tensor indptr, std::vector<torch::Tensor> indices_list,
torch::Tensor nodes, bool with_edge_ids,
torch::optional<int64_t> output_size);
c10::intrusive_ptr<
Future<std::tuple<torch::Tensor, std::vector<torch::Tensor>>>>
IndexSelectCSCBatchedAsync(
torch::Tensor indptr, std::vector<torch::Tensor> indices_list,
torch::Tensor nodes, bool with_edge_ids,
torch::optional<int64_t> output_size);
} // namespace ops
} // namespace graphbolt
#endif // GRAPHBOLT_INDEX_SELECT_H_
+82
View File
@@ -0,0 +1,82 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file io_uring.cc
* @brief io_uring related functions.
*/
#include "./io_uring.h"
#ifdef HAVE_LIBRARY_LIBURING
#include <errno.h>
#include <liburing.h>
#include <liburing/io_uring.h>
#include <stddef.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <memory>
#include <mutex>
struct io_uring_probe_destroyer {
void operator()(struct io_uring_probe* p) {
if (p) io_uring_free_probe(p);
}
};
#endif
namespace graphbolt {
namespace io_uring {
bool IsAvailable() {
#ifdef HAVE_LIBRARY_LIBURING
/** @brief The cached value of whether io_uring is available. */
static bool cached_is_available;
/** @brief Ensure cached_is_available is initialized once and thread-safe. */
static std::once_flag initialization_flag;
std::call_once(initialization_flag, []() {
// https://unix.stackexchange.com/a/596284/314554
cached_is_available =
!(syscall(
__NR_io_uring_register, 0, IORING_UNREGISTER_BUFFERS, NULL, 0) &&
errno == ENOSYS);
std::unique_ptr<struct io_uring_probe, io_uring_probe_destroyer> probe(
io_uring_get_probe(), io_uring_probe_destroyer());
if (probe.get()) {
cached_is_available =
cached_is_available &&
io_uring_opcode_supported(probe.get(), IORING_OP_READ);
cached_is_available =
cached_is_available &&
io_uring_opcode_supported(probe.get(), IORING_OP_READV);
} else {
cached_is_available = false;
}
});
return cached_is_available;
#else
return false;
#endif
}
void SetNumThreads(int64_t count) { num_threads = count; }
} // namespace io_uring
} // namespace graphbolt
+42
View File
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file io_uring.h
* @brief io_uring related functions.
*/
#ifndef GRAPHBOLT_IO_URING_H_
#define GRAPHBOLT_IO_URING_H_
#include <cstdint>
#include <optional>
namespace graphbolt {
namespace io_uring {
bool IsAvailable();
/** @brief Set a limit on # background io_uring threads. */
inline std::optional<int64_t> num_threads;
/**
* @brief Set the number of background io_uring threads.
*/
void SetNumThreads(int64_t count);
} // namespace io_uring
} // namespace graphbolt
#endif // GRAPHBOLT_IO_URING_H_
+77
View File
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2023 by Contributors
*
* @file isin.cc
* @brief Isin op.
*/
#include <graphbolt/cuda_ops.h>
#include <graphbolt/isin.h>
#include "./macro.h"
#include "./utils.h"
namespace {
static constexpr int kSearchGrainSize = 4096;
} // namespace
namespace graphbolt {
namespace sampling {
torch::Tensor IsInCPU(
const torch::Tensor& elements, const torch::Tensor& test_elements) {
torch::Tensor sorted_test_elements;
std::tie(sorted_test_elements, std::ignore) = test_elements.sort(
/*stable=*/false, /*dim=*/0, /*descending=*/false);
torch::Tensor result = torch::empty_like(elements, torch::kBool);
size_t num_test_elements = test_elements.size(0);
size_t num_elements = elements.size(0);
AT_DISPATCH_INTEGRAL_TYPES(
elements.scalar_type(), "IsInOperation", ([&] {
const scalar_t* elements_ptr = elements.data_ptr<scalar_t>();
const scalar_t* sorted_test_elements_ptr =
sorted_test_elements.data_ptr<scalar_t>();
bool* result_ptr = result.data_ptr<bool>();
torch::parallel_for(
0, num_elements, kSearchGrainSize, [&](size_t start, size_t end) {
for (auto i = start; i < end; i++) {
result_ptr[i] = std::binary_search(
sorted_test_elements_ptr,
sorted_test_elements_ptr + num_test_elements,
elements_ptr[i]);
}
});
}));
return result;
}
torch::Tensor IsIn(
const torch::Tensor& elements, const torch::Tensor& test_elements) {
if (utils::is_on_gpu(elements) && utils::is_on_gpu(test_elements)) {
GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(
c10::DeviceType::CUDA, "IsInOperation",
{ return ops::IsIn(elements, test_elements); });
} else {
return IsInCPU(elements, test_elements);
}
}
torch::Tensor IsNotInIndex(
const torch::Tensor& elements, const torch::Tensor& test_elements) {
auto mask = IsIn(elements, test_elements);
if (utils::is_on_gpu(mask)) {
GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(
c10::DeviceType::CUDA, "NonzeroOperation",
{ return ops::Nonzero(mask, true); });
}
return torch::nonzero(torch::logical_not(mask)).squeeze(1);
}
c10::intrusive_ptr<Future<torch::Tensor>> IsNotInIndexAsync(
const torch::Tensor& elements, const torch::Tensor& test_elements) {
return async([=] { return IsNotInIndex(elements, test_elements); });
}
} // namespace sampling
} // namespace graphbolt
+40
View File
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2023 by Contributors
* @file macro.h
* @brief Graphbolt macros.
*/
#ifndef GRAPHBOLT_MACRO_H_
#define GRAPHBOLT_MACRO_H_
#include <torch/script.h>
namespace graphbolt {
// Dispatch operator implementation function to CUDA device only.
#ifdef GRAPHBOLT_USE_CUDA
#define GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(device_type, name, ...) \
if (device_type == c10::DeviceType::CUDA) { \
[[maybe_unused]] auto XPU = c10::DeviceType::CUDA; \
__VA_ARGS__ \
} else { \
TORCH_CHECK(false, name, " is only available on CUDA device."); \
}
#else
#define GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(device_type, name, ...) \
TORCH_CHECK(false, name, " is only available on CUDA device.");
#endif
// This includes all integer, float and boolean types.
#define GRAPHBOLT_DISPATCH_CASE_ALL_TYPES(...) \
AT_DISPATCH_CASE_ALL_TYPES(__VA_ARGS__) \
AT_DISPATCH_CASE(at::ScalarType::Half, __VA_ARGS__) \
AT_DISPATCH_CASE(at::ScalarType::BFloat16, __VA_ARGS__) \
AT_DISPATCH_CASE(at::ScalarType::Bool, __VA_ARGS__)
#define GRAPHBOLT_DISPATCH_ALL_TYPES(TYPE, NAME, ...) \
AT_DISPATCH_SWITCH(TYPE, NAME, GRAPHBOLT_DISPATCH_CASE_ALL_TYPES(__VA_ARGS__))
} // namespace graphbolt
#endif // GRAPHBOLT_MACRO_H_
+523
View File
@@ -0,0 +1,523 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file partitioned_cache_policy.cc
* @brief Partitioned cache policy implementation on the CPU.
*/
#include "./partitioned_cache_policy.h"
#include <algorithm>
#include <limits>
#include <numeric>
#include "./utils.h"
namespace graphbolt {
namespace storage {
constexpr int kIntGrainSize = 256;
torch::Tensor AddOffset(torch::Tensor keys, int64_t offset) {
if (offset == 0) return keys;
auto output = torch::empty_like(
keys, keys.options().pinned_memory(utils::is_pinned(keys)));
AT_DISPATCH_INDEX_TYPES(
keys.scalar_type(), "AddOffset", ([&] {
auto keys_ptr = keys.data_ptr<index_t>();
auto output_ptr = output.data_ptr<index_t>();
graphbolt::parallel_for_each(
0, keys.numel(), kIntGrainSize, [&](int64_t i) {
const auto result = keys_ptr[i] + offset;
if constexpr (!std::is_same_v<index_t, int64_t>) {
TORCH_CHECK(
std::numeric_limits<index_t>::min() <= result &&
result <= std::numeric_limits<index_t>::max());
}
output_ptr[i] = static_cast<index_t>(result);
});
}));
return output;
}
template <typename CachePolicy>
PartitionedCachePolicy::PartitionedCachePolicy(
CachePolicy, int64_t capacity, int64_t num_partitions)
: capacity_(capacity) {
TORCH_CHECK(num_partitions >= 1, "# partitions need to be positive.");
for (int64_t i = 0; i < num_partitions; i++) {
const auto begin = i * capacity / num_partitions;
const auto end = (i + 1) * capacity / num_partitions;
policies_.emplace_back(std::make_unique<CachePolicy>(end - begin));
}
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>
PartitionedCachePolicy::Partition(torch::Tensor keys) {
const int64_t num_parts = policies_.size();
torch::Tensor offsets = torch::empty(
num_parts * num_parts + 1, keys.options().dtype(torch::kInt64));
auto offsets_ptr = offsets.data_ptr<int64_t>();
std::fill_n(offsets_ptr, offsets.size(0), int64_t{});
auto indices = torch::empty_like(keys, keys.options().dtype(torch::kInt64));
auto part_id = torch::empty_like(keys, keys.options().dtype(torch::kInt32));
const auto num_keys = keys.size(0);
auto part_id_ptr = part_id.data_ptr<int32_t>();
AT_DISPATCH_INDEX_TYPES(
keys.scalar_type(), "PartitionedCachePolicy::partition", ([&] {
auto keys_ptr = keys.data_ptr<index_t>();
namespace gb = graphbolt;
gb::parallel_for_each(0, num_parts, 1, [&](int64_t tid) {
const auto begin = tid * num_keys / num_parts;
const auto end = (tid + 1) * num_keys / num_parts;
for (int64_t i = begin; i < end; i++) {
const auto part_id = PartAssignment(keys_ptr[i]);
offsets_ptr[tid * num_parts + part_id]++;
part_id_ptr[i] = part_id;
}
});
}));
// Transpose the offsets tensor, take cumsum and transpose back.
auto offsets_permuted = torch::empty_like(offsets);
auto offsets_permuted_ptr = offsets_permuted.data_ptr<int64_t>();
graphbolt::parallel_for_each(
0, num_parts * num_parts, kIntGrainSize, [&](int64_t i) {
const auto part_id = i % num_parts;
const auto tid = i / num_parts;
// + 1 so that we have exclusive_scan after torch.cumsum().
offsets_permuted_ptr[part_id * num_parts + tid + 1] = offsets_ptr[i];
});
offsets_permuted_ptr[0] = 0;
// offsets = offsets_permuted.cumsum(0); @TODO implement this in parallel.
std::inclusive_scan(
offsets_permuted_ptr, offsets_permuted_ptr + num_parts * num_parts + 1,
offsets_ptr);
offsets_ptr = offsets.data_ptr<int64_t>();
graphbolt::parallel_for_each(
0, num_parts * num_parts, kIntGrainSize, [&](int64_t i) {
const auto part_id = i % num_parts;
const auto tid = i / num_parts;
offsets_permuted_ptr[i] = offsets_ptr[part_id * num_parts + tid];
});
auto indices_ptr = indices.data_ptr<int64_t>();
auto permuted_keys = torch::empty_like(keys);
auto offsets_sliced = torch::empty(num_parts + 1, offsets.options());
auto offsets_sliced_ptr = offsets_sliced.data_ptr<int64_t>();
offsets_sliced_ptr[0] = 0;
AT_DISPATCH_INDEX_TYPES(
keys.scalar_type(), "PartitionedCachePolicy::partition", ([&] {
auto keys_ptr = keys.data_ptr<index_t>();
auto permuted_keys_ptr = permuted_keys.data_ptr<index_t>();
namespace gb = graphbolt;
gb::parallel_for_each(0, num_parts, 1, [&](int64_t tid) {
const auto begin = tid * num_keys / num_parts;
const auto end = (tid + 1) * num_keys / num_parts;
for (int64_t i = begin; i < end; i++) {
const auto part_id = part_id_ptr[i];
auto& offset = offsets_permuted_ptr[tid * num_parts + part_id];
indices_ptr[offset] = i;
permuted_keys_ptr[offset++] = keys_ptr[i];
}
offsets_sliced_ptr[tid + 1] = offsets_ptr[(tid + 1) * num_parts];
});
}));
return {offsets_sliced, indices, permuted_keys};
}
std::tuple<
torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor,
torch::Tensor>
PartitionedCachePolicy::Query(torch::Tensor keys, const int64_t offset) {
keys = AddOffset(keys, offset);
if (policies_.size() == 1) {
std::lock_guard lock(mtx_);
auto [positions, output_indices, missing_keys, found_pointers] =
policies_[0]->Query(keys);
auto found_and_missing_offsets = torch::empty(4, found_pointers.options());
auto found_and_missing_offsets_ptr =
found_and_missing_offsets.data_ptr<int64_t>();
// Found offsets part.
found_and_missing_offsets_ptr[0] = 0;
found_and_missing_offsets_ptr[1] = found_pointers.size(0);
// Missing offsets part.
found_and_missing_offsets_ptr[2] = 0;
found_and_missing_offsets_ptr[3] = missing_keys.size(0);
auto found_offsets = found_and_missing_offsets.slice(0, 0, 2);
auto missing_offsets = found_and_missing_offsets.slice(0, 2);
missing_keys = AddOffset(missing_keys, -offset);
return {positions, output_indices, missing_keys,
found_pointers, found_offsets, missing_offsets};
};
torch::Tensor offsets, indices, permuted_keys;
std::tie(offsets, indices, permuted_keys) = Partition(keys);
auto offsets_ptr = offsets.data_ptr<int64_t>();
auto indices_ptr = indices.data_ptr<int64_t>();
std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>
results(policies_.size());
torch::Tensor result_offsets_tensor =
torch::empty(policies_.size() * 2 + 1, offsets.options());
auto result_offsets = result_offsets_tensor.data_ptr<int64_t>();
namespace gb = graphbolt;
{
std::lock_guard lock(mtx_);
gb::parallel_for_each(0, policies_.size(), 1, [&](int64_t tid) {
const auto begin = offsets_ptr[tid];
const auto end = offsets_ptr[tid + 1];
results[tid] =
policies_.at(tid)->Query(permuted_keys.slice(0, begin, end));
result_offsets[tid] = std::get<0>(results[tid]).size(0);
result_offsets[tid + policies_.size()] =
std::get<2>(results[tid]).size(0);
});
}
std::exclusive_scan(
result_offsets, result_offsets + result_offsets_tensor.size(0),
result_offsets, 0);
torch::Tensor positions = torch::empty(
result_offsets[policies_.size()],
std::get<0>(results[0]).options().pinned_memory(utils::is_pinned(keys)));
torch::Tensor output_indices = torch::empty_like(
indices, indices.options().pinned_memory(utils::is_pinned(keys)));
torch::Tensor missing_keys = torch::empty(
indices.size(0) - positions.size(0),
std::get<2>(results[0]).options().pinned_memory(utils::is_pinned(keys)));
torch::Tensor found_pointers = torch::empty(
positions.size(0),
std::get<3>(results[0]).options().pinned_memory(utils::is_pinned(keys)));
auto missing_offsets =
torch::empty(policies_.size() + 1, result_offsets_tensor.options());
auto output_indices_ptr = output_indices.data_ptr<int64_t>();
auto missing_offsets_ptr = missing_offsets.data_ptr<int64_t>();
missing_offsets_ptr[0] = 0;
gb::parallel_for_each(0, policies_.size(), 1, [&](int64_t tid) {
auto out_index_ptr = indices_ptr + offsets_ptr[tid];
auto begin = result_offsets[tid];
auto end = result_offsets[tid + 1];
const auto num_selected = end - begin;
auto indices_ptr = std::get<1>(results[tid]).data_ptr<int64_t>();
for (int64_t i = 0; i < num_selected; i++) {
output_indices_ptr[begin + i] = out_index_ptr[indices_ptr[i]];
}
auto selected_positions_ptr = std::get<0>(results[tid]).data_ptr<int64_t>();
std::transform(
selected_positions_ptr, selected_positions_ptr + num_selected,
positions.data_ptr<int64_t>() + begin,
[off = tid * capacity_ / policies_.size()](auto x) { return x + off; });
auto selected_pointers_ptr = std::get<3>(results[tid]).data_ptr<int64_t>();
std::copy(
selected_pointers_ptr, selected_pointers_ptr + num_selected,
found_pointers.data_ptr<int64_t>() + begin);
begin = result_offsets[policies_.size() + tid];
end = result_offsets[policies_.size() + tid + 1];
missing_offsets[tid + 1] = end - result_offsets[policies_.size()];
const auto num_missing = end - begin;
for (int64_t i = 0; i < num_missing; i++) {
output_indices_ptr[begin + i] =
out_index_ptr[indices_ptr[i + num_selected]];
}
std::memcpy(
reinterpret_cast<std::byte*>(missing_keys.data_ptr()) +
(begin - positions.size(0)) * missing_keys.element_size(),
std::get<2>(results[tid]).data_ptr(),
num_missing * missing_keys.element_size());
});
auto found_offsets = result_offsets_tensor.slice(0, 0, policies_.size() + 1);
missing_keys = AddOffset(missing_keys, -offset);
return std::make_tuple(
positions, output_indices, missing_keys, found_pointers, found_offsets,
missing_offsets);
}
c10::intrusive_ptr<Future<std::vector<torch::Tensor>>>
PartitionedCachePolicy::QueryAsync(torch::Tensor keys, const int64_t offset) {
return async([=] {
auto
[positions, output_indices, missing_keys, found_pointers, found_offsets,
missing_offsets] = Query(keys, offset);
return std::vector{positions, output_indices, missing_keys,
found_pointers, found_offsets, missing_offsets};
});
}
std::tuple<
torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor,
torch::Tensor>
PartitionedCachePolicy::QueryAndReplace(
torch::Tensor keys, const int64_t offset) {
keys = AddOffset(keys, offset);
if (policies_.size() == 1) {
std::lock_guard lock(mtx_);
auto [positions, output_indices, pointers, missing_keys] =
policies_[0]->QueryAndReplace(keys);
auto found_and_missing_offsets = torch::empty(4, pointers.options());
auto found_and_missing_offsets_ptr =
found_and_missing_offsets.data_ptr<int64_t>();
// Found offsets part.
found_and_missing_offsets_ptr[0] = 0;
found_and_missing_offsets_ptr[1] = keys.size(0) - missing_keys.size(0);
// Missing offsets part.
found_and_missing_offsets_ptr[2] = 0;
found_and_missing_offsets_ptr[3] = missing_keys.size(0);
auto found_offsets = found_and_missing_offsets.slice(0, 0, 2);
auto missing_offsets = found_and_missing_offsets.slice(0, 2);
missing_keys = AddOffset(missing_keys, -offset);
return {positions, output_indices, pointers,
missing_keys, found_offsets, missing_offsets};
}
torch::Tensor offsets, indices, permuted_keys;
std::tie(offsets, indices, permuted_keys) = Partition(keys);
auto offsets_ptr = offsets.data_ptr<int64_t>();
auto indices_ptr = indices.data_ptr<int64_t>();
std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>
results(policies_.size());
torch::Tensor result_offsets_tensor =
torch::empty(policies_.size() * 2 + 1, offsets.options());
auto result_offsets = result_offsets_tensor.data_ptr<int64_t>();
namespace gb = graphbolt;
{
std::lock_guard lock(mtx_);
gb::parallel_for_each(0, policies_.size(), 1, [&](int64_t tid) {
const auto begin = offsets_ptr[tid];
const auto end = offsets_ptr[tid + 1];
results[tid] = policies_.at(tid)->QueryAndReplace(
permuted_keys.slice(0, begin, end));
const auto missing_cnt = std::get<3>(results[tid]).size(0);
result_offsets[tid] = end - begin - missing_cnt;
result_offsets[tid + policies_.size()] = missing_cnt;
});
}
std::exclusive_scan(
result_offsets, result_offsets + result_offsets_tensor.size(0),
result_offsets, 0);
torch::Tensor positions = torch::empty(
keys.size(0),
std::get<0>(results[0]).options().pinned_memory(utils::is_pinned(keys)));
torch::Tensor output_indices = torch::empty_like(
indices, indices.options().pinned_memory(utils::is_pinned(keys)));
torch::Tensor pointers = torch::empty(
keys.size(0),
std::get<2>(results[0]).options().pinned_memory(utils::is_pinned(keys)));
torch::Tensor missing_keys = torch::empty(
result_offsets[2 * policies_.size()] - result_offsets[policies_.size()],
std::get<3>(results[0]).options().pinned_memory(utils::is_pinned(keys)));
auto missing_offsets =
torch::empty(policies_.size() + 1, result_offsets_tensor.options());
auto positions_ptr = positions.data_ptr<int64_t>();
auto output_indices_ptr = output_indices.data_ptr<int64_t>();
auto pointers_ptr = pointers.data_ptr<int64_t>();
auto missing_offsets_ptr = missing_offsets.data_ptr<int64_t>();
missing_offsets_ptr[0] = 0;
gb::parallel_for_each(0, policies_.size(), 1, [&](int64_t tid) {
auto out_index_ptr = indices_ptr + offsets_ptr[tid];
auto begin = result_offsets[tid];
auto end = result_offsets[tid + 1];
const auto num_selected = end - begin;
auto indices_ptr = std::get<1>(results[tid]).data_ptr<int64_t>();
for (int64_t i = 0; i < num_selected; i++) {
output_indices_ptr[begin + i] = out_index_ptr[indices_ptr[i]];
}
auto selected_positions_ptr = std::get<0>(results[tid]).data_ptr<int64_t>();
std::transform(
selected_positions_ptr, selected_positions_ptr + num_selected,
positions_ptr + begin,
[off = tid * capacity_ / policies_.size()](auto x) { return x + off; });
auto selected_pointers_ptr = std::get<2>(results[tid]).data_ptr<int64_t>();
std::copy(
selected_pointers_ptr, selected_pointers_ptr + num_selected,
pointers_ptr + begin);
begin = result_offsets[policies_.size() + tid];
end = result_offsets[policies_.size() + tid + 1];
missing_offsets[tid + 1] = end - result_offsets[policies_.size()];
const auto num_missing = end - begin;
for (int64_t i = 0; i < num_missing; i++) {
output_indices_ptr[begin + i] =
out_index_ptr[indices_ptr[i + num_selected]];
}
auto missing_positions_ptr = selected_positions_ptr + num_selected;
std::transform(
missing_positions_ptr, missing_positions_ptr + num_missing,
positions_ptr + begin,
[off = tid * capacity_ / policies_.size()](auto x) { return x + off; });
auto missing_pointers_ptr = selected_pointers_ptr + num_selected;
std::copy(
missing_pointers_ptr, missing_pointers_ptr + num_missing,
pointers_ptr + begin);
std::memcpy(
reinterpret_cast<std::byte*>(missing_keys.data_ptr()) +
(begin - result_offsets[policies_.size()]) *
missing_keys.element_size(),
std::get<3>(results[tid]).data_ptr(),
num_missing * missing_keys.element_size());
});
auto found_offsets = result_offsets_tensor.slice(0, 0, policies_.size() + 1);
missing_keys = AddOffset(missing_keys, -offset);
return std::make_tuple(
positions, output_indices, pointers, missing_keys, found_offsets,
missing_offsets);
}
c10::intrusive_ptr<Future<std::vector<torch::Tensor>>>
PartitionedCachePolicy::QueryAndReplaceAsync(
torch::Tensor keys, const int64_t offset) {
return async([=] {
auto
[positions, output_indices, pointers, missing_keys, found_offsets,
missing_offsets] = QueryAndReplace(keys, offset);
return std::vector{positions, output_indices, pointers,
missing_keys, found_offsets, missing_offsets};
});
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>
PartitionedCachePolicy::Replace(
torch::Tensor keys, torch::optional<torch::Tensor> offsets,
const int64_t offset) {
keys = AddOffset(keys, offset);
if (policies_.size() == 1) {
std::lock_guard lock(mtx_);
auto [positions, pointers] = policies_[0]->Replace(keys);
if (!offsets.has_value()) {
offsets = torch::empty(2, pointers.options());
auto offsets_ptr = offsets->data_ptr<int64_t>();
offsets_ptr[0] = 0;
offsets_ptr[1] = pointers.size(0);
}
return {positions, pointers, *offsets};
}
const auto offsets_provided = offsets.has_value();
torch::Tensor indices, permuted_keys;
if (!offsets_provided) {
std::tie(offsets, indices, permuted_keys) = Partition(keys);
} else {
permuted_keys = keys;
}
auto output_positions = torch::empty_like(
keys, keys.options()
.dtype(torch::kInt64)
.pinned_memory(utils::is_pinned(keys)));
auto output_pointers = torch::empty_like(
keys, keys.options()
.dtype(torch::kInt64)
.pinned_memory(utils::is_pinned(keys)));
auto offsets_ptr = offsets->data_ptr<int64_t>();
auto indices_ptr = offsets_provided ? nullptr : indices.data_ptr<int64_t>();
auto output_positions_ptr = output_positions.data_ptr<int64_t>();
auto output_pointers_ptr = output_pointers.data_ptr<int64_t>();
namespace gb = graphbolt;
std::unique_lock lock(mtx_);
std::atomic<size_t> semaphore = policies_.size();
gb::parallel_for_each(0, policies_.size(), 1, [&](int64_t tid) {
const auto begin = offsets_ptr[tid];
const auto end = offsets_ptr[tid + 1];
auto [positions, pointers] =
policies_.at(tid)->Replace(permuted_keys.slice(0, begin, end));
const auto ticket = semaphore.fetch_add(-1, std::memory_order_release) - 1;
if (ticket == 0) {
// This thread was the last thread in the critical region.
lock.unlock();
}
auto positions_ptr = positions.data_ptr<int64_t>();
const auto off = tid * capacity_ / policies_.size();
if (indices_ptr) {
for (int64_t i = 0; i < positions.size(0); i++) {
output_positions_ptr[indices_ptr[begin + i]] = positions_ptr[i] + off;
}
} else {
std::transform(
positions_ptr, positions_ptr + positions.size(0),
output_positions_ptr + begin, [off](auto x) { return x + off; });
}
auto pointers_ptr = pointers.data_ptr<int64_t>();
std::copy(
pointers_ptr, pointers_ptr + pointers.size(0),
output_pointers_ptr + begin);
});
return {output_positions, output_pointers, *offsets};
}
c10::intrusive_ptr<Future<std::vector<torch::Tensor>>>
PartitionedCachePolicy::ReplaceAsync(
torch::Tensor keys, torch::optional<torch::Tensor> offsets,
const int64_t offset) {
return async([=] {
auto [positions, pointers, offsets_out] = Replace(keys, offsets, offset);
return std::vector{positions, pointers, offsets_out};
});
}
template <bool write>
void PartitionedCachePolicy::ReadingWritingCompletedImpl(
torch::Tensor pointers, torch::Tensor offsets) {
if (policies_.size() == 1) {
if constexpr (write)
policies_[0]->WritingCompleted(pointers);
else
policies_[0]->ReadingCompleted(pointers);
return;
}
auto offsets_ptr = offsets.data_ptr<int64_t>();
namespace gb = graphbolt;
gb::parallel_for_each(0, policies_.size(), 1, [&](int64_t tid) {
const auto begin = offsets_ptr[tid];
const auto end = offsets_ptr[tid + 1];
if constexpr (write)
policies_.at(tid)->WritingCompleted(pointers.slice(0, begin, end));
else
policies_.at(tid)->ReadingCompleted(pointers.slice(0, begin, end));
});
}
void PartitionedCachePolicy::ReadingCompleted(
torch::Tensor pointers, torch::Tensor offsets) {
ReadingWritingCompletedImpl<false>(pointers, offsets);
}
void PartitionedCachePolicy::WritingCompleted(
torch::Tensor pointers, torch::Tensor offsets) {
ReadingWritingCompletedImpl<true>(pointers, offsets);
}
c10::intrusive_ptr<Future<void>> PartitionedCachePolicy::ReadingCompletedAsync(
torch::Tensor pointers, torch::Tensor offsets) {
return async([=] { return ReadingCompleted(pointers, offsets); });
}
c10::intrusive_ptr<Future<void>> PartitionedCachePolicy::WritingCompletedAsync(
torch::Tensor pointers, torch::Tensor offsets) {
return async([=] { return WritingCompleted(pointers, offsets); });
}
template <typename CachePolicy>
c10::intrusive_ptr<PartitionedCachePolicy> PartitionedCachePolicy::Create(
int64_t capacity, int64_t num_partitions) {
static_assert(std::is_base_of_v<BaseCachePolicy, CachePolicy>);
return c10::make_intrusive<PartitionedCachePolicy>(
CachePolicy(), capacity, num_partitions);
}
template c10::intrusive_ptr<PartitionedCachePolicy>
PartitionedCachePolicy::Create<S3FifoCachePolicy>(int64_t, int64_t);
template c10::intrusive_ptr<PartitionedCachePolicy>
PartitionedCachePolicy::Create<SieveCachePolicy>(int64_t, int64_t);
template c10::intrusive_ptr<PartitionedCachePolicy>
PartitionedCachePolicy::Create<LruCachePolicy>(int64_t, int64_t);
template c10::intrusive_ptr<PartitionedCachePolicy>
PartitionedCachePolicy::Create<ClockCachePolicy>(int64_t, int64_t);
} // namespace storage
} // namespace graphbolt
+181
View File
@@ -0,0 +1,181 @@
/**
* Copyright (c) 2023, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file partitioned_cache_policy.h
* @brief Partitioned cache policy implementation on the CPU.
*/
#ifndef GRAPHBOLT_PARTITIONED_CACHE_H_
#define GRAPHBOLT_PARTITIONED_CACHE_H_
#include <graphbolt/async.h>
#include <torch/custom_class.h>
#include <torch/torch.h>
#include <mutex>
#include <pcg_random.hpp>
#include <random>
#include <type_traits>
#include <vector>
#include "./cache_policy.h"
namespace graphbolt {
namespace storage {
/**
* @brief PartitionedCachePolicy works by partitioning the key space to a set
* number of partitions that is provided as the second argument of its
* constructor. Since the partitioning is random but deterministic, the caching
* policy performance is not affected as the key distribution stays the same in
* each partition.
**/
class PartitionedCachePolicy : public torch::CustomClassHolder {
public:
/**
* @brief The policy query function.
* @param capacity The capacity of the cache.
* @param num_partitions The number of caching policies instantiated in a
* one-to-one mapping to each partition.
*/
template <typename CachePolicy>
PartitionedCachePolicy(CachePolicy, int64_t capacity, int64_t num_partitions);
/**
* @brief The policy query function.
* @param keys The keys to query the cache.
* @param offset The offset to be added to the keys.
*
* @return (positions, indices, missing_keys, found_ptrs, found_offsets,
* missing_offsets), where positions has the locations of the keys which were
* found in the cache, missing_keys has the keys that were not found and
* indices is defined such that keys[indices[:positions.size(0)]] gives us the
* keys for the found pointers and keys[indices[positions.size(0):]] is
* identical to missing_keys. The found_offsets tensor holds the partition
* offsets for the found pointers. The missing_offsets holds the partition
* offsets for the missing_keys.
*/
std::tuple<
torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor,
torch::Tensor>
Query(torch::Tensor keys, int64_t offset);
c10::intrusive_ptr<Future<std::vector<torch::Tensor>>> QueryAsync(
torch::Tensor keys, int64_t offset);
/**
* @brief The policy query and then replace function.
* @param keys The keys to query the cache.
* @param offset The offset to be added to the keys.
*
* @return (positions, indices, pointers, missing_keys, found_offsets,
* missing_offsets), where positions has the locations of the keys which were
* emplaced into the cache, pointers point to the emplaced CacheKey pointers
* in the cache, missing_keys has the keys that were not found and just
* inserted and indices is defined such that keys[indices[:keys.size(0) -
* missing_keys.size(0)]] gives us the keys for the found keys and
* keys[indices[keys.size(0) - missing_keys.size(0):]] is identical to
* missing_keys. The found_offsets tensor holds the partition offsets for the
* found pointers. The missing_offsets holds the partition offsets for the
* missing_keys and missing pointers.
*/
std::tuple<
torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor,
torch::Tensor>
QueryAndReplace(torch::Tensor keys, int64_t offset);
c10::intrusive_ptr<Future<std::vector<torch::Tensor>>> QueryAndReplaceAsync(
torch::Tensor keys, int64_t offset);
/**
* @brief The policy replace function.
* @param keys The keys to query the cache.
* @param offsets The partition offsets for the keys.
* @param offset The offset to be added to the keys.
*
* @return (positions, pointers, offsets), where positions holds the locations
* of the replaced entries in the cache, pointers holds the CacheKey pointers
* for the inserted keys and offsets holds the partition offsets for pointers.
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor> Replace(
torch::Tensor keys, torch::optional<torch::Tensor> offsets,
int64_t offset);
c10::intrusive_ptr<Future<std::vector<torch::Tensor>>> ReplaceAsync(
torch::Tensor keys, torch::optional<torch::Tensor> offsets,
int64_t offset);
template <bool write>
void ReadingWritingCompletedImpl(
torch::Tensor pointers, torch::Tensor offsets);
/**
* @brief A reader has finished reading these keys, so they can be
* evicted.
* @param pointers The CacheKey pointers in the cache to unmark.
* @param offsets The partition offsets for the pointers.
*/
void ReadingCompleted(torch::Tensor pointers, torch::Tensor offsets);
/**
* @brief A writer has finished writing these keys, so they can be evicted.
* @param pointers The CacheKey pointers in the cache to unmark.
* @param offsets The partition offsets for the pointers.
*/
void WritingCompleted(torch::Tensor pointers, torch::Tensor offsets);
c10::intrusive_ptr<Future<void>> ReadingCompletedAsync(
torch::Tensor pointers, torch::Tensor offsets);
c10::intrusive_ptr<Future<void>> WritingCompletedAsync(
torch::Tensor pointers, torch::Tensor offsets);
template <typename CachePolicy>
static c10::intrusive_ptr<PartitionedCachePolicy> Create(
int64_t capacity, int64_t num_partitions);
private:
static constexpr uint64_t seed = 1e9 + 7;
/**
* @brief Deterministic assignment of keys to different parts.
*/
int32_t PartAssignment(int64_t key) {
pcg32 rng(seed, key);
std::uniform_int_distribution<int32_t> dist(0, policies_.size() - 1);
return dist(rng);
}
/**
* @brief The partition function for a given keys tensor.
* @param keys The keys to query the cache.
*
* @return (offsets, indices, permuted_keys), the returned tensors have the
* following properties:
* permuted_keys[offsets[i]: offsets[i + 1]] belong to part i and
* keys[indices] == permuted_keys
*/
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor> Partition(
torch::Tensor keys);
int64_t capacity_;
std::vector<std::unique_ptr<BaseCachePolicy>> policies_;
std::mutex mtx_;
};
} // namespace storage
} // namespace graphbolt
#endif // GRAPHBOLT_PARTITIONED_CACHE_H_
+233
View File
@@ -0,0 +1,233 @@
/**
* Copyright (c) 2023 by Contributors
* @file python_binding.cc
* @brief Graph bolt library Python binding.
*/
#include <graphbolt/fused_csc_sampling_graph.h>
#include <graphbolt/isin.h>
#include <graphbolt/serialize.h>
#include <graphbolt/unique_and_compact.h>
#ifdef GRAPHBOLT_USE_CUDA
#include "./cuda/cooperative_minibatching_utils.h"
#include "./cuda/max_uva_threads.h"
#endif
#include "./cnumpy.h"
#include "./feature_cache.h"
#include "./index_select.h"
#include "./io_uring.h"
#include "./partitioned_cache_policy.h"
#include "./random.h"
#include "./utils.h"
#ifdef GRAPHBOLT_USE_CUDA
#include "./cuda/extension/gpu_cache.h"
#include "./cuda/extension/gpu_graph_cache.h"
#endif
namespace graphbolt {
namespace sampling {
TORCH_LIBRARY(graphbolt, m) {
m.class_<FusedSampledSubgraph>("FusedSampledSubgraph")
.def(torch::init<>())
.def_readwrite("indptr", &FusedSampledSubgraph::indptr)
.def_readwrite("indices", &FusedSampledSubgraph::indices)
.def_readwrite(
"original_row_node_ids", &FusedSampledSubgraph::original_row_node_ids)
.def_readwrite(
"original_column_node_ids",
&FusedSampledSubgraph::original_column_node_ids)
.def_readwrite(
"original_edge_ids", &FusedSampledSubgraph::original_edge_ids)
.def_readwrite("type_per_edge", &FusedSampledSubgraph::type_per_edge)
.def_readwrite("etype_offsets", &FusedSampledSubgraph::etype_offsets);
m.class_<Future<void>>("VoidFuture").def("wait", &Future<void>::Wait);
m.class_<Future<torch::Tensor>>("TensorFuture")
.def("wait", &Future<torch::Tensor>::Wait);
m.class_<Future<std::vector<torch::Tensor>>>("TensorListFuture")
.def("wait", &Future<std::vector<torch::Tensor>>::Wait);
m.class_<Future<c10::intrusive_ptr<FusedSampledSubgraph>>>(
"FusedSampledSubgraphFuture")
.def("wait", &Future<c10::intrusive_ptr<FusedSampledSubgraph>>::Wait);
m.class_<Future<std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>>>(
"UniqueAndCompactBatchedFuture")
.def(
"wait",
&Future<std::vector<std::tuple<
torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>>::
Wait);
m.class_<Future<
std::vector<std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>>>>(
"RankSortFuture")
.def(
"wait",
&Future<std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>>>::Wait);
m.class_<Future<std::tuple<torch::Tensor, torch::Tensor, int64_t, int64_t>>>(
"GpuGraphCacheQueryFuture")
.def(
"wait",
&Future<std::tuple<torch::Tensor, torch::Tensor, int64_t, int64_t>>::
Wait);
m.class_<Future<std::tuple<torch::Tensor, std::vector<torch::Tensor>>>>(
"GpuGraphCacheReplaceFuture")
.def(
"wait",
&Future<std::tuple<torch::Tensor, std::vector<torch::Tensor>>>::Wait);
m.class_<storage::OnDiskNpyArray>("OnDiskNpyArray")
.def("index_select", &storage::OnDiskNpyArray::IndexSelect);
m.class_<FusedCSCSamplingGraph>("FusedCSCSamplingGraph")
.def("num_nodes", &FusedCSCSamplingGraph::NumNodes)
.def("num_edges", &FusedCSCSamplingGraph::NumEdges)
.def("csc_indptr", &FusedCSCSamplingGraph::CSCIndptr)
.def("indices", &FusedCSCSamplingGraph::Indices)
.def("node_type_offset", &FusedCSCSamplingGraph::NodeTypeOffset)
.def("type_per_edge", &FusedCSCSamplingGraph::TypePerEdge)
.def("node_type_to_id", &FusedCSCSamplingGraph::NodeTypeToID)
.def("edge_type_to_id", &FusedCSCSamplingGraph::EdgeTypeToID)
.def("node_attributes", &FusedCSCSamplingGraph::NodeAttributes)
.def("edge_attributes", &FusedCSCSamplingGraph::EdgeAttributes)
.def("node_attribute", &FusedCSCSamplingGraph::NodeAttribute)
.def("edge_attribute", &FusedCSCSamplingGraph::EdgeAttribute)
.def("set_csc_indptr", &FusedCSCSamplingGraph::SetCSCIndptr)
.def("set_indices", &FusedCSCSamplingGraph::SetIndices)
.def("set_node_type_offset", &FusedCSCSamplingGraph::SetNodeTypeOffset)
.def("set_type_per_edge", &FusedCSCSamplingGraph::SetTypePerEdge)
.def("set_node_type_to_id", &FusedCSCSamplingGraph::SetNodeTypeToID)
.def("set_edge_type_to_id", &FusedCSCSamplingGraph::SetEdgeTypeToID)
.def("set_node_attributes", &FusedCSCSamplingGraph::SetNodeAttributes)
.def("set_edge_attributes", &FusedCSCSamplingGraph::SetEdgeAttributes)
.def("add_node_attribute", &FusedCSCSamplingGraph::AddNodeAttribute)
.def("add_edge_attribute", &FusedCSCSamplingGraph::AddEdgeAttribute)
.def("in_subgraph", &FusedCSCSamplingGraph::InSubgraph)
.def("sample_neighbors", &FusedCSCSamplingGraph::SampleNeighbors)
.def(
"sample_neighbors_async",
&FusedCSCSamplingGraph::SampleNeighborsAsync)
.def(
"temporal_sample_neighbors",
&FusedCSCSamplingGraph::TemporalSampleNeighbors)
.def("copy_to_shared_memory", &FusedCSCSamplingGraph::CopyToSharedMemory)
.def_pickle(
// __getstate__
[](const c10::intrusive_ptr<FusedCSCSamplingGraph>& self)
-> torch::Dict<
std::string, torch::Dict<std::string, torch::Tensor>> {
return self->GetState();
},
// __setstate__
[](torch::Dict<std::string, torch::Dict<std::string, torch::Tensor>>
state) -> c10::intrusive_ptr<FusedCSCSamplingGraph> {
auto g = c10::make_intrusive<FusedCSCSamplingGraph>();
g->SetState(state);
return g;
});
#ifdef GRAPHBOLT_USE_CUDA
m.class_<cuda::GpuCache>("GpuCache")
.def("query", &cuda::GpuCache::Query)
.def("query_async", &cuda::GpuCache::QueryAsync)
.def("replace", &cuda::GpuCache::Replace);
m.def("gpu_cache", &cuda::GpuCache::Create);
m.class_<cuda::GpuGraphCache>("GpuGraphCache")
.def("query", &cuda::GpuGraphCache::Query)
.def("query_async", &cuda::GpuGraphCache::QueryAsync)
.def("replace", &cuda::GpuGraphCache::Replace)
.def("replace_async", &cuda::GpuGraphCache::ReplaceAsync);
m.def("gpu_graph_cache", &cuda::GpuGraphCache::Create);
#endif
m.def("fused_csc_sampling_graph", &FusedCSCSamplingGraph::Create);
m.class_<storage::PartitionedCachePolicy>("PartitionedCachePolicy")
.def("query", &storage::PartitionedCachePolicy::Query)
.def("query_async", &storage::PartitionedCachePolicy::QueryAsync)
.def(
"query_and_replace",
&storage::PartitionedCachePolicy::QueryAndReplace)
.def(
"query_and_replace_async",
&storage::PartitionedCachePolicy::QueryAndReplaceAsync)
.def("replace", &storage::PartitionedCachePolicy::Replace)
.def("replace_async", &storage::PartitionedCachePolicy::ReplaceAsync)
.def(
"reading_completed",
&storage::PartitionedCachePolicy::ReadingCompleted)
.def(
"reading_completed_async",
&storage::PartitionedCachePolicy::ReadingCompletedAsync)
.def(
"writing_completed",
&storage::PartitionedCachePolicy::WritingCompleted)
.def(
"writing_completed_async",
&storage::PartitionedCachePolicy::WritingCompletedAsync);
m.def(
"s3_fifo_cache_policy",
&storage::PartitionedCachePolicy::Create<storage::S3FifoCachePolicy>);
m.def(
"sieve_cache_policy",
&storage::PartitionedCachePolicy::Create<storage::SieveCachePolicy>);
m.def(
"lru_cache_policy",
&storage::PartitionedCachePolicy::Create<storage::LruCachePolicy>);
m.def(
"clock_cache_policy",
&storage::PartitionedCachePolicy::Create<storage::ClockCachePolicy>);
m.class_<storage::FeatureCache>("FeatureCache")
.def("is_pinned", &storage::FeatureCache::IsPinned)
.def_property("nbytes", &storage::FeatureCache::NumBytes)
.def("index_select", &storage::FeatureCache::IndexSelect)
.def("query", &storage::FeatureCache::Query)
.def("query_async", &storage::FeatureCache::QueryAsync)
.def("replace", &storage::FeatureCache::Replace)
.def("replace_async", &storage::FeatureCache::ReplaceAsync);
m.def("feature_cache", &storage::FeatureCache::Create);
m.def(
"load_from_shared_memory", &FusedCSCSamplingGraph::LoadFromSharedMemory);
m.def("unique_and_compact", &UniqueAndCompact);
m.def("unique_and_compact_batched", &UniqueAndCompactBatched);
m.def("unique_and_compact_batched_async", &UniqueAndCompactBatchedAsync);
m.def("isin", &IsIn);
m.def("is_not_in_index", &IsNotInIndex);
m.def("is_not_in_index_async", &IsNotInIndexAsync);
m.def("index_select", &ops::IndexSelect);
m.def("index_select_async", &ops::IndexSelectAsync);
m.def("scatter_async", &ops::ScatterAsync);
m.def("index_select_csc", &ops::IndexSelectCSC);
m.def("index_select_csc_batched", &ops::IndexSelectCSCBatched);
m.def("index_select_csc_batched_async", &ops::IndexSelectCSCBatchedAsync);
m.def("ondisk_npy_array", &storage::OnDiskNpyArray::Create);
m.def("detect_io_uring", &io_uring::IsAvailable);
m.def("set_num_io_uring_threads", &io_uring::SetNumThreads);
m.def("set_worker_id", &utils::SetWorkerId);
m.def("set_seed", &RandomEngine::SetManualSeed);
#ifdef GRAPHBOLT_USE_CUDA
m.def("set_max_uva_threads", &cuda::set_max_uva_threads);
m.def("rank_sort", &cuda::RankSort);
m.def("rank_sort_async", &cuda::RankSortAsync);
#endif
#ifdef HAS_IMPL_ABSTRACT_PYSTUB
m.impl_abstract_pystub("dgl.graphbolt.base", "//dgl.graphbolt.base");
#endif
m.def(
"expand_indptr(Tensor indptr, ScalarType dtype, Tensor? node_ids, "
"SymInt? output_size) -> Tensor"
#ifdef HAS_PT2_COMPLIANT_TAG
,
{at::Tag::pt2_compliant_tag}
#endif
);
m.def(
"indptr_edge_ids(Tensor indptr, ScalarType dtype, Tensor? offset, "
"SymInt? output_size) -> "
"Tensor"
#ifdef HAS_PT2_COMPLIANT_TAG
,
{at::Tag::pt2_compliant_tag}
#endif
);
}
} // namespace sampling
} // namespace graphbolt
+72
View File
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2023 by Contributors
* @file random.cc
* @brief Random Engine.
*/
#include "./random.h"
#include <torch/torch.h>
namespace graphbolt {
namespace {
// Get a unique integer ID representing this thread.
inline uint32_t GetThreadId() {
static int num_threads = 0;
static std::mutex mutex;
static thread_local int id = -1;
if (id == -1) {
std::lock_guard<std::mutex> guard(mutex);
id = num_threads;
num_threads++;
}
return id;
}
}; // namespace
std::mutex RandomEngine::manual_seed_mutex;
std::optional<uint64_t> RandomEngine::manual_seed;
/** @brief Constructor with default seed. */
RandomEngine::RandomEngine() {
std::random_device rd;
std::lock_guard lock(manual_seed_mutex);
if (!manual_seed.has_value()) manual_seed = rd();
SetSeed(manual_seed.value());
}
/** @brief Constructor with given seed. */
RandomEngine::RandomEngine(uint64_t seed) : RandomEngine(seed, GetThreadId()) {}
/** @brief Constructor with given seed. */
RandomEngine::RandomEngine(uint64_t seed, uint64_t stream) {
SetSeed(seed, stream);
}
/** @brief Get the thread-local random number generator instance. */
RandomEngine* RandomEngine::ThreadLocal() {
static thread_local RandomEngine engine;
return &engine;
}
/** @brief Set the seed. */
void RandomEngine::SetSeed(uint64_t seed) { SetSeed(seed, GetThreadId()); }
/** @brief Set the seed. */
void RandomEngine::SetSeed(uint64_t seed, uint64_t stream) {
rng_.seed(seed, stream);
}
/** @brief Manually fix the seed. */
void RandomEngine::SetManualSeed(int64_t seed) {
// Intentionally set the seed for current thread also.
RandomEngine::ThreadLocal()->SetSeed(seed);
std::lock_guard lock(manual_seed_mutex);
manual_seed = seed;
}
} // namespace graphbolt
+79
View File
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2023 by Contributors
*
* @file random.h
* @brief Random Engine class.
*/
#ifndef GRAPHBOLT_RANDOM_H_
#define GRAPHBOLT_RANDOM_H_
#include <mutex>
#include <optional>
#include <pcg_random.hpp>
#include <random>
#include <thread>
namespace graphbolt {
/**
* @brief Thread-local Random Number Generator class.
*/
class RandomEngine {
public:
/** @brief Constructor with default seed. */
RandomEngine();
/** @brief Constructor with given seed. */
explicit RandomEngine(uint64_t seed);
explicit RandomEngine(uint64_t seed, uint64_t stream);
/** @brief Get the thread-local random number generator instance. */
static RandomEngine* ThreadLocal();
/** @brief Set the seed. */
void SetSeed(uint64_t seed);
void SetSeed(uint64_t seed, uint64_t stream);
/** @brief Protect manual seed accesses. */
static std::mutex manual_seed_mutex;
/** @brief Manually fix the seed. */
static std::optional<uint64_t> manual_seed;
static void SetManualSeed(int64_t seed);
/**
* @brief Generate a uniform random integer in [low, high).
*/
template <typename T>
T RandInt(T lower, T upper) {
std::uniform_int_distribution<T> dist(lower, upper - 1);
return dist(rng_);
}
/**
* @brief Generate a uniform random real number in [low, high).
*/
template <typename T>
T Uniform(T lower, T upper) {
std::uniform_real_distribution<T> dist(lower, upper);
return dist(rng_);
}
/**
* @brief Generate random non-negative floating-point values according to
* exponential distribution. Probability density function: P(x|λ) = λe^(-λx).
*/
template <typename T>
T Exponential(T lambda) {
std::exponential_distribution<T> dist(lambda);
return dist(rng_);
}
private:
pcg32 rng_;
};
} // namespace graphbolt
#endif // GRAPHBOLT_RANDOM_H_
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2023 by Contributors
* @file graphbolt/src/serialize.cc
* @brief Source file of serialize.
*/
#include <graphbolt/serialize.h>
#include <torch/torch.h>
namespace torch {
serialize::InputArchive& operator>>(
serialize::InputArchive& archive,
graphbolt::sampling::FusedCSCSamplingGraph& graph) {
graph.Load(archive);
return archive;
}
serialize::OutputArchive& operator<<(
serialize::OutputArchive& archive,
const graphbolt::sampling::FusedCSCSamplingGraph& graph) {
graph.Save(archive);
return archive;
}
} // namespace torch
+167
View File
@@ -0,0 +1,167 @@
/**
* Copyright (c) 2023 by Contributors
* @file shared_memory.cc
* @brief Source file of graphbolt shared memory.
*/
#ifndef _WIN32
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#endif // !_WIN32
#include <graphbolt/shared_memory.h>
#include <stdio.h>
#include <string.h>
#include <torch/torch.h>
namespace graphbolt {
namespace sampling {
// Two processes opening the same path are guaranteed to access the same shared
// memory object if and only if path begins with a slash ('/') character.
constexpr char kSharedMemNamePrefix[] = "/dgl.graphbolt.";
constexpr char kSharedMemNameSuffix[] = ".lock";
// A prefix and a suffix are added to the name of the shared memory to create
// the name of the shared memory object.
inline std::string DecorateName(const std::string& name) {
return kSharedMemNamePrefix + name + kSharedMemNameSuffix;
}
SharedMemory::SharedMemory(const std::string& name)
: name_(name), size_(0), ptr_(nullptr) {
#ifdef _WIN32
this->handle_ = nullptr;
#else // _WIN32
this->file_descriptor_ = -1;
this->is_creator_ = false;
#endif // _WIN32
}
#ifdef _WIN32
SharedMemory::~SharedMemory() {
if (ptr_) CHECK(UnmapViewOfFile(ptr_)) << "Win32 Error: " << GetLastError();
if (handle_) CloseHandle(handle_);
}
void* SharedMemory::Create(size_t size) {
size_ = size;
std::string decorated_name = DecorateName(name_);
handle_ = CreateFileMapping(
INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
static_cast<DWORD>(size >> 32), static_cast<DWORD>(size & 0xFFFFFFFF),
decorated_name.c_str());
TORCH_CHECK(
handle_ != nullptr, "Failed to open ", decorated_name,
", Win32 error: ", GetLastError());
ptr_ = MapViewOfFile(handle_, FILE_MAP_ALL_ACCESS, 0, 0, size);
TORCH_CHECK(
ptr_ != nullptr, "Memory mapping failed, Win32 error: ", GetLastError());
return ptr_;
}
void* SharedMemory::Open() {
std::string decorated_name = DecorateName(name_);
handle_ = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, decorated_name.c_str());
TORCH_CHECK(
handle_ != nullptr, "Failed to open ", decorated_name,
", Win32 Error: ", GetLastError());
ptr_ = MapViewOfFile(handle_, FILE_MAP_ALL_ACCESS, 0, 0, 0);
TORCH_CHECK(
ptr_ != nullptr, "Memory mapping failed, Win32 error: ", GetLastError());
// Obtain the size of the memory-mapped file.
MEMORY_BASIC_INFORMATION memInfo;
TORCH_CHECK(
VirtualQuery(ptr_, &memInfo, sizeof(memInfo)) != 0,
"Failed to get the size of shared memory: ", GetLastError());
size_ = static_cast<size_t>(memInfo.RegionSize);
return ptr_;
}
bool SharedMemory::Exists(const std::string& name) {
std::string decorated_name = DecorateName(name);
HANDLE handle =
OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, decorated_name.c_str());
bool exists = handle != nullptr;
if (exists) {
CloseHandle(handle);
}
return exists;
}
#else // _WIN32
SharedMemory::~SharedMemory() {
if (ptr_ && size_ != 0) CHECK(munmap(ptr_, size_) != -1) << strerror(errno);
if (file_descriptor_ != -1) close(file_descriptor_);
std::string decorated_name = DecorateName(name_);
if (is_creator_ && decorated_name != "") shm_unlink(decorated_name.c_str());
}
void *SharedMemory::Create(size_t size) {
size_ = size;
is_creator_ = true;
// TODO(zhenkun): handle the error properly if the shared memory object
// already exists.
std::string decorated_name = DecorateName(name_);
file_descriptor_ =
shm_open(decorated_name.c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
TORCH_CHECK(file_descriptor_ != -1, "Failed to open: ", strerror(errno));
auto status = ftruncate(file_descriptor_, size);
TORCH_CHECK(status != -1, "Failed to truncate the file: ", strerror(errno));
ptr_ =
mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, file_descriptor_, 0);
TORCH_CHECK(
ptr_ != MAP_FAILED,
"Failed to map shared memory, mmap failed with error: ", strerror(errno));
return ptr_;
}
void *SharedMemory::Open() {
std::string decorated_name = DecorateName(name_);
file_descriptor_ =
shm_open(decorated_name.c_str(), O_RDWR, S_IRUSR | S_IWUSR);
TORCH_CHECK(
file_descriptor_ != -1, "Failed to open ", decorated_name, ": ",
strerror(errno));
struct stat shm_stat;
TORCH_CHECK(
fstat(file_descriptor_, &shm_stat) == 0,
"Failed to get the size of shared memory: ", strerror(errno));
size_ = shm_stat.st_size;
ptr_ = mmap(
NULL, size_, PROT_READ | PROT_WRITE, MAP_SHARED, file_descriptor_, 0);
TORCH_CHECK(
ptr_ != MAP_FAILED,
"Failed to map shared memory, mmap failed with error: ", strerror(errno));
return ptr_;
}
bool SharedMemory::Exists(const std::string &name) {
std::string decorated_name = DecorateName(name);
int file_descriptor =
shm_open(decorated_name.c_str(), O_RDONLY, S_IRUSR | S_IWUSR);
bool exists = file_descriptor > 0;
if (exists) {
close(file_descriptor);
}
return exists;
}
#endif // _WIN32
} // namespace sampling
} // namespace graphbolt
+223
View File
@@ -0,0 +1,223 @@
/**
* Copyright (c) 2023 by Contributors
*
* @file shared_memory_helper.cc
* @brief Share memory helper implementation.
*/
#include "./shared_memory_helper.h"
#include <graphbolt/serialize.h>
#include <graphbolt/shared_memory.h>
#include <torch/torch.h>
#include <cstring>
#include <string>
#include <tuple>
#include <vector>
namespace graphbolt {
namespace sampling {
static std::string GetSharedMemoryMetadataName(const std::string& name) {
return name + "_metadata";
}
static std::string GetSharedMemoryDataName(const std::string& name) {
return name + "_data";
}
// To avoid unaligned memory access, we round the size of the binary buffer to
// the nearest multiple of 8 bytes.
inline static int64_t GetRoundedSize(int64_t size) {
constexpr int64_t ALIGNED_SIZE = 8;
return (size + ALIGNED_SIZE - 1) / ALIGNED_SIZE * ALIGNED_SIZE;
}
SharedMemoryHelper::SharedMemoryHelper(const std::string& name)
: name_(name),
metadata_size_(0),
data_size_(0),
metadata_shared_memory_(nullptr),
data_shared_memory_(nullptr),
metadata_offset_(0),
data_offset_(0) {}
void SharedMemoryHelper::InitializeRead() {
metadata_offset_ = 0;
data_offset_ = 0;
if (metadata_shared_memory_ == nullptr) {
// Reader process opens the shared memory.
metadata_shared_memory_ =
std::make_unique<SharedMemory>(GetSharedMemoryMetadataName(name_));
metadata_shared_memory_->Open();
metadata_size_ = metadata_shared_memory_->GetSize();
data_shared_memory_ =
std::make_unique<SharedMemory>(GetSharedMemoryDataName(name_));
data_shared_memory_->Open();
data_size_ = data_shared_memory_->GetSize();
}
}
void SharedMemoryHelper::WriteTorchArchive(
torch::serialize::OutputArchive&& archive) {
metadata_to_write_.emplace_back(std::move(archive));
}
torch::serialize::InputArchive SharedMemoryHelper::ReadTorchArchive() {
auto metadata_ptr = this->GetCurrentMetadataPtr();
int64_t metadata_size = static_cast<int64_t*>(metadata_ptr)[0];
torch::serialize::InputArchive archive;
archive.load_from(
static_cast<const char*>(metadata_ptr) + sizeof(int64_t), metadata_size);
auto rounded_size = GetRoundedSize(metadata_size);
this->MoveMetadataPtr(sizeof(int64_t) + rounded_size);
return archive;
}
void SharedMemoryHelper::WriteTorchTensor(
torch::optional<torch::Tensor> tensor) {
torch::serialize::OutputArchive archive;
archive.write("has_value", tensor.has_value());
if (tensor.has_value()) {
archive.write("shape", tensor.value().sizes());
archive.write("dtype", tensor.value().scalar_type());
}
this->WriteTorchArchive(std::move(archive));
tensors_to_write_.push_back(tensor);
}
torch::optional<torch::Tensor> SharedMemoryHelper::ReadTorchTensor() {
auto archive = this->ReadTorchArchive();
bool has_value = read_from_archive<bool>(archive, "has_value");
if (has_value) {
auto shape = read_from_archive<std::vector<int64_t>>(archive, "shape");
auto dtype = read_from_archive<torch::ScalarType>(archive, "dtype");
auto data_ptr = this->GetCurrentDataPtr();
auto tensor = torch::from_blob(data_ptr, shape, dtype);
auto rounded_size = GetRoundedSize(tensor.numel() * tensor.element_size());
this->MoveDataPtr(rounded_size);
return tensor;
} else {
return torch::nullopt;
}
}
void SharedMemoryHelper::WriteTorchTensorDict(
torch::optional<torch::Dict<std::string, torch::Tensor>> tensor_dict) {
torch::serialize::OutputArchive archive;
if (!tensor_dict.has_value()) {
archive.write("has_value", false);
this->WriteTorchArchive(std::move(archive));
return;
}
archive.write("has_value", true);
auto dict_value = tensor_dict.value();
archive.write("num_tensors", static_cast<int64_t>(dict_value.size()));
int counter = 0;
for (auto it = dict_value.begin(); it != dict_value.end(); ++it) {
archive.write(std::string("key_") + std::to_string(counter), it->key());
counter++;
}
this->WriteTorchArchive(std::move(archive));
for (auto it = dict_value.begin(); it != dict_value.end(); ++it) {
this->WriteTorchTensor(it->value());
}
}
torch::optional<torch::Dict<std::string, torch::Tensor>>
SharedMemoryHelper::ReadTorchTensorDict() {
auto archive = this->ReadTorchArchive();
if (!read_from_archive<bool>(archive, "has_value")) {
return torch::nullopt;
}
int64_t num_tensors = read_from_archive<int64_t>(archive, "num_tensors");
torch::Dict<std::string, torch::Tensor> tensor_dict;
for (int64_t i = 0; i < num_tensors; ++i) {
auto key = read_from_archive<std::string>(
archive, std::string("key_") + std::to_string(i));
auto tensor = this->ReadTorchTensor();
tensor_dict.insert(key, tensor.value());
}
return tensor_dict;
}
void SharedMemoryHelper::SerializeMetadata() {
for (auto& archive : metadata_to_write_) {
std::stringstream serialized;
archive.save_to(serialized);
metadata_strings_to_write_.push_back(std::move(serialized.str()));
}
metadata_to_write_.clear();
}
void SharedMemoryHelper::WriteMetadataToSharedMemory() {
metadata_offset_ = 0;
for (const auto& str : metadata_strings_to_write_) {
auto metadata_ptr = this->GetCurrentMetadataPtr();
static_cast<int64_t*>(metadata_ptr)[0] = str.size();
memcpy(
static_cast<char*>(metadata_ptr) + sizeof(int64_t), str.data(),
str.size());
int64_t rounded_size = GetRoundedSize(str.size());
this->MoveMetadataPtr(sizeof(int64_t) + rounded_size);
}
metadata_strings_to_write_.clear();
}
void SharedMemoryHelper::WriteTorchTensorInternal(
torch::optional<torch::Tensor> tensor) {
if (tensor.has_value()) {
size_t memory_size = tensor.value().numel() * tensor.value().element_size();
auto data_ptr = this->GetCurrentDataPtr();
auto contiguous_tensor = tensor.value().contiguous();
memcpy(data_ptr, contiguous_tensor.data_ptr(), memory_size);
this->MoveDataPtr(GetRoundedSize(memory_size));
}
}
void SharedMemoryHelper::Flush() {
size_t data_size = 0;
for (auto tensor : tensors_to_write_) {
if (tensor.has_value()) {
auto tensor_size = tensor.value().numel() * tensor.value().element_size();
data_size += GetRoundedSize(tensor_size);
}
}
// Serialize the metadata archives.
SerializeMetadata();
// Create the shared memory objects.
const size_t metadata_size = std::accumulate(
metadata_strings_to_write_.begin(), metadata_strings_to_write_.end(), 0,
[](size_t sum, const std::string& str) {
return sum + sizeof(int64_t) + GetRoundedSize(str.size());
});
metadata_shared_memory_ =
std::make_unique<SharedMemory>(GetSharedMemoryMetadataName(name_));
metadata_shared_memory_->Create(metadata_size);
metadata_size_ = metadata_size;
// Write the metadata and tensor data to the shared memory.
WriteMetadataToSharedMemory();
data_shared_memory_ =
std::make_unique<SharedMemory>(GetSharedMemoryDataName(name_));
data_shared_memory_->Create(data_size);
data_size_ = data_size;
data_offset_ = 0;
for (auto tensor : tensors_to_write_) {
this->WriteTorchTensorInternal(tensor);
}
metadata_to_write_.clear();
tensors_to_write_.clear();
}
std::pair<SharedMemoryPtr, SharedMemoryPtr>
SharedMemoryHelper::ReleaseSharedMemory() {
return std::make_pair(
std::move(metadata_shared_memory_), std::move(data_shared_memory_));
}
} // namespace sampling
} // namespace graphbolt
+153
View File
@@ -0,0 +1,153 @@
/**
* Copyright (c) 2023 by Contributors
*
* @file shared_memory_helper.h
* @brief Share memory helper.
*/
#ifndef GRAPHBOLT_SHARED_MEMORY_HELPER_H_
#define GRAPHBOLT_SHARED_MEMORY_HELPER_H_
#include <graphbolt/shared_memory.h>
#include <torch/torch.h>
#include <memory>
#include <sstream>
#include <string>
#include <tuple>
#include <vector>
namespace graphbolt {
namespace sampling {
/**
* @brief SharedMemoryHelper is a helper class to write/read data structures
* to/from shared memory.
*
* In order to write data structure to shared memory, we need to serialize the
* data structure to a binary buffer and then write the buffer to the shared
* memory. However, the size of the binary buffer is not known in advance. To
* solve this problem, we use two shared memory objects: one for storing the
* metadata and the other for storing the binary buffer. The metadata includes
* the metadata of data structures such as size and shape. The size of the
* metadata is decided by the size of metadata. The size of the binary buffer is
* decided by the size of the data structures.
*
* To avoid repeated shared memory allocation, this helper class uses lazy data
* structure writing. The data structures are written to the shared memory only
* when `Flush` is called. The data structures are written in the order of
* calling `WriteTorchArchive`, `WriteTorchTensor` and `WriteTorchTensorDict`,
* and also read in the same order.
*
* The usage of this class as a writer is as follows:
* @code{.cpp}
* SharedMemoryHelper shm_helper("shm_name", 1024, true);
* shm_helper.WriteTorchArchive(archive);
* shm_helper.WriteTorchTensor(tensor);
* shm_helper.WriteTorchTensorDict(tensor_dict);
* shm_helper.Flush();
* // After `Flush`, the data structures are written to the shared memory.
* // Then the helper class can be used as a reader.
* shm_helper.InitializeRead();
* auto archive = shm_helper.ReadTorchArchive();
* auto tensor = shm_helper.ReadTorchTensor();
* auto tensor_dict = shm_helper.ReadTorchTensorDict();
* @endcode
*
* The usage of this class as a reader is as follows:
* @code{.cpp}
* SharedMemoryHelper shm_helper("shm_name", 1024, false);
* shm_helper.InitializeRead();
* auto archive = shm_helper.ReadTorchArchive();
* auto tensor = shm_helper.ReadTorchTensor();
* auto tensor_dict = shm_helper.ReadTorchTensorDict();
* @endcode
*
*
*/
class SharedMemoryHelper {
public:
/**
* @brief Constructor of the shared memory helper.
* @param name The name of the shared memory.
*/
SharedMemoryHelper(const std::string& name);
/** @brief Initialize this helper class before reading. */
void InitializeRead();
void WriteTorchArchive(torch::serialize::OutputArchive&& archive);
torch::serialize::InputArchive ReadTorchArchive();
void WriteTorchTensor(torch::optional<torch::Tensor> tensor);
torch::optional<torch::Tensor> ReadTorchTensor();
void WriteTorchTensorDict(
torch::optional<torch::Dict<std::string, torch::Tensor>> tensor_dict);
torch::optional<torch::Dict<std::string, torch::Tensor>>
ReadTorchTensorDict();
/** @brief Flush the data structures to the shared memory. */
void Flush();
/** @brief Release the shared memory and return their left values. */
std::pair<SharedMemoryPtr, SharedMemoryPtr> ReleaseSharedMemory();
private:
/**
* @brief Serialize metadata to string.
*/
void SerializeMetadata();
/**
* @brief Write the metadata to the shared memory. This function is
* called by `Flush`.
*/
void WriteMetadataToSharedMemory();
/**
* @brief Write the tensor data to the shared memory. This function is
* called by `Flush`.
*/
void WriteTorchTensorInternal(torch::optional<torch::Tensor> tensor);
inline void* GetCurrentMetadataPtr() const {
return static_cast<char*>(metadata_shared_memory_->GetMemory()) +
metadata_offset_;
}
inline void* GetCurrentDataPtr() const {
return static_cast<char*>(data_shared_memory_->GetMemory()) + data_offset_;
}
inline void MoveMetadataPtr(int64_t offset) {
TORCH_CHECK(
metadata_offset_ + offset <= metadata_size_,
"The size of metadata exceeds the maximum size of shared memory.");
metadata_offset_ += offset;
}
inline void MoveDataPtr(int64_t offset) {
TORCH_CHECK(
data_offset_ + offset <= data_size_,
"The size of data exceeds the maximum size of shared memory.");
data_offset_ += offset;
}
std::string name_;
bool is_creator_;
size_t metadata_size_;
size_t data_size_;
// The shared memory objects for storing metadata and tensor data.
SharedMemoryPtr metadata_shared_memory_, data_shared_memory_;
// The read/write offsets of the metadata and tensor data.
size_t metadata_offset_, data_offset_;
// The data structures to write to the shared memory. They are written to the
// shared memory only when `Flush` is called.
std::vector<torch::serialize::OutputArchive> metadata_to_write_;
std::vector<std::string> metadata_strings_to_write_;
std::vector<torch::optional<torch::Tensor>> tensors_to_write_;
};
} // namespace sampling
} // namespace graphbolt
#endif // GRAPHBOLT_SHARED_MEMORY_HELPER_H_
+99
View File
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2023 by Contributors
*
* @file unique_and_compact.cc
* @brief Unique and compact op.
*/
#include <graphbolt/cuda_ops.h>
#include <graphbolt/unique_and_compact.h>
#include "./concurrent_id_hash_map.h"
#include "./macro.h"
#include "./utils.h"
namespace graphbolt {
namespace sampling {
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
UniqueAndCompact(
const torch::Tensor& src_ids, const torch::Tensor& dst_ids,
const torch::Tensor unique_dst_ids, const int64_t rank,
const int64_t world_size) {
if (utils::is_on_gpu(src_ids) && utils::is_on_gpu(dst_ids) &&
utils::is_on_gpu(unique_dst_ids)) {
GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(
c10::DeviceType::CUDA, "unique_and_compact", {
return ops::UniqueAndCompact(
src_ids, dst_ids, unique_dst_ids, rank, world_size);
});
}
TORCH_CHECK(
world_size <= 1,
"Cooperative Minibatching (arXiv:2310.12403) is supported only on GPUs.");
auto num_dst = unique_dst_ids.size(0);
torch::Tensor ids = torch::cat({unique_dst_ids, src_ids});
auto [unique_ids, compacted_src, compacted_dst] = AT_DISPATCH_INDEX_TYPES(
ids.scalar_type(), "unique_and_compact", ([&] {
ConcurrentIdHashMap<index_t> id_map(ids, num_dst);
return std::make_tuple(
id_map.GetUniqueIds(), id_map.MapIds(src_ids),
id_map.MapIds(dst_ids));
}));
auto offsets = torch::zeros(2, c10::TensorOptions().dtype(torch::kInt64));
offsets.data_ptr<int64_t>()[1] = unique_ids.size(0);
return {unique_ids, compacted_src, compacted_dst, offsets};
}
std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>
UniqueAndCompactBatched(
const std::vector<torch::Tensor>& src_ids,
const std::vector<torch::Tensor>& dst_ids,
const std::vector<torch::Tensor> unique_dst_ids, const int64_t rank,
const int64_t world_size) {
TORCH_CHECK(
src_ids.size() == dst_ids.size() &&
dst_ids.size() == unique_dst_ids.size(),
"The batch dimension of the parameters need to be identical.");
bool all_on_gpu = true;
for (std::size_t i = 0; i < src_ids.size(); i++) {
all_on_gpu = all_on_gpu && utils::is_on_gpu(src_ids[i]) &&
utils::is_on_gpu(dst_ids[i]) &&
utils::is_on_gpu(unique_dst_ids[i]);
if (!all_on_gpu) break;
}
if (all_on_gpu) {
GRAPHBOLT_DISPATCH_CUDA_ONLY_DEVICE(
c10::DeviceType::CUDA, "unique_and_compact", {
return ops::UniqueAndCompactBatched(
src_ids, dst_ids, unique_dst_ids, rank, world_size);
});
}
std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>
results;
results.reserve(src_ids.size());
for (std::size_t i = 0; i < src_ids.size(); i++) {
results.emplace_back(UniqueAndCompact(
src_ids[i], dst_ids[i], unique_dst_ids[i], rank, world_size));
}
return results;
}
c10::intrusive_ptr<Future<std::vector<
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>>>>
UniqueAndCompactBatchedAsync(
const std::vector<torch::Tensor>& src_ids,
const std::vector<torch::Tensor>& dst_ids,
const std::vector<torch::Tensor> unique_dst_ids, const int64_t rank,
const int64_t world_size) {
return async(
[=] {
return UniqueAndCompactBatched(
src_ids, dst_ids, unique_dst_ids, rank, world_size);
},
utils::is_on_gpu(src_ids.at(0)));
}
} // namespace sampling
} // namespace graphbolt
+36
View File
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2024, GT-TDAlab (Muhammed Fatih Balin & Umit V. Catalyurek)
* All rights reserved.
*
* 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.
*
* @file utils.cc
* @brief Graphbolt utils implementations.
*/
#include "./utils.h"
#include <optional>
namespace graphbolt {
namespace utils {
namespace {
std::optional<int64_t> worker_id;
}
std::optional<int64_t> GetWorkerId() { return worker_id; }
void SetWorkerId(int64_t worker_id_value) { worker_id = worker_id_value; }
} // namespace utils
} // namespace graphbolt
+103
View File
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2023 by Contributors
* @file utils.h
* @brief Graphbolt utils.
*/
#ifndef GRAPHBOLT_UTILS_H_
#define GRAPHBOLT_UTILS_H_
#include <torch/script.h>
#include <optional>
namespace graphbolt {
namespace utils {
/**
* @brief If this process is a worker part as part of a DataLoader, then returns
* the assigned worker id less than the # workers.
*/
std::optional<int64_t> GetWorkerId();
/**
* @brief If this process is a worker part as part of a DataLoader, then this
* function is called to initialize its worked id to be less than the # workers.
*/
void SetWorkerId(int64_t worker_id_value);
/**
* @brief Checks whether the tensor is stored on the GPU.
*/
inline bool is_on_gpu(const torch::Tensor& tensor) {
return tensor.device().is_cuda();
}
/**
* @brief Checks whether the tensor is stored on the GPU or the pinned memory.
*/
inline bool is_accessible_from_gpu(const torch::Tensor& tensor) {
return is_on_gpu(tensor) || tensor.is_pinned();
}
/**
* @brief Checks whether the tensor is stored on the pinned memory.
*/
inline bool is_pinned(const torch::Tensor& tensor) {
// If this process is a worker, we should avoid initializing the CUDA context.
return !GetWorkerId() && tensor.is_pinned();
}
/**
* @brief Checks whether the tensors are all stored on the GPU or the pinned
* memory.
*/
template <typename TensorContainer>
inline bool are_accessible_from_gpu(const TensorContainer& tensors) {
for (auto& tensor : tensors) {
if (!is_accessible_from_gpu(tensor)) return false;
}
return true;
}
/**
* @brief Parses the source and destination node type from a given edge type
* triple seperated with ":".
*/
inline std::pair<std::string, std::string> parse_src_dst_ntype_from_etype(
std::string etype) {
auto first_seperator_it = std::find(etype.begin(), etype.end(), ':');
auto second_seperator_pos =
std::find(first_seperator_it + 1, etype.end(), ':') - etype.begin();
return {
etype.substr(0, first_seperator_it - etype.begin()),
etype.substr(second_seperator_pos + 1)};
}
/**
* @brief Retrieves the value of the tensor at the given index.
*
* @note If the tensor is not contiguous, it will be copied to a contiguous
* tensor.
*
* @tparam T The type of the tensor.
* @param tensor The tensor.
* @param index The index.
*
* @return T The value of the tensor at the given index.
*/
template <typename T>
T GetValueByIndex(const torch::Tensor& tensor, int64_t index) {
TORCH_CHECK(
index >= 0 && index < tensor.numel(),
"The index should be within the range of the tensor, but got index ",
index, " and tensor size ", tensor.numel());
auto contiguous_tensor = tensor.contiguous();
auto data_ptr = contiguous_tensor.data_ptr<T>();
return data_ptr[index];
}
} // namespace utils
} // namespace graphbolt
#endif // GRAPHBOLT_UTILS_H_