Skip to content

fix(profile): stop MaddisonSlatkin profile scoring hanging on macOS (arm64)#272

Merged
ms609 merged 6 commits into
cpp-searchfrom
claude/fix-maddisonslatkin-arm64-hang
Jul 12, 2026
Merged

fix(profile): stop MaddisonSlatkin profile scoring hanging on macOS (arm64)#272
ms609 merged 6 commits into
cpp-searchfrom
claude/fix-maddisonslatkin-arm64-hang

Conversation

@ms609

@ms609 ms609 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Problem

R CMD check --run-donttest hangs for 6 hours (the GitHub ceiling) on macOS, intermittently taking the ubuntu R 4.1 leg instead. The hang is in checking examples with --run-donttest, on the pre-existing TreeLength(..., concavity = "profile") example in tree_length.R (dataset inapplicable.phyData[[1]] = Agnarsson2004, 62 tips).

Root cause

PrepareDataProfile()StepInformation() runs the exact MaddisonSlatkin recursion for 3–5 token characters. Its memoization uses fixed-capacity open-addressing maps (OAFlatMap: logB_cache, logPVec_idx) reserved on the assumption that a character never needs more entries than reserved.

A skewed 3-state character has a low split-count, so the feasibility gate classes it feasible for the exact solver — but on many tips it generates far more distinct (token, leaves) keys than reserved. Agnarsson2004 column 83 is [42, 9, 2]: ~9000 keys vs logPVec_idx's 4096-entry reserve. Once the table fills, probe_slot() — which stops only on an empty slot — loops forever, and that loop lives outside LogB/LogPVec, so the existing 2-second wall-clock budget cannot interrupt it.

It's a race the platform decides:

  • x86-64 (windows, ubuntu): the 2 s budget usually fires first → returns NA → the R caller falls back to the Monte Carlo approximation → completes. The blowup is invisible.
  • Apple Silicon (arm64): the table fills before 2 s elapses → infinite probe_slothang.

This is pre-existing and independent of any feature work; it surfaced as a CI timeout because the profile example exercises exactly such a character.

Fix

Add OAFlatMap::at_capacity() (true at load factor 0.5 — the reserved size) and, alongside the existing time-budget guard in LogB()/LogPVec(), bail to the NA → Monte-Carlo fallback the instant a memo table reaches its reserved capacity.

Bailing at the reserve keeps entries_ within its reserved storage, so the table can neither fill (no probe_slot infinite loop) nor reallocate (which would also have dangled the FixedProbList* held across recursion) — fixing both the hang and a latent dangling-pointer UB in the same blowup case. Characters within reserve are unaffected (exact result unchanged); over-reserve characters were already broken (UB/hang) and now return the correct MC approximation.

Verification

  • macOS arm64 (the platform that hung): the [42, 9, 2] character now completes in 0.91 s (was a 6 h hang) with the capacity warning; full profile TreeLength(Agnarsson2004) completes in 14 s. Confirmed green on macOS-latest, ubuntu-24.04 (R 4.1) and windows-latest.
  • x86-64: all profile/parsimony tests pass (MaddisonSlatkin 56, pp-info 131, ts-profile 43, tree_length 75, iw-profile 15 — 0 failures), including a new regression test asserting the overflowing character falls back instead of hanging.

🤖 Generated with Claude Code

ms609 and others added 4 commits July 11, 2026 20:12
…ters

PrepareDataProfile() -> StepInformation() runs the exact MaddisonSlatkin
recursion for 3-5 token characters. Its memoisation uses fixed-capacity
open-addressing maps (OAFlatMap: logB_cache, logPVec_idx) reserved on the
assumption that a character never needs more entries than reserved. Large
multistate characters on many tips violate that: e.g. a 3-token [42,9,2]
character on 62 tips generates ~9000 distinct (token, leaves) keys, far above
logPVec_idx's 4096-entry reserve. Once such a table fills, probe_slot() -- which
stops only on an empty slot -- loops forever, and that loop lives outside
LogB/LogPVec so the 2 s wall-clock budget cannot interrupt it.

On x86-64 the time budget usually wins the race (bails to NA, and the R caller
falls back to the Monte Carlo approximation), so the blowup is invisible. On
Apple Silicon the table fills before 2 s elapses and the search hangs
indefinitely -- reproduced as a 6 h CI timeout in "checking examples with
--run-donttest" for TreeLength(concavity = "profile") on Agnarsson2004
(inapplicable.phyData[[1]]).

Fix: add OAFlatMap::at_capacity() (true at load factor 0.5, i.e. the reserved
size) and, alongside the existing time-budget guard in LogB()/LogPVec(), bail to
the NA -> Monte-Carlo fallback the moment a memo table reaches its reserved
capacity. Bailing at the reserve keeps entries_ within its reserved storage, so
the table can neither fill (no probe_slot infinite loop) nor reallocate (which
would also have dangled the FixedProbList* held across recursion) -- fixing both
the hang and a latent dangling-pointer UB in the same blowup case. Characters
within reserve are unaffected (exact result unchanged); over-reserve characters
were already broken (UB/hang) and now return the correct MC approximation.

Verified on x86-64: the [42,9,2] character now warns and completes in ~1.5 s;
profile TreeLength(Agnarsson2004) completes; all profile/parsimony tests pass
(MaddisonSlatkin 53, pp-info 131, ts-profile 43, tree_length 75, iw-profile 15).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert StepInformation() on a skewed 3-state character that overflows the exact
solver's memo tables (the inapplicable Agnarsson2004 col-83 pattern [42,9,2])
falls back to the Monte Carlo approximation with a warning instead of hanging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ove before merge)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n macOS arm64)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ms609 added a commit that referenced this pull request Jul 11, 2026
…by PR #272)

The macOS/ubuntu --run-donttest hang was diagnosed to a pre-existing profile-
parsimony (MaddisonSlatkin) infinite loop, fixed separately on cpp-search in
PR #272. This throwaway probe lane has served its purpose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ms609 and others added 2 commits July 11, 2026 21:48
… race

Which fallback fires -- the capacity guard or the 2 s time budget -- is timing
dependent (fast machine hits capacity first, slow machine hits 2 s first), so
asserting the specific cache warning flaked on CI (passed once, failed on
re-run). Assert the anti-hang property (completes, finite) plus that some
fallback warning containing 'exceeded' fired, without pinning which guard won.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow r-conventions: no right-hand/non-local assignment (the withCallingHandlers
handler used <<- to accumulate) and camelCase variables. testthat::capture_warnings()
collects every warning with no superassignment and evaluates in-frame so si is
still bound for the completion/finite assertions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ms609 ms609 merged commit 634b02f into cpp-search Jul 12, 2026
2 checks passed
@ms609 ms609 deleted the claude/fix-maddisonslatkin-arm64-hang branch July 12, 2026 05:26
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.

1 participant