Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 4 additions & 19 deletions pyg_lib/csrc/sampler/cuda/random_walk_kernel.cu
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
#include <ATen/ATen.h>
#include <ATen/cuda/CUDAContext.h>
#include <torch/library.h>

#include "pyg_lib/csrc/utils/cuda/helpers.h"

namespace pyg {
namespace sampler {

namespace {

int threads() {
const auto props = at::cuda::getCurrentDeviceProperties();
return std::min(props->maxThreadsPerBlock, 1024);
}

int blocks(int numel) {
const auto props = at::cuda::getCurrentDeviceProperties();
const auto blocks_per_sm = props->maxThreadsPerMultiProcessor / 256;
const auto max_blocks = props->multiProcessorCount * blocks_per_sm;
const auto max_threads = threads();
return std::min(max_blocks, (numel + max_threads - 1) / max_threads);
}

#define CUDA_1D_KERNEL_LOOP(scalar_t, i, n) \
for (scalar_t i = (blockIdx.x * blockDim.x) + threadIdx.x; i < (n); \
i += (blockDim.x * gridDim.x))

template <typename scalar_t>
__global__ void random_walk_kernel_impl(
const scalar_t* __restrict__ rowptr_data,
Expand Down Expand Up @@ -74,7 +58,8 @@ at::Tensor random_walk_kernel(const at::Tensor& rowptr,
const auto rand_data = rand.data_ptr<float>();
auto out_data = out.data_ptr<scalar_t>();

random_walk_kernel_impl<<<blocks(seed.size(0)), threads(), 0, stream>>>(
random_walk_kernel_impl<<<pyg::utils::blocks(seed.size(0)),
pyg::utils::threads(), 0, stream>>>(
rowptr_data, col_data, seed_data, rand_data, out_data, seed.size(0),
walk_length);
});
Expand Down
90 changes: 90 additions & 0 deletions pyg_lib/csrc/sampler/cuda/subgraph_kernel.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include <ATen/ATen.h>
#include <torch/library.h>

#include "pyg_lib/csrc/utils/cuda/helpers.h"

namespace pyg {
namespace sampler {

namespace {

#define FULL_MASK 0xffffffff

Comment thread
rusty1s marked this conversation as resolved.
Outdated
template <typename scalar_t>
__global__ void subgraph_deg_kernel_impl(
const scalar_t* __restrict__ rowptr_data,
const scalar_t* __restrict__ col_data,
const scalar_t* __restrict__ nodes_data,
const scalar_t* __restrict__ to_local_node_data,
scalar_t* __restrict__ out_data,
int64_t num_nodes) {
CUDA_1D_KERNEL_LOOP(scalar_t, thread_idx, 32 * num_nodes) {
scalar_t i = thread_idx >> 5; // thread_idx / 32
scalar_t lane = thread_idx & (32 - 1); // thread_idx % 32

auto v = nodes_data[i];

scalar_t deg = 0;
for (scalar_t j = rowptr_data[v] + lane; j < rowptr_data[v + 1]; j += 32) {
if (to_local_node_data[col_data[j]] >= 0) // contiguous access
deg++;
}

for (scalar_t offset = 16; offset > 0; offset /= 2) // warp-level reduction
deg += __shfl_down_sync(FULL_MASK, deg, offset);

if (lane == 0)
out_data[i] = deg;
}
}

std::tuple<at::Tensor, at::Tensor, c10::optional<at::Tensor>> subgraph_kernel(
const at::Tensor& rowptr,
const at::Tensor& col,
const at::Tensor& nodes,
const bool return_edge_id) {
TORCH_CHECK(rowptr.is_cuda(), "'rowptr' must be a CUDA tensor");
TORCH_CHECK(col.is_cuda(), "'col' must be a CUDA tensor");
TORCH_CHECK(nodes.is_cuda(), "'nodes' must be a CUDA tensor");

const auto stream = at::cuda::getCurrentCUDAStream();

// We maintain a O(N) vector to map global node indices to local ones.
// TODO Can we do this without O(N) storage requirement?
const auto to_local_node = nodes.new_full({rowptr.size(0) - 1}, -1);

@ZenoTan ZenoTan May 3, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does N means the number of nodes in the graph?
What if we could filter each node in nodes_data since it should be much smaller than rowptr_data.
Otherwise we may consider caching this tensor to reduce memory allocation for each time.

@rusty1s rusty1s May 4, 2022

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points! We use this vector as the mapping from global node indices to new local ones. In C++, we use a map for this but can't do the same in CUDA. I don't know of a more elegant solution for this.

Caching is an option as well, but requires a (non-intuitive and backend-specific) change in input arguments. I added it as a TODO for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's GPU hash table/set which may require some atomic operations when you build it, but lookup is fast.
I found that caching is not a good option since you have to reset the array every time.

Since you can sample on GPU, then the graph is not that big, a node array is not that bad and can make the code less complicated

const auto arange = at::arange(nodes.size(0), nodes.options());
to_local_node.index_copy_(/*dim=*/0, nodes, arange);

const auto deg = nodes.new_empty({nodes.size(0)});
const auto out_rowptr = rowptr.new_zeros({nodes.size(0) + 1});
at::Tensor out_col;
c10::optional<at::Tensor> out_edge_id = c10::nullopt;

AT_DISPATCH_INTEGRAL_TYPES(nodes.scalar_type(), "subgraph_kernel", [&] {
const auto rowptr_data = rowptr.data_ptr<scalar_t>();
const auto col_data = col.data_ptr<scalar_t>();
const auto nodes_data = nodes.data_ptr<scalar_t>();
const auto to_local_node_data = to_local_node.data_ptr<scalar_t>();
auto deg_data = deg.data_ptr<scalar_t>();

// Compute induced subgraph degree, parallelize with 32 threads per node:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually not sure if it is necessary to parallelize with 32 threads per nodes. Most of the time we are dealing with sparse data and a lot of threads will not go into for loop.

If you are looking for extreme performance, you can bundle to_local_node_data and col_data into one iterator structure and use this function. I haven't seen any better performance than it in the past.
https://nvlabs.github.io/cub/structcub_1_1_device_segmented_reduce.html#a4854a13561cb66d46aa617aab16b8825

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have an example of bundling to_local_node_data and col_data into one iterator structure? This looks really interesting.

I am okay with dropping the warp-level parallelism for now, but we will lose the contiguous access to col_data, and probably under-utilize the number of threads available on modern GPUs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second look, this doesn't seem possible since col_data refers to edges, while to_local_node_data refers to nodes, while we actually want do the compute across the number of nodes in the induced subgraph.

subgraph_deg_kernel_impl<<<pyg::utils::blocks(32 * nodes.size(0)),
pyg::utils::threads(), 0, stream>>>(
rowptr_data, col_data, nodes_data, to_local_node_data, deg_data,
nodes.size(0));

auto tmp = out_rowptr.narrow(0, 1, nodes.size(0));
at::cumsum_out(tmp, deg, /*dim=*/0);
});

return std::make_tuple(out_rowptr, deg, deg);
}

} // namespace

TORCH_LIBRARY_IMPL(pyg, CUDA, m) {
m.impl(TORCH_SELECTIVE_NAME("pyg::subgraph"), TORCH_FN(subgraph_kernel));
}

} // namespace sampler
} // namespace pyg
27 changes: 27 additions & 0 deletions pyg_lib/csrc/utils/cuda/helpers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include <ATen/ATen.h>
#include <ATen/cuda/CUDAContext.h>

namespace pyg {
namespace utils {

__host__ inline int threads() {
const auto props = at::cuda::getCurrentDeviceProperties();
return std::min(props->maxThreadsPerBlock, 1024);
}

__host__ inline int blocks(int numel) {
const auto props = at::cuda::getCurrentDeviceProperties();
const auto blocks_per_sm = props->maxThreadsPerMultiProcessor / 256;
const auto max_blocks = props->multiProcessorCount * blocks_per_sm;
const auto max_threads = threads();
return std::min(max_blocks, (numel + max_threads - 1) / max_threads);
}

#define CUDA_1D_KERNEL_LOOP(scalar_t, i, n) \
for (scalar_t i = (blockIdx.x * blockDim.x) + threadIdx.x; i < (n); \
Comment thread
rusty1s marked this conversation as resolved.
i += (blockDim.x * gridDim.x))

} // namespace utils
} // namespace pyg
15 changes: 11 additions & 4 deletions test/csrc/sampler/test_subgraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@

TEST(SubgraphTest, BasicAssertions) {
auto options = at::TensorOptions().dtype(at::kLong);
#ifdef WITH_CUDA
options = options.device(at::kCUDA);
#endif

auto nodes = at::arange(1, 5, options);
auto graph = cycle_graph(/*num_nodes=*/6, options);

auto out = pyg::sampler::subgraph(/*rowptr=*/std::get<0>(graph),
/*col=*/std::get<1>(graph), nodes);

std::cout << std::get<0>(out) << std::endl;
std::cout << std::get<1>(out) << std::endl;
std::cout << std::get<2>(out).value() << std::endl;

auto expected_rowptr = at::tensor({0, 1, 3, 5, 6}, options);
EXPECT_TRUE(at::equal(std::get<0>(out), expected_rowptr));
auto expected_col = at::tensor({1, 0, 2, 1, 3, 2}, options);
EXPECT_TRUE(at::equal(std::get<1>(out), expected_col));
auto expected_edge_id = at::tensor({3, 4, 5, 6, 7, 8}, options);
EXPECT_TRUE(at::equal(std::get<2>(out).value(), expected_edge_id));
/* auto expected_col = at::tensor({1, 0, 2, 1, 3, 2}, options); */
/* EXPECT_TRUE(at::equal(std::get<1>(out), expected_col)); */
/* auto expected_edge_id = at::tensor({3, 4, 5, 6, 7, 8}, options); */
/* EXPECT_TRUE(at::equal(std::get<2>(out).value(), expected_edge_id)); */
}