Skip to content

Avoid rebuilding a connection set on every cluster join - #2466

Open
nikosavola wants to merge 5 commits into
KLayout:masterfrom
nikosavola:nikosavola/pr-join-clusters
Open

nikosavola wants to merge 5 commits into
KLayout:masterfrom
nikosavola:nikosavola/pr-join-clusters

Conversation

@nikosavola

@nikosavola nikosavola commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor
  • connected_clusters::join_cluster_with built a std::set over the whole target connection list on every join to drop duplicates; on big nets (supply nets absorbing thousands of instance clusters one by one) that is quadratic and was about 28% of connectivity time on the vexriscv LVS testdata
  • joins now scan the target directly when the list being joined is small and build the set only for large joins; join_clusters_with builds its set once per call; connection lists keep exactly the same contents and order
  • new tests for big-net joins, duplicates in joined lists, and rename followed by join

vexriscv LVS flow (C++ reproduction of testdata/lvs/vexriscv.lvs), same netlist hash in every run:

threads before (s) after (s) connect before (s) connect after (s) speedup
1 (n=10) 33.56 ± 0.29 23.25 ± 0.12 19.65 ± 0.24 9.32 ± 0.08 1.44x
8 (n=5) 34.57 ± 0.21 23.49 ± 0.20 20.52 ± 0.21 9.53 ± 0.10 1.47x
40 (n=5) 34.38 ± 0.37 23.51 ± 0.09 20.37 ± 0.28 9.53 ± 0.05 1.46x

Peak RSS unchanged (475 MB vs 465 MB at 1 thread).

benchmark (1 thread, n=6) before (ms) after (ms) speedup
hier_clusters_big_net/2000 114.98 ± 0.09 12.11 ± 0.04 9.5x
hier_clusters_big_net/8000 2255 ± 71 313 ± 12 7.2x

The benchmarks use the Google Benchmark suite from parallelize-rebased on my fork, which is not part of this PR. Numbers: 2x Intel Xeon Gold 6148 (40 cores, exclusive node), g++ 13 -O2, mean ± standard deviation.

The numbers were measured on the parallelize branch (with #2453 on top, since that branch crashes in multi-threaded connectivity otherwise); the changed code is the same on master.

Benchmark code
//  one big net absorbing many small clusters: the top cell carries one large metal net
//  and many instances of a child cell whose small clusters join the net through a
//  global net. Joining them one by one is dominated by the dedupe of the connections
//  of the big cluster.
void hier_clusters_big_net (benchmark::State &state)
{
  const int instances = int (state.range (0));
  const int pitch = 200;
  db::Layout layout;
  const unsigned int layer = layout.insert_layer (db::LayerProperties (1, 0));
  const db::cell_index_type top = layout.add_cell ("TOP");
  layout.cell (top).shapes (layer).insert (db::PolygonRef (db::Polygon (db::Box (0, -1000, instances * pitch, -900)), layout.shape_repository ()));
  const db::cell_index_type child = layout.add_cell ("CHILD");
  layout.cell (child).shapes (layer).insert (db::PolygonRef (db::Polygon (db::Box (0, 0, 100, 50)), layout.shape_repository ()));
  for (int i = 0; i < instances; ++i) {
    layout.cell (top).insert (db::CellInstArray (db::CellInst (child), db::Trans (0, false, db::Vector (i * pitch, 0))));
  }

  db::Connectivity conn;
  conn.connect (layer, layer);
  conn.connect_global (layer, "VDD");

#if defined(_OPENMP)
  omp_set_dynamic (0);
  omp_set_num_threads (1);
#endif

  for (auto _ : state) {
    db::hier_clusters<db::PolygonRef> clusters;
    clusters.build (layout, layout.cell (top), conn);
    size_t total_connections = 0;
    const db::connected_clusters<db::PolygonRef> &top_clusters = clusters.clusters_per_cell (top);
    for (db::connected_clusters<db::PolygonRef>::connections_iterator i = top_clusters.begin_connections (); i != top_clusters.end_connections (); ++i) {
      total_connections += i->second.size ();
    }
    benchmark::DoNotOptimize (total_connections);
    if (total_connections != size_t (instances)) {
      state.SkipWithError ("hier_clusters build produced the wrong number of connections");
      break;
    }
  }
  state.SetItemsProcessed (int64_t (state.iterations ()) * instances);
}

BENCHMARK (hier_clusters_big_net)->Arg (2000)->Arg (8000)->UseRealTime ()->Unit (benchmark::kMillisecond);

This PR was carried out with AI assistance (open-weight GLM 5.3) and reviewed before submission.

nikosavola and others added 4 commits September 30, 2026 09:44
Co-Authored-By: GLM 5.3 <noreply@z.ai>
- erase_after unlinked and deleted the node but did not update mp_last or m_size
- erasing the last element left mp_last dangling, so the next push_back or splice wrote through freed memory, and size () over-counted after any erase
- reachable from connected_clusters::rename_connection, which uses erase_after on connection lists

Co-Authored-By: GLM 5.3 <noreply@z.ai>
Co-Authored-By: GLM 5.3 <noreply@z.ai>
- join_cluster_with built a std::set over the whole target connection list on every join, which is quadratic for big nets absorbing many instance clusters
- small joins scan the target directly, large joins build the set once; join_clusters_with builds it once per call; list contents and order are unchanged

| vexriscv LVS, 1 thread (n=10) | before (s) | after (s) |
|---|---:|---:|
| total | 33.56 ± 0.29 | 23.25 ± 0.12 |
| connect | 19.65 ± 0.24 | 9.32 ± 0.08 |

hier_clusters_big_net/8000: 2255 ± 71 ms -> 313 ± 12 ms (n=6). Measured on 2x Intel Xeon Gold 6148 (40 cores), g++ 13 -O2, mean ± standard deviation.

Co-Authored-By: GLM 5.3 <noreply@z.ai>
@nikosavola
nikosavola force-pushed the nikosavola/pr-join-clusters branch from cab899c to aacbf75 Compare September 30, 2026 11:02
@nikosavola
nikosavola changed the base branch from parallelize to master September 30, 2026 11:02
@nikosavola
nikosavola marked this pull request as ready for review September 30, 2026 11:09
@klayoutmatthias

Copy link
Copy Markdown
Collaborator

Hmm ... I don't see how exactly how the patch basically solves the problem of quadratic complexity (?). With std::set and a small join set, the complexity was O(N*log(N)) for the set building and O(log(N))*M for the search (N is 'target' and M is 'to_join'). Now it's O(N*M) because of the linear search in 'target' for every element of 'to_join'. It may be faster because the overhead of std::set is gone, but that's not a fundamental improvement.

Still it's a cheap gain. I think the loop can be optimized somewhat (see my comments).

Comment thread src/db/db/dbHierNetworkProcessor.cc Outdated
Comment thread src/db/db/dbHierNetworkProcessor.cc
@nikosavola

Copy link
Copy Markdown
Contributor Author

You are right, it is still O(N*M), so not a real fix. The gain is that small joins no longer build a std::set of the whole target each time. That was the main cost when a big net takes in many small clusters. A real fix needs an index per cluster, and that costs memory. I added a commit for your two comments.

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