Skip to content
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

Bug fix: properly partition networks containing one-sided gap-junction connections #1774

Merged
merged 9 commits into from
Jan 6, 2022

Conversation

noraabiakar
Copy link
Contributor

The current partition_load_balance function assumes that gap-junction connections are always double-sided (i.e. if gid x has a gap-junction connection from peer gid y, then gid y must also have a gap-junction connection from peer gid x). This used to be a requirement that was checked prior to #1682. Since then, single-sided gap-junctions are in principle allowed, but partition_load_balance still operates under the bidirectional gap-junction connection assumption resulting in some gids being present in multiple cell-groups.
This PR modifies the partition_load_balance function to do the following:

  1. On each rank, generate a gj_connection list per cell. (gj_connection list is a vector of gids that have an outgoing connection ending at the cell under consideration).
  2. On each rank, gather all the gj_connection lists for each cell in the network.
  3. On each rank, modify the global list of gj_connections in the network to make all gj_connections bidirectional.
  4. On each rank, use the global list of gj_connections in the network to partition the graph.

Fixes #1767.

@bcumming
Copy link
Member

bors try

1 similar comment
@noraabiakar
Copy link
Contributor Author

bors try

@bors
Copy link

bors bot commented Nov 26, 2021

try

Already running a review

bors bot added a commit that referenced this pull request Nov 27, 2021
@bors
Copy link

bors bot commented Nov 29, 2021

try

Build failed:

@noraabiakar
Copy link
Contributor Author

bors try

bors bot added a commit that referenced this pull request Nov 29, 2021
@bors
Copy link

bors bot commented Nov 29, 2021

try

Build failed:

@noraabiakar
Copy link
Contributor Author

bors try

bors bot added a commit that referenced this pull request Dec 2, 2021
@bors
Copy link

bors bot commented Dec 2, 2021

try

Build succeeded:

@thorstenhater
Copy link
Contributor

Hi @noraabiakar,
could you elaborate why that bug happened a bit more,
would be helpful before diving into the code.

@noraabiakar
Copy link
Contributor Author

Let's say we have a system of 2 cells and cell 0 has an incoming gap junction connection from cell 1 (but no outgoing connection). This is not supported at the moment but it won't raise any exceptions.

Our connected components algorithm assumes that all gap junction connections are bidirectional and will come up with the following cells groups: {0, 1} and {1}, which is incorrect. What this PR does is change all unidirectional gap-junctions to bidirectional gap-junctions prior to calling the connected components algorithm which will then do the correct thing again, generating only one cell groups {0,1}.

Copy link
Member

@bcumming bcumming left a comment

Choose a reason for hiding this comment

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

Looks good. Just a few suggestions to improve performance

std::vector<int> counts_internal, displs_internal;

// Vector of individual vector sizes
std::vector<int> internal_sizes(values.size());
Copy link
Member

Choose a reason for hiding this comment

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

int is a narrowing cast from size_t, so I would suggest using unsigned long.


// Generate a local gj_connection table.
// The table is indexed by the index of the target gid in the gid_part of that domain.
// If gid_part[domain_id] = [a, b[; local_gj_connection of gid `x` is at index `x-a`.
Copy link
Member

Choose a reason for hiding this comment

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

The formatting of the braces is wonky: should it be [a, b]?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's intended to show that b in not in the range.

Copy link
Member

Choose a reason for hiding this comment

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

A "half open range" [a, b) == [a, a+1, a+2, ..., b-1]?

local_gj_connection_table[gid-dom_range.first].push_back(c.peer.gid);
}
}
// Sort the inner vectors of gj_connections.
Copy link
Member

Choose a reason for hiding this comment

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

Change comment to give more context, e.g. sort the gj connections of each local cell

// If gid is not in the peer connection table insert it such that
// the vector remains sorted.
auto it = std::lower_bound(peer_conns.begin(), peer_conns.end(), gid);
if (it == peer_conns.end() || *it != gid) {
Copy link
Member

Choose a reason for hiding this comment

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

This has the potential to be really expensive both in terms of time, and in memory if there are many insertions.
How about looping over once to count how many insertions have to be made (if any), allocate a new vector if need be, then do the insertions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I understand what you mean, but I changed the implementation.

Copy link
Member

Choose a reason for hiding this comment

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

I might have misunderstood what was being done here, but it looks like:

  1. search through a sorted vector for a value
  2. if the value is not found, insert it such that the vector is still sorted

There are two performance issues here:

  1. inserting in a vector will require n/2 copies to move the values after the inserted value out of the way
  2. insertion may also require reallocation of more memory, and a copy of the whole vector contents

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the new implementation is better. The vector doesn't need to be sorted in the end.

  1. Check that a gid is available in the global_gj_connection_table[peer] vector, if not, insert it into an unordered_set.
  2. Append the values of the unordered_set onto the global_gj_connection_table[gid] vectors.

Comment on lines 128 to 129
auto conns = global_gj_connection_table[element];
for (const auto& peer: conns) {
Copy link
Member

Choose a reason for hiding this comment

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

To avoid making a copy of global_gj_connection_table[element], which is a std::vector, and looking up peer twice in the visited set, consider the following:

for (const auto& peer: global_gj_connection_table[element]) {
  if (visited.insert(peer).second) {
    q.push(peer);
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that was supposed to be const auto& conns = global_gj_connection_table[element]. I will implement your suggestion anyway.

Copy link
Member

Choose a reason for hiding this comment

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

I actually meant what I proposed: there is no need to create the const auto& cons, though whether you do or don't is a matter of personal preference.

@noraabiakar
Copy link
Contributor Author

bors try

bors bot added a commit that referenced this pull request Jan 5, 2022
@bors
Copy link

bors bot commented Jan 5, 2022

try

Build failed:


// Append the missing peers into the global_gj_connections table
for (unsigned i = 0; i < global_gj_connection_table.size(); ++i) {
std::move(missing_peers[i].begin(), missing_peers[i].end(), std::back_inserter(global_gj_connection_table[i]));
Copy link
Member

Choose a reason for hiding this comment

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

are the entries in global_gj_connection_table[i] supposed to be sorted?
The loop above assumes that they are, which enables std::binary_search/std::lower_bound, however back_inserter to add missing values is not guaranteed to maintain sorted values.

@bcumming bcumming merged commit 0636bbf into arbor-sim:master Jan 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Uni-directional gap junctions
3 participants