Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Feb 4, 2026

The consensus_tree function segfaulted on arm64 for trees exceeding 16383 leaves due to fixed-size stack allocations (std::bitset<16384> and std::array<int32, 65532>). The 33333-leaf test case triggered stack overflow.

Changes

Dynamic allocation strategy:

  • Stack allocation for trees ≤8192 leaves (performance-critical path)
  • Heap allocation for trees 8192-100000 leaves
  • Graceful error for trees >100000 leaves

ClusterTable.h:

  • Added DynamicBitset class that switches between std::bitset<8193> (stack) and std::vector<bool> (heap) based on tree size
  • Replaced fixed std::bitset<ct_max_leaves + 1> Xswitch with DynamicBitset Xswitch
  • Updated constructor to initialize Xswitch.resize(n_leaves + 1)

consensus.cpp:

  • Refactored core logic into template consensus_tree_impl<StackContainer>
  • Added size-based dispatch: std::array<int32, 32768> for small trees, std::vector<int32> for large
  • Macros CT_PUSH/CT_POP work transparently with both containers

Tests:

  • Updated consensus test to verify 33333-leaf case succeeds
  • Updated ClusterTable test to verify heap allocation at 20000 leaves
  • Both verify error handling at 100001 leaves

Backward compatibility

The ct_max_leaves constant (16383) is preserved for TreeDist 2.9.2 compatibility, though no longer enforced as a hard limit.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 4 commits February 4, 2026 13:14
- Add stack threshold (8192) and heap limit (100000) constants
- Create DynamicBitset class for Xswitch with stack/heap allocation
- Template consensus_tree_impl to handle both array and vector S
- Update error messages with specific limits
- Update tests for new limits and add heap allocation tests

Co-authored-by: ms609 <1695515+ms609@users.noreply.github.com>
Co-authored-by: ms609 <1695515+ms609@users.noreply.github.com>
Co-authored-by: ms609 <1695515+ms609@users.noreply.github.com>
Co-authored-by: ms609 <1695515+ms609@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor consensus_tree logic for dynamic tree sizes Fix consensus_tree segfault on arm64 by implementing dynamic stack/heap allocation Feb 4, 2026
Copilot AI requested a review from ms609 February 4, 2026 13:25
@github-actions
Copy link

github-actions bot commented Feb 4, 2026

⚠️ This benchmark result is outdated. See the latest comment below.

Performance benchmark results

Call Status Change Time (ms)
as.Splits(bigTrees) ⚪ NSD -5.4% 22.6 →
24.3, 23.7
as.Splits(someTrees) ⚪ NSD 0.74% 11.2 →
11, 11.2
Consensus(forest1k.888, check = FALSE) ⚪ NSD -2.78% 94.8 →
96.1, 97.9
Consensus(forest201.80, check = FALSE) ⚪ NSD -2.94% 3.98 →
4.09, 4.11
Consensus(forest21.260, 0.5, FALSE) 🟣 ~same -4.07% 1.19 →
1.24, 1.25
Consensus(forest21.260) 🟣 ~same -3.53% 1.19 →
1.23, 1.24
Consensus(forestMaj, 0.5, FALSE) ⚪ NSD -2.03% 2.92 →
2.95, 2.99
DropTip(tr2000, 5) ⚪ NSD 0.17% 20 →
19.7, 20
DropTip(tr80, 5) ⚪ NSD 1.24% 0.105 →
0.103, 0.104
DropTip(unlen2k, 5) ⚪ NSD 0.17% 0.21 →
0.209, 0.211
DropTip(unlen80, 5) ⚪ NSD -0.5% 0.0402 →
0.04, 0.0407
lapply(bigSplits, as.phylo) ⚪ NSD 0.31% 30.5 →
30.5, 30.2
lapply(someSplits, as.phylo) ⚪ NSD 0.57% 14 →
13.9, 13.9
PathLengths(tr2000, full = TRUE) ⚪ NSD 0.23% 19.2 →
19.3, 18.9
PathLengths(tr80, full = TRUE) ⚪ NSD -0.17% 0.104 →
0.104, 0.105
PathLengths(tr80Unif, full = TRUE) ⚪ NSD 68.95% 0.107 →
0.106, 0.0326
RootTree(tr2000, 5) ⚪ NSD -4.59% 0.369 →
0.376, 0.392
RootTree(tr80, c("t3", "t36")) ⚪ NSD -0.83% 0.069 →
0.0703, 0.0686
RootTree(tr80, "t3") ⚪ NSD -0.86% 0.0491 →
0.0494, 0.0496
RootTree(tr80, "t30") ⚪ NSD -0.45% 0.0493 →
0.0497, 0.0494
RootTree(unlen2k, 5) ⚪ NSD -0.7% 0.331 →
0.336, 0.329
RootTree(unlen80, c("t3", "t36")) ⚪ NSD -1.95% 0.0627 →
0.0642, 0.0636
RootTree(unlen80, "t3") ⚪ NSD 0.07% 0.0428 →
0.043, 0.0426
RootTree(unlen80, "t30") ⚪ NSD -0.05% 0.043 →
0.0434, 0.0425
TreeDist::RobinsonFoulds(forest201.80) ⚪ NSD -2.29% 14.9 →
15.1, 15.3
TreeDist::RobinsonFoulds(forest21.888) ⚪ NSD -3.7% 3.12 →
3.15, 3.26
TreeTools:::path_lengths(tr80$edge, tr80$edge.length, FALSE) ⚪ NSD -0.03% 0.0925 →
0.0921, 0.0929
TreeTools:::postorder_order(bal40) ⚪ NSD -0.61% 0.00165 →
0.00166, 0.00167
TreeTools:::postorder_order(bal40k) ⚪ NSD -0.75% 0.441 →
0.442, 0.446
TreeTools:::postorder_order(dbal40) ⚪ NSD -0.65% 0.00171 →
0.00172, 0.00174
TreeTools:::postorder_order(dbal40k) ⚪ NSD 0.47% 2.14 →
2.12, 2.14
TreeTools:::postorder_order(dpec40) ⚪ NSD -3.18% 0.00252 →
0.00255, 0.00281
TreeTools:::postorder_order(dpec40k) 🟣 ~same 3.94% 3420 →
3290, 3290
TreeTools:::postorder_order(drnd80) ⚪ NSD -1.03% 0.00398 →
0.00396, 0.00413
TreeTools:::postorder_order(nbal40) ⚪ NSD -2.91% 0.00206 →
0.00209, 0.00217
TreeTools:::postorder_order(nbal40k) ⚪ NSD 0.85% 2.19 →
2.16, 2.18
TreeTools:::postorder_order(npec40) ⚪ NSD -2.45% 0.00282 →
0.00284, 0.00312
TreeTools:::postorder_order(npec40k) 🟣 ~same 3.72% 3440 →
3310, 3310
TreeTools:::postorder_order(nrnd80) ⚪ NSD -1.32% 0.00453 →
0.00457, 0.00461
TreeTools:::postorder_order(pec40) ⚪ NSD -9.07% 0.00165 →
0.00165, 0.00195
TreeTools:::postorder_order(pec40k) ⚪ NSD 1.71% 0.435 →
0.428, 0.427
TreeTools:::postorder_order(rnd80) ⚪ NSD -2.91% 0.00209 →
0.00208, 0.0023

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

⚠️ This benchmark result is outdated. See the latest comment below.

Performance benchmark results

Call Status Change Time (ms)
as.Splits(bigTrees) ⚪ NSD 0.72% 24.2 →
23.2, 24.7
as.Splits(someTrees) ⚪ NSD 1.38% 12.2 →
11.8, 12.2
Consensus(forest1k.888, check = FALSE) ⚪ NSD 3.24% 105 →
101, 103
Consensus(forest201.80, check = FALSE) ⚪ NSD 5.72% 4.47 →
4.21, 4.23
Consensus(forest21.260, 0.5, FALSE) ⚪ NSD -0.84% 1.24 →
1.25, 1.26
Consensus(forest21.260) ⚪ NSD -0.98% 1.23 →
1.25, 1.25
Consensus(forestMaj, 0.5, FALSE) ⚪ NSD -0.97% 3.07 →
3.06, 3.13
DropTip(tr2000, 5) 🟣 ~same 4.37% 22.2 →
20.9, 21.4
DropTip(tr80, 5) ⚪ NSD 0.2% 0.108 →
0.108, 0.108
DropTip(unlen2k, 5) ⚪ NSD -1.81% 0.212 →
0.213, 0.221
DropTip(unlen80, 5) ⚪ NSD 0.12% 0.0413 →
0.0411, 0.0414
lapply(bigSplits, as.phylo) ⚪ NSD -0.03% 31 →
30.9, 31.2
lapply(someSplits, as.phylo) ⚪ NSD 1.97% 14.8 →
14.6, 14.2
PathLengths(tr2000, full = TRUE) ⚪ NSD -0.12% 21 →
19.9, 21.4
PathLengths(tr80, full = TRUE) 🟢 Faster! 71.91% 0.105 →
0.0294, 0.0302
PathLengths(tr80Unif, full = TRUE) 🟢 Faster! 70.75% 0.107 →
0.0312, 0.0315
RootTree(tr2000, 5) ⚪ NSD 0.85% 0.384 →
0.379, 0.382
RootTree(tr80, c("t3", "t36")) ⚪ NSD 0.24% 0.0714 →
0.0712, 0.0712
RootTree(tr80, "t3") ⚪ NSD 0.69% 0.0509 →
0.0505, 0.0505
RootTree(tr80, "t30") ⚪ NSD 1.73% 0.0515 →
0.0506, 0.0505
RootTree(unlen2k, 5) ⚪ NSD 0.87% 0.342 →
0.339, 0.339
RootTree(unlen80, c("t3", "t36")) ⚪ NSD 0.41% 0.0666 →
0.0662, 0.0666
RootTree(unlen80, "t3") ⚪ NSD 2.15% 0.0452 →
0.0439, 0.0445
RootTree(unlen80, "t30") ⚪ NSD 1.92% 0.0455 →
0.0443, 0.0449
TreeDist::RobinsonFoulds(forest201.80) ⚪ NSD -2.83% 16.2 →
16.7, 16.8
TreeDist::RobinsonFoulds(forest21.888) ⚪ NSD -0.33% 3.32 →
3.23, 3.37
TreeTools:::path_lengths(tr80$edge, tr80$edge.length, FALSE) ⚪ NSD 3.9% 0.0945 →
0.0913, 0.0904
TreeTools:::postorder_order(bal40) ⚪ NSD -2.42% 0.00165 →
0.00168, 0.0017
TreeTools:::postorder_order(bal40k) ⚪ NSD -0.56% 0.443 →
0.445, 0.446
TreeTools:::postorder_order(dbal40) ⚪ NSD -1.73% 0.00173 →
0.00173, 0.00179
TreeTools:::postorder_order(dbal40k) ⚪ NSD 1.03% 2.16 →
2.13, 2.14
TreeTools:::postorder_order(dpec40) ⚪ NSD -0.8% 0.00252 →
0.00252, 0.00256
TreeTools:::postorder_order(dpec40k) 🟣 ~same 1.68% 3350 →
3300, 3290
TreeTools:::postorder_order(drnd80) ⚪ NSD -1.52% 0.00396 →
0.00402, 0.00403
TreeTools:::postorder_order(nbal40) ⚪ NSD -1.9% 0.0021 →
0.0021, 0.00218
TreeTools:::postorder_order(nbal40k) ⚪ NSD 0.65% 2.2 →
2.17, 2.2
TreeTools:::postorder_order(npec40) ⚪ NSD 0.03% 0.00284 →
0.00283, 0.00285
TreeTools:::postorder_order(npec40k) 🟣 ~same 2.53% 3410 →
3320, 3320
TreeTools:::postorder_order(nrnd80) ⚪ NSD -0.43% 0.00459 →
0.0046, 0.00461
TreeTools:::postorder_order(pec40) ⚪ NSD 0.06% 0.00167 →
0.00165, 0.0017
TreeTools:::postorder_order(pec40k) ⚪ NSD -0.48% 0.435 →
0.438, 0.437
TreeTools:::postorder_order(rnd80) ⚪ NSD 2.36% 0.00212 →
0.00203, 0.00213

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

Performance benchmark results

Call Status Change Time (ms)
as.Splits(bigTrees) ⚪ NSD 1.19% 23.7 →
24, 22.2
as.Splits(someTrees) ⚪ NSD -2.53% 11.1 →
11.4, 11.3
Consensus(forest1k.888, check = FALSE) 🟠 Slower 🙁 -6.38% 95.1 →
101, 102
Consensus(forest201.80, check = FALSE) ⚪ NSD -2.8% 4.01 →
4.16, 4.09
Consensus(forest21.260, 0.5, FALSE) 🟣 ~same -4.1% 1.2 →
1.25, 1.25
Consensus(forest21.260) 🟣 ~same -3.97% 1.19 →
1.24, 1.24
Consensus(forestMaj, 0.5, FALSE) ⚪ NSD -1.66% 2.95 →
3, 2.99
DropTip(tr2000, 5) ⚪ NSD 0.38% 20.6 →
20.4, 20.7
DropTip(tr80, 5) ⚪ NSD -1.25% 0.104 →
0.106, 0.104
DropTip(unlen2k, 5) 🟢 Faster! 23.94% 0.277 →
0.208, 0.217
DropTip(unlen80, 5) ⚪ NSD -0.93% 0.0401 →
0.0408, 0.04
lapply(bigSplits, as.phylo) 🟣 ~same 1.91% 30.9 →
30.2, 30.3
lapply(someSplits, as.phylo) ⚪ NSD -0.99% 13.9 →
14.1, 13.9
PathLengths(tr2000, full = TRUE) ⚪ NSD 0.41% 20 →
20, 19.8
PathLengths(tr80, full = TRUE) ⚪ NSD -0.21% 0.102 →
0.102, 0.103
PathLengths(tr80Unif, full = TRUE) 🟢 Faster! 69.82% 0.102 →
0.0301, 0.0313
RootTree(tr2000, 5) ⚪ NSD -3.49% 0.374 →
0.395, 0.374
RootTree(tr80, c("t3", "t36")) ⚪ NSD 0.94% 0.0701 →
0.0697, 0.0692
RootTree(tr80, "t3") ⚪ NSD -1.33% 0.0492 →
0.0502, 0.0493
RootTree(tr80, "t30") ⚪ NSD -0.44% 0.0496 →
0.0501, 0.0494
RootTree(unlen2k, 5) ⚪ NSD 0% 0.334 →
0.332, 0.337
RootTree(unlen80, c("t3", "t36")) ⚪ NSD 0.06% 0.0648 →
0.0653, 0.0641
RootTree(unlen80, "t3") ⚪ NSD -1.15% 0.0428 →
0.0435, 0.043
RootTree(unlen80, "t30") ⚪ NSD -0.49% 0.0433 →
0.044, 0.043
TreeDist::RobinsonFoulds(forest201.80) ⚪ NSD -5.09% 14.9 →
15.4, 16.3
TreeDist::RobinsonFoulds(forest21.888) ⚪ NSD -3.93% 3.14 →
3.26, 3.27
TreeTools:::path_lengths(tr80$edge, tr80$edge.length, FALSE) ⚪ NSD 0.29% 0.0913 →
0.0908, 0.0912
TreeTools:::postorder_order(bal40) ⚪ NSD -3.1% 0.00161 →
0.00167, 0.00164
TreeTools:::postorder_order(bal40k) ⚪ NSD -0.56% 0.44 →
0.445, 0.441
TreeTools:::postorder_order(dbal40) ⚪ NSD -1.75% 0.00171 →
0.00174, 0.00173
TreeTools:::postorder_order(dbal40k) ⚪ NSD 1.07% 2.14 →
2.1, 2.13
TreeTools:::postorder_order(dpec40) ⚪ NSD -0.79% 0.00252 →
0.00254, 0.00254
TreeTools:::postorder_order(dpec40k) 🟣 ~same 2.12% 3360 →
3280, 3290
TreeTools:::postorder_order(drnd80) ⚪ NSD -1.24% 0.00396 →
0.004, 0.00401
TreeTools:::postorder_order(nbal40) ⚪ NSD -0.97% 0.00206 →
0.00209, 0.00208
TreeTools:::postorder_order(nbal40k) ⚪ NSD 1.34% 2.19 →
2.16, 2.16
TreeTools:::postorder_order(npec40) ⚪ NSD -1.39% 0.00281 →
0.00284, 0.00284
TreeTools:::postorder_order(npec40k) 🟣 ~same 3.54% 3440 →
3310, 3320
TreeTools:::postorder_order(nrnd80) ⚪ NSD -0.44% 0.00454 →
0.00454, 0.00458
TreeTools:::postorder_order(pec40) ⚪ NSD 0.61% 0.00165 →
0.00165, 0.00163
TreeTools:::postorder_order(pec40k) ⚪ NSD -0.33% 0.434 →
0.435, 0.435
TreeTools:::postorder_order(rnd80) ⚪ NSD 0.48% 0.00207 →
0.00205, 0.00208

@ms609 ms609 marked this pull request as ready for review February 4, 2026 16:50
@ms609
Copy link
Owner

ms609 commented Feb 4, 2026

@copilot Good start thank you; I had to make some improvements to get this going.

Now I'm seeing:

Failure (test-ClusterTable.R:140:3): ClusterTable with complex trees
Expected `as.numeric(RobinsonFoulds(r4))` to equal `c(8, 0, 8, 8, 0, 8)`.
Differences:
  `actual`: 8.0 8.0 8.0 8.0 8.0 8.0
`expected`: 8.0 0.0 8.0 8.0 0.0 8.0

Here's the test that's triggering it.
I never did understand why the script was working with const but not constexpr. Perhaps you can look into this. The TreeDist source is available in the ms609/TreeDist GitHub repo, or via CRAN; let me know if you need specific files copying here.

test_that("ClusterTable with complex trees", {
  skip_if_not_installed("TreeDist", "2.9.2.9000")
  library("TreeDist")
  
  # Test exposes failures in C++ - constexpr not playing nicely with Rcpp
  # Specifically if replacing
  # const int16 L_COL = int16(0);
  # const int16 R_COL = int16(1);
  # const int16 X_COLS = int16(2);
  # hard-coding, using enum all fails.
  tr1 <- structure(list(
    edge = structure(c(8L, 8L, 9L, 10L, 10L, 9L, 11L, 11L, 8L, 12L, 12L,
                       1L, 9L, 10L, 2L, 3L, 11L, 4L, 5L, 12L, 6L, 7L),
                     dim = c(11L, 2L)),
    Nnode = 5L, tip.label = c("t1", "t2", "t3", "t4", "t5", "t6", "t7")),
    class = "phylo", order = "preorder")
  tr2 <- structure(list(
    edge = structure(c(8L, 9L, 10L, 10L, 9L, 11L, 11L, 8L, 12L, 12L, 8L,
                       9L, 10L, 1L, 2L, 11L, 3L, 4L, 12L, 5L, 6L, 7L),
                     dim = c(11L, 2L)),
    Nnode = 5L, tip.label = c("t1", "t2", "t3", "t4", "t5", "t6", "t7")),
    class = "phylo", order = "preorder")
  t4 <- list(a = tr1, b = tr2, c = tr1, d = tr2)
  r4 <- RootTree(t4, 1)
  
  expect_equal(as.numeric(RobinsonFoulds(r4)), c(8, 0, 8, 8, 0, 8))
})

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