-
Notifications
You must be signed in to change notification settings - Fork 62
pyg::subgraph CUDA implementation
#42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
07f6a85
058c0bc
f85ff1e
4aa86b9
4bb8087
ef68b2b
377e261
9b5d9f7
9b4e6e6
bf23274
9113818
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
| 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have an example of bundling I am okay with dropping the warp-level parallelism for now, but we will lose the contiguous access to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On a second look, this doesn't seem possible since |
||
| 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 | ||
| 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); \ | ||
|
rusty1s marked this conversation as resolved.
|
||
| i += (blockDim.x * gridDim.x)) | ||
|
|
||
| } // namespace utils | ||
| } // namespace pyg | ||
Uh oh!
There was an error while loading. Please reload this page.