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

gds: init scc computation using kosaraju's algorithm #4893

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open

Conversation

sdht0
Copy link
Contributor

@sdht0 sdht0 commented Feb 12, 2025

Description

Single-threaded implementation of SCC using Kosaraju's_algorithm.

  • Does not use EdgeCompute, but instead is directly called from exec().
  • Added couple of tests. Are the internals IDs assigned in a deterministic manner?

Contributor agreement

src/function/gds/strongly_connected_components.cpp Outdated Show resolved Hide resolved
void compute() {
for (const auto& [tableID, numNodes] : numNodesMap) {
sccState.pinTable(tableID);
for (auto& [fromEntry, toEntry, relEntry] : graph->getRelFromToEntryInfos()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better way to loop through the vertices and their neighbors?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine. Though I would constraint kosaraju to single rel table for now until we figure out what's the right API for DFS. Let's validate this in StronglyConnectedComponent::bind()

return;
};
sccState.updateVisit(node.offset, true);
auto scanState = graph->prepareRelScan(relEntry, "");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This state should be recreated in each recursion, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes but the performance might be terrible. I would do something like

auto state = graph->prepareRelScan(); // Always reuse this scan state

dfs(n):
   nbrs; 
   for chunk : scanFwd(state) {
         // update nbrs
   } 
   for nbr : nbrs : {
       dfs(nbr)
   }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I implemented this if I understood you correctly. Is it the case that maintaining a second copy of the graph is faster than scanning the main graph?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't quite understand what do you by a copy of graph. I was trying to reuse scanState in the example above.

Copy link
Contributor

@andyfengHKU andyfengHKU left a comment

Choose a reason for hiding this comment

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

Overall LGTM. Let's discuss a bit more with @semihsalihoglu-uw

src/function/gds/gds_utils.cpp Show resolved Hide resolved
src/function/gds/strongly_connected_components.cpp Outdated Show resolved Hide resolved
src/function/gds/strongly_connected_components.cpp Outdated Show resolved Hide resolved
src/function/gds/strongly_connected_components.cpp Outdated Show resolved Hide resolved
return;
};
sccState.updateVisit(node.offset, true);
auto scanState = graph->prepareRelScan(relEntry, "");
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes but the performance might be terrible. I would do something like

auto state = graph->prepareRelScan(); // Always reuse this scan state

dfs(n):
   nbrs; 
   for chunk : scanFwd(state) {
         // update nbrs
   } 
   for nbr : nbrs : {
       dfs(nbr)
   }

src/function/gds/strongly_connected_components.cpp Outdated Show resolved Hide resolved
src/function/gds/strongly_connected_components.cpp Outdated Show resolved Hide resolved
src/function/gds/strongly_connected_components.cpp Outdated Show resolved Hide resolved
src/function/gds/strongly_connected_components.cpp Outdated Show resolved Hide resolved
}

private:
std::vector<nodeID_t> queue;
Copy link
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 will perform if the size of nodes is 1B. Let's see it's performance first.

Copy link

Benchmark Result

Master commit hash: a88d57e3d3a1323feb88a31f946171cd17f20ac9
Branch commit hash: 2cb9982ed971891ff67270882bd503c2669a2a1c

Query Group Query Name Mean Time - Commit (ms) Mean Time - Master (ms) Diff
aggregation q24 719.84 724.38 -4.54 (-0.63%)
aggregation q28 6367.33 6365.18 2.16 (0.03%)
filter q14 119.74 126.15 -6.41 (-5.08%)
filter q15 117.77 125.06 -7.29 (-5.83%)
filter q16 302.94 301.31 1.63 (0.54%)
filter q17 445.37 446.32 -0.95 (-0.21%)
filter q18 1897.09 1939.86 -42.77 (-2.20%)
filter zonemap-node 80.74 90.69 -9.95 (-10.97%)
filter zonemap-node-lhs-cast 82.27 88.98 -6.71 (-7.54%)
filter zonemap-node-null 81.28 88.59 -7.31 (-8.25%)
filter zonemap-rel 5422.08 5422.24 -0.16 (-0.00%)
fixed_size_expr_evaluator q07 561.50 578.36 -16.85 (-2.91%)
fixed_size_expr_evaluator q08 793.52 807.96 -14.45 (-1.79%)
fixed_size_expr_evaluator q09 796.81 811.59 -14.78 (-1.82%)
fixed_size_expr_evaluator q10 228.22 245.64 -17.42 (-7.09%)
fixed_size_expr_evaluator q11 221.28 238.22 -16.94 (-7.11%)
fixed_size_expr_evaluator q12 221.58 236.79 -15.21 (-6.42%)
fixed_size_expr_evaluator q13 1454.91 1456.47 -1.56 (-0.11%)
fixed_size_seq_scan q23 107.22 117.14 -9.92 (-8.47%)
join q29 760.10 732.30 27.80 (3.80%)
join q30 10540.17 10505.58 34.59 (0.33%)
join q31 7.77 7.93 -0.16 (-2.00%)
join SelectiveTwoHopJoin 55.66 57.26 -1.60 (-2.80%)
ldbc_snb_ic q35 2620.54 2603.11 17.44 (0.67%)
ldbc_snb_ic q36 458.47 482.67 -24.20 (-5.01%)
ldbc_snb_is q32 5.61 5.32 0.29 (5.55%)
ldbc_snb_is q33 15.44 17.42 -1.98 (-11.36%)
ldbc_snb_is q34 1.37 1.10 0.27 (24.62%)
multi-rel multi-rel-large-scan 1309.96 1642.31 -332.35 (-20.24%)
multi-rel multi-rel-lookup 45.10 21.48 23.61 (109.93%)
multi-rel multi-rel-small-scan 68.56 92.81 -24.24 (-26.12%)
order_by q25 128.77 131.07 -2.30 (-1.76%)
order_by q26 441.85 454.88 -13.04 (-2.87%)
order_by q27 1394.83 1406.59 -11.75 (-0.84%)
recursive_join recursive-join-bidirection 273.02 310.91 -37.89 (-12.19%)
recursive_join recursive-join-dense 5776.11 7376.56 -1600.45 (-21.70%)
recursive_join recursive-join-path 23896.08 24322.43 -426.36 (-1.75%)
recursive_join recursive-join-sparse 1050.98 1047.53 3.45 (0.33%)
recursive_join recursive-join-trail 5962.55 7353.06 -1390.51 (-18.91%)
scan_after_filter q01 167.97 170.35 -2.37 (-1.39%)
scan_after_filter q02 149.92 160.39 -10.47 (-6.53%)
shortest_path_ldbc100 q37 89.04 88.63 0.41 (0.46%)
shortest_path_ldbc100 q38 375.07 371.13 3.94 (1.06%)
shortest_path_ldbc100 q39 62.23 66.35 -4.12 (-6.21%)
shortest_path_ldbc100 q40 452.41 426.33 26.08 (6.12%)
var_size_expr_evaluator q03 2049.40 2082.96 -33.56 (-1.61%)
var_size_expr_evaluator q04 2207.87 2225.81 -17.95 (-0.81%)
var_size_expr_evaluator q05 2908.47 2628.58 279.89 (10.65%)
var_size_expr_evaluator q06 1348.45 1334.23 14.22 (1.07%)
var_size_seq_scan q19 1435.03 1464.03 -29.00 (-1.98%)
var_size_seq_scan q20 2305.37 2346.12 -40.75 (-1.74%)
var_size_seq_scan q21 2263.76 2278.22 -14.46 (-0.63%)
var_size_seq_scan q22 125.01 127.74 -2.73 (-2.14%)

Copy link
Contributor

@andyfengHKU andyfengHKU left a comment

Choose a reason for hiding this comment

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

I just have minor comments.

Let's get some benchmark numbers both in terms of performance and correctness. You can validate this against Neo4j. If the benchmark goes fine, let's extend it to other algorithms as well, Howe did some setup here. You can take a look how much of it will be useful.

Also let's understand if std::queue will scale or not.

return true;
}

bool getVisit(offset_t offset) { return visits[offset]; }
Copy link
Contributor

Choose a reason for hiding this comment

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

rename to visited

for (auto chunk : graph->scanBwd(nodeID, scanState)) {
chunk.forEach([&](auto nbrNodeID, auto) { nbrs.push_back(nbrNodeID.offset); });
}
neighbors[node] = nbrs;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to save nbrs into neighbors. I thought you could just keep it as local variable. Same for fwdDFS

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This helps reuse the allocation from forwardDFS in backwardsDFS.

return;
};
sccState.updateVisit(node.offset, true);
auto scanState = graph->prepareRelScan(relEntry, "");
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't quite understand what do you by a copy of graph. I was trying to reuse scanState in the example above.

void compute() {
for (const auto& [tableID, numNodes] : numNodesMap) {
sccState.pinTable(tableID);
for (auto& [fromEntry, toEntry, relEntry] : graph->getRelFromToEntryInfos()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine. Though I would constraint kosaraju to single rel table for now until we figure out what's the right API for DFS. Let's validate this in StronglyConnectedComponent::bind()

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.

2 participants