Skip to content

Sort RR graph edges with stable counting sort - #3800

Open
soheilshahrouz wants to merge 15 commits into
masterfrom
rr_gen_edge_counting_sort
Open

Sort RR graph edges with stable counting sort#3800
soheilshahrouz wants to merge 15 commits into
masterfrom
rr_gen_edge_counting_sort

Conversation

@soheilshahrouz

Copy link
Copy Markdown
Contributor

Sorting RR graph edges by destination node and by switch configurability used std::stable_sort with a comparator, which is slow for the tens of millions of edges in large graphs.

This PR adds vtr_sort.h with a stable counting sort. t_rr_graph_storage::sort_edges is replaced with sort_edges_by_keys, which takes one or more small integer keys and sorts with counting sort passes instead of a comparison sort. Both edge sorts in rr_graph_storage and the RR graph serializer now use it.

The resulting edge order is identical to the old stable sort, so RR graph contents and QoR are unchanged.

@soheilshahrouz

Copy link
Copy Markdown
Contributor Author

Impact on RR graph generation time
stratixiv_arch.timing.xml, --route_chan_width 300

Device width master (s) branch (s) Ratio
100 27.55 23.07 0.837
200 113.20 92.21 0.815
250 173.25 139.70 0.806
300 256.22 204.30 0.797
geomean 0.814

@soheilshahrouz

Copy link
Copy Markdown
Contributor Author

Impact on reading binary RR graph files
The same architecture and channel width as the previous comment.

Device width File size master (s) branch (s) Ratio
100 0.85 GB 7.65 7.26 0.949
200 3.4 GB 31.85 29.75 0.934
250 5.3 GB 48.73 45.80 0.940
geomean 0.941

@vaughnbetz vaughnbetz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice results and the code looks good. I feel like I just failed a C++ test, but I don't see ways to make it simpler :). I have a couple of commenting suggestions but that's it. Probably want to make sure this shows up in the web vtrutil documentation too.

Comment thread libs/librrgraph/src/base/rr_graph_storage.cpp Outdated
Comment thread libs/libvtrutil/src/vtr_sort.h
* @param num_keys Exclusive upper bound on the keys. Every key must be smaller than this.
*/
template<typename InIt, typename OutIt, typename KeyFn>
void stable_counting_sort(InIt first, InIt last, OutIt out, size_t num_keys, KeyFn key_of) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think explaining the function arguments instead of template params would be more clear and also more consistent. possibly could even have both.


for (InIt it = first; it != last; ++it) {
size_t key = static_cast<size_t>(key_of(*it));
out[offsets[key]++] = *it;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This was hard to understand for me. I think if 'offsets[key] += 1' was in a separate line it would've been much easier to understand.

Comment on lines +60 to +62
for (size_t key = 1; key <= num_keys; ++key) {
offsets[key] += offsets[key - 1];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You should add a high level comment explaining the overall algorithm stages (counting, then prefix sum then writing to output)

Comment on lines +103 to +104
template<typename KeyFn>
sort_key(size_t, KeyFn) -> sort_key<KeyFn>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add a comment explaining this. I think it's here to help the type system deduce the type, but the syntax is pretty uncommon.

stable_counting_sort(first, last, std::begin(out), least_significant.num_keys, least_significant.key_of);

// Remaining passes ping-pong between out and scratch, ending in out
if constexpr (num_keys > 1) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Super nice use of if constexpr! However I think some comments for what this means and why it's here would be nice here, this feature relatively new to C++ and people might not be familiar with it.

* @param num_keys Exclusive upper bound on the keys. Every key must be smaller than this.
*/
template<typename Container, typename KeyFn>
void stable_counting_sort(Container& items, Container& scratch, size_t num_keys, KeyFn key_of) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if this and the other function shouldn't be in the details namespace. radix sort with one key compiles to just calling counting sort, with the added benefit of not having to manage a scratch memory. There also aren't any users of this function outside this file.

@AlexandreSinger AlexandreSinger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you so much @soheilshahrouz , I really really love this PR a ton.

I had some comments on how this can be made a bit clearer. Other than that, it makes sense to me.

I did have a comment on using uint32_t for offests in this function. This would limit us to RR graphs with 4 billion nodes. I think in the past we always use size_t which can be as many as 2^64; but correct me if I am wrong here.

* such inputs are a poor match for this function.
*
* @tparam InIt Forward iterator over the input elements.
* @tparam OutIt Random access iterator to the output range.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be nice to assert that InIt and OutIt are correct iterator types for this method. If we are going to go full C++, we may as well go all out LOL.

This would look like this within the function:

static_assert(std::forward_iterator<InIt>);
static_assert(std:: random_access_iterator <OutIt>);

This has no runtime overhead since these asserts will be checked at compile-time. I think this is possible in our version of C++.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actually, I just saw the "requires" down below. If you already considered this and it did not work I understand; but I think it would be good to have these requires in here if we can. Better safe.

/// Exclusive upper bound on the keys
size_t num_keys;
/// Callable mapping an element to its key
KeyFn key_of;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of a full template here, I would opt for an std::function with templated arguments. It self-documents what this is expected to be.

Correct me if I am wrong, but I think the type for this should be:

std::function<size_t(KeyType)> key_of;

I think this should be done throughout this file. It adds explicit type-casting and I would find it easier to follow.

template <typename t_comp_func>
void sort_edges(t_comp_func comparison_function) {

template <typename... KeyFns>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I personally would add an example for how to use this when there are multiple sort_keys are passed in. Variadic arguments in C++ can be very confusing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would also add an @param here explaining this argument. Its fun, but can be confusing.

VTR_ASSERT(new_index == vec.size());

vec = std::move(new_vec);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the benefit of making this a lambda? I am not entirely sure, but frankly, I think this lambda may just be compiled down into a templated static function due to the hackary we are doing with remove_refernece. Why not just do that directly?

Something like:

template<typename T>
void array_rearange(std::vector<T>& vec, T default_value) {
  std::vector<T> new_vec(...);
  ...
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think doing this will make this much easier to follow.

* counting sort pass per key, least significant key first. The input is read
* directly, so a generated sequence such as a vtr::StrongIdRange needs no copy first.
*
* Example, sorting edges by (source node, destination node):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you explain the example slightly more. I know its a bit of a pain, but I think it would be helpful to say something like "such that edges with the lower source ID, then the lowest destination ID, are put first in sorted edges". It just provides a concrete example of how to read this, since many sorting functions in std allow you to change if you want to go from lowest first or highest first.

On that note: it is currently not clear whether this is lowest first or highest first. You may want to specify.


size_t num_elements = 0;
for (InIt it = first; it != last; ++it) {
size_t key = static_cast<size_t>(key_of(*it));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a bit tricky... You are assuming that the key_of function's result is always castable to a size_t. You may want to document that somewhere. That is actually something that people may find confusing since they may want to sort on a floating-point key for example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Obviously a floating-point key does not work for counting-sort; but the user may not be aware of that. I think there is a static assert you can use to ensure that the result of key_of is at least an integer type.

size_t num_elements = 0;
for (InIt it = first; it != last; ++it) {
size_t key = static_cast<size_t>(key_of(*it));
VTR_ASSERT_MSG(key < num_keys, "Sort key must be smaller than num_keys");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Up to you: This is in the hot loop. I recommend graduating it to VTR_ASSERT_DEBUG.

The other asserts are fine in my opinion since they happen once per sort call, but I will leave this up to you. I understand if you want to keep this since this would be explosive if incorrect.

num_elements++;
}
VTR_ASSERT_MSG(num_elements <= std::numeric_limits<uint32_t>::max(),
"Number of sorted elements must fit in the 32 bit offsets");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why 32-bit offsets? Does this mean that the vectors being sorted can only be at most length 2^32? I would document this limitation if that is the case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think size_t can be as high as 64 bits on a 64 bit operating system. And I am pretty sure we can have RR graphs bigger than 4 billion nodes; but maybe I am incorrect here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants