Build a Dendrogram class, adapt Louvain/Leiden/ECG to use it#1359
Build a Dendrogram class, adapt Louvain/Leiden/ECG to use it#1359rapids-bot[bot] merged 21 commits intorapidsai:branch-0.18from
Conversation
1) Separate flatten_dendogram from dendogram class 2) Add initialize_dendogram_level function 3) Created an ECG variation of Louvain that initializes the dendogram with a random ordering of vertex ids rather than creating a new graph.
Codecov Report
@@ Coverage Diff @@
## branch-0.18 #1359 +/- ##
===============================================
+ Coverage 60.38% 60.64% +0.25%
===============================================
Files 67 69 +2
Lines 3029 3120 +91
===============================================
+ Hits 1829 1892 +63
- Misses 1200 1228 +28
Continue to review full report at Codecov.
|
|
rerun tests |
|
|
||
| #include <memory> | ||
| #include <rmm/device_buffer.hpp> | ||
| #include <vector> |
There was a problem hiding this comment.
Do we have any guidance on ordering include statements?
What I do is including more local headers first (rmm headers are more local than std C++ headers) like the following.
#include <rmm/device_buffer.hpp>
#include <memory>
#include <vector>
AFAIK, this is quite widely used in RAPIDS, and I started to follow this practice, and we may better stick to this unless we have other guidelines?
There was a problem hiding this comment.
I had forgotten to leave the blank line and clang-format reordered them. This will be updated in my next push
| template <typename vertex_t> | ||
| class Dendrogram { | ||
| public: | ||
| Dendrogram() : level_size_(), level_ptr_() {} |
There was a problem hiding this comment.
Do we need this? This does not do anything special, so can't we just rely on the default constructor (automatically generated by the compiler).
There was a problem hiding this comment.
Residual. An earlier version (pre-PR) had something that was actually initialized here. When I separated the code for flattening the Dendrogram out of the class (in preparation for moving to RAFT), the constructor became simpler.
Removed in next push.
| namespace cugraph { | ||
|
|
||
| template <typename vertex_t> | ||
| class Dendrogram { |
There was a problem hiding this comment.
What's our guideline on class naming? IIRC, we're asked to use dendrogram_t instead of Dendrogram.
There was a problem hiding this comment.
I like class names to start with a capital letter. dendrogram_t should be a type (which technically could be a class )
There was a problem hiding this comment.
We have a mix in our code base. I prefer the capital letter starting a class name, and using the _t suffix on type parameters in the templates.
We should codify our coding standards better.
There was a problem hiding this comment.
Yes, I thought the same: naming convention is dendogram_t. If not, then what is the rule of naming types (a class is a type...)?
|
|
||
| void add_level(vertex_t num_verts) | ||
| { | ||
| cudaStream_t stream{0}; |
There was a problem hiding this comment.
This is not stream ready. Better follow rmm's approach?
https://github.com/rapidsai/rmm/blob/branch-0.18/include/rmm/device_buffer.hpp#L289
There was a problem hiding this comment.
That might be more than we need, especially since RAFT (where this class will be moved in 0.19) doesn't use RMM the same way. I'll push a change to partially address this. Let me know if you think I should go further than the next push or if that's sufficient for now (since we'll have to refactor this method for RAFT integration).
There was a problem hiding this comment.
We at least need a mechanism to specify the stream to execute this. The RMM way is one example (and I think following the RMM style is good to enforce some consistency across RAPIDS projects) but if this is too much and there is a simpler way, I am OK as well. But we at least need a mechanism to specify the stream to execute this.
|
|
||
| size_t num_levels() const { return level_size_.size(); } | ||
|
|
||
| vertex_t *get_level_ptr_unsafe(size_t level) const |
There was a problem hiding this comment.
Just FYI,
I have used nocheck instead of unsafe, but I think unsafe is a better name.
If no one opposes, I will submit a PR replacing nocheck with unsafe to be more consistent.
There was a problem hiding this comment.
Little more on this.
I just realized that I used nocheck following cudf.
cudf is using unsafe as well (e.g. set_bit_unsafe) but nocheck is used in cudf::column_device_view which is more widely used than bit manipulation utilities.
This being said, any preference between nocheck vs unsafe?
There was a problem hiding this comment.
I should probably switch to nocheck. I recalled using the unsafe suffix in some previous work and it seemed to be appropriate here. However, after your comment I looked back at some of that. Generally what I have done in the past is:
nochecksuffix indicating don't check boundsunsafesuffix indicating the method is not thread-safe
That seems like a better and more consistent naming approach.
There was a problem hiding this comment.
Changed to nocheck in next push
| #include <ctime> | ||
| #include <utilities/error.hpp> | ||
| #include "utilities/graph_utils.cuh" | ||
| #include <utilities/graph_utils.cuh> |
There was a problem hiding this comment.
Better order include statements?
There was a problem hiding this comment.
I can do that in the next push.
| IdxT *parts, | ||
| ValT *weights) | ||
| __global__ void match_check_kernel( | ||
| IdxT size, IdxT num_verts, IdxT *offsets, IdxT *indices, IdxT *parts, ValT *weights) |
There was a problem hiding this comment.
Should we better use thrust?
thrust::transform(
...
[...]__device__(auto ...) {
auto source = thrust::upper_bound(thrust::seq, ...);
...
}
);
What do we get by writing a custom kernel?
There was a problem hiding this comment.
Don't know. This custom kernel was already there. My clang-format restructured this line.
I'd rather address this issue in the MNMG ECG implementation when we refactor this than to do this now. I'm adding a FIXME to remind me to do that.
There was a problem hiding this comment.
I agree w/ @seunghwak . The long-held policy has been to avoid custom-written kernels as much as possible. And use thrust / CUB instead.
There was a problem hiding this comment.
Let me restate my response, perhaps I was too terse.
I agree, this seems like a place where we should just use thrust/cub. There's no reason that I can think of that this custom kernel would be better than a thrust/cub implementation. There's nothing in this kernel that takes advantage of features that would make a custom kernel perform better, and generally it increases the maintenance cost of the code.
I modified this file (ecg.cu) solely for the purpose of using the Dendrogram class, since the ECG algorithm calls Louvain and I modified Louvain. I'm reluctant, in general, to rewrite things in the code that I tangentially touch. It tends to make the work continue without getting anything finished and merged into the baseline.
The only reason this was even in the list of changes is because I ran clang-format and it changed the format of these lines of code. I made no changes relevant to this kernel or the calls to it.
I have a branch where I have started the MNMG ECG work. In that branch I will completely refactor how the ECG code works. My intention is to remove this kernel in that branch which hopefully will be merged sometime during release 0.19.
If we think it's critical that I do this for release 0.18, I can certainly take a few hours and refactor this once I'm done addressing the device_uvector request.
| #include <rmm/thrust_rmm_allocator.h> | ||
| #include <community/dendrogram.cuh> | ||
| #include <experimental/graph_functions.hpp> | ||
| #include <raft/handle.hpp> |
There was a problem hiding this comment.
Better order include statements?
There was a problem hiding this comment.
Fixed in next push.
There was a problem hiding this comment.
@seunghwak, include statements order is dictated by clang-format
There was a problem hiding this comment.
If you put blank lines in between blocks of statements, clang-format will honor the blocks.
| weight_t wt = runner(max_level, resolution); | ||
|
|
||
| return runner(clustering, max_level, resolution); | ||
| thrust::device_vector<vertex_t> vertex_ids_v(graph.number_of_vertices); |
There was a problem hiding this comment.
If you're not dealing with non-arithmetic types, rmm::device_uvector will be more efficient and more stream ready.
Note that rmm::device_uvector does not invoke default constructor to initialize vector elements, so if your code expects initialization, you need to call thrust::fill().
There was a problem hiding this comment.
Changed in the next push.
| IdxT *parts, | ||
| ValT *weights) | ||
| __global__ void match_check_kernel( | ||
| IdxT size, IdxT num_verts, IdxT *offsets, IdxT *indices, IdxT *parts, ValT *weights) |
There was a problem hiding this comment.
What are you using "IdxT" rather than "index_t" ? It seems like we are not using capitalized types anywhere else
There was a problem hiding this comment.
This is an existing kernel that has existed for a while. All I did was reformat.
I'd like to defer this until I finish implementing MNMG ECG when I will be reworking all of this code. (@seunghwak suggested I delete the kernel entirely).
| // a distributed implementation of get_permutation_vector, preferably without | ||
| // comms... | ||
| // | ||
|
|
There was a problem hiding this comment.
Is there an issue to address this TODO? If so, add reference. If not, should there be? I would prefer not to lose sight that these TODOs need to be addressed
There was a problem hiding this comment.
Deleted the TODO (and the others in this file). These were notes to myself related to refactoring ECG for MNMG.
The work described in this note is already in an MNMG ECG branch that stalled when @seunghwak and I discussed a new graph primitive that is required.
|
@ChuckHastings any thought on moving Dendrogram into RAFT? I believe @cjnolet has a need |
My plan was to get everything working here first and move to raft in 0.19. Fewer moving parts that way. I think @cjnolet was not in a big hurry. |
Sounds good. I'll be moving a bunch of sparse prims into RAFT in 0.19 as well, including the single-linkage clustering. |
|
|
||
| size_t num_levels() const { return level_size_.size(); } | ||
|
|
||
| vertex_t *get_level_ptr_unsafe(size_t level) const |
There was a problem hiding this comment.
It is dangerous to have a const getter return a non-const pointer to a member. Something like this, for example, can happen:
template<typename T>
void foo(Dendrogram<T> const& d)
{
d.get_level_ptr_unsafe(0)[0] = T{3};
}
int main(void)
{
Dendrogram<int> d;
foo(d);
return 0;
}
foo() should not be permitted to modify the const& argument, d. But it can. This breaks the contract that the getter is supposed to fulfill (namely being an immutable getter).
(to compile this simple example, I changed device_buffer to std::vector<vertex_t>, but the idea is the same).
At the very least, get_level_ptr_unsafe() should not be a const member.
There was a problem hiding this comment.
Sorry, that was careless. Next push will address this.
There should be a const version that returns a const pointer and a non-const version that returns a non-const pointer as it is used in both contexts.
|
|
||
| vertex_t get_level_size_unsafe(size_t level) const { return level_size_[level]; } | ||
|
|
||
| vertex_t *current_level_begin() const { return get_level_ptr_unsafe(current_level()); } |
There was a problem hiding this comment.
Same comment about making this a non-const getter.
|
|
||
| vertex_t *current_level_begin() const { return get_level_ptr_unsafe(current_level()); } | ||
|
|
||
| vertex_t *current_level_end() const { return current_level_begin() + current_level_size(); } |
There was a problem hiding this comment.
Same comment about making this a non-const getter.
| namespace cugraph { | ||
|
|
||
| template <typename vertex_t> | ||
| class Dendrogram { |
There was a problem hiding this comment.
Yes, I thought the same: naming convention is dendogram_t. If not, then what is the rule of naming types (a class is a type...)?
| IdxT *parts, | ||
| ValT *weights) | ||
| __global__ void match_check_kernel( | ||
| IdxT size, IdxT num_verts, IdxT *offsets, IdxT *indices, IdxT *parts, ValT *weights) |
There was a problem hiding this comment.
I agree w/ @seunghwak . The long-held policy has been to avoid custom-written kernels as much as possible. And use thrust / CUB instead.
| using graph_type = GraphCSRView<vertex_t, edge_t, weight_t>; | ||
|
|
||
| CUGRAPH_EXPECTS(graph.edge_data != nullptr, | ||
| "Invalid input argument: louvain expects a weighted graph"); |
There was a problem hiding this comment.
Why public inheritance? If no method is overriden (virtual) then public inheritance is too much of an exposure (hence creating unnecessary coupling). Why not use private inheritance?
Moreover, if Louvain class is meant to be publicly derived (and it seems it is since it exposes at least one virtual method) then it should have a virtual destructor.
| using graph_type = GraphCSRView<vertex_t, edge_t, weight_t>; | ||
|
|
||
| CUGRAPH_EXPECTS(graph.edge_data != nullptr, | ||
| "Invalid input argument: louvain expects a weighted graph"); |
There was a problem hiding this comment.
Again, confusion about class naming convention, shouldn't be <small_caps>_t? I understand that some are older (legacy) classes, but perhaps new classed might follow the rule?
| #include <rmm/thrust_rmm_allocator.h> | ||
| #include <community/dendrogram.cuh> | ||
| #include <experimental/graph_functions.hpp> | ||
| #include <raft/handle.hpp> |
There was a problem hiding this comment.
@seunghwak, include statements order is dictated by clang-format
| vertex_t ensemble_size, | ||
| vertex_t *clustering) | ||
| { | ||
| using graph_type = GraphCSRView<vertex_t, edge_t, weight_t>; |
There was a problem hiding this comment.
We should try and get rid of legacy classes in SG Louvain and ECG to use graph_t. Or the MG path could be used to support the 1 GPU case.
There was a problem hiding this comment.
There's work currently scheduled for 0.19 that will adapt all of these to use the graph primitives. We should be able to consider getting rid of the legacy versions at that point.
|
rerun tests |
1 similar comment
|
rerun tests |
This is now failing on Pascal (using a method that only works on > Pascal). Working on an update to the python tests to disable these tests on a Pascal system. |
|
@gpucibot merge |
rlratzel
left a comment
There was a problem hiding this comment.
LGTM, also happy to see the new notebook test skipping mechanism. One suggestion which need not hold up my approval (and I can file an issue if you agree and want to defer this):
I'm thinking it might be better to generalize the keywords a bit more with the side effect of making them more self documenting to NB users who run across them outside the context of our test infra. Maybe for example:
# AUTOMATED TESTING: skip
skips that NB when run from our test scripts
# AUTOMATED TESTING: skip on Pascal
skips that NB when run from our test scripts on a Pascal system
I'm just thinking the AUTOMATED TESTING tag makes it obvious this isn't a comment an unknowing user can just reword for clarity at some point later.
Preparing for MNMG Leiden and ECG identified an area for code cleanup.
The original cuGraph implementation of Louvain would flatten the hierarchical clustering as it was computed, filling (and returning) the final clustering. This adds an awkward step in the middle of the Louvain computation. Additionally, since Louvain (and Leiden and ECG which derive from it) is actually a hierarchical clustering algorithm it would be nice to generate the actual Dendrogram.
This PR implements a Dendrogram class, a function for flattening the Dendrogram, and modifies Louvain, Leiden and ECG to use the Dendrogram class.
It was suggested that the Dendrogram class could be moved to raft, decided to defer that until later, it's easy enough to move.