Part 1 — the xarm7 ~140x Linux slowdown: ROOT-CAUSED + FIXED
xarm7_ik.solve() was ~230x slower on Linux/OpenBLAS than macOS/Accelerate (5307 ms vs 23 ms/solve). Not build, not the rescue, not BLAS threads, not raw CPU.
Root cause: the cached-RR caches (_PRIMED_LINEARITY_MAP, _DERIVATION_CACHE) were keyed on exact DH float tuples. poe_to_dh produces values that differ in the last ~1e-12 bits across BLAS backends, so the macOS-baked key ≠ the Linux-runtime key — 55/176 lock-sample lookups missed on Linux → cached-RR returned None → jointlock fell through to the slow two_intersecting/search_1d path.
Fix (done): quantize the DH key to 1e-6 at every store/lookup site. Verified on the Linux runner: 176/176 primed, 0 misses, 38 ms/solve. Regression test added (a primed sub-chain stays found under ~1e-12 jitter). This is a real user-facing fix — every cached-RR arm on Linux was affected.
Part 2 — the real exposure: the search_1d fallback is brutal (this issue going forward)
The bug above was only catastrophic because the path it fell into is pathologically slow. Measured cost of two_intersecting/search_1d: 2725 ms/solve on macOS-M3, ~5.3 s on Linux (130x the cached-RR path on the same machine).
When is search_1d actually reached?
- Never by auto-dispatch. The dispatcher explicitly does not route to the tier-1 search solvers; any 6R that misses tier-0 goes to tier-2 RR/HP (~5 ms). (See
dispatcher.py docstring.)
- Jointlock last-resort. When the cached-RR primary sweep comes up empty for a reachable pose, jointlock re-runs with the named inner solver. For sub-chains named
two_intersecting/two_parallel (e.g. xarm7), that named solver is search_1d. With the key fix, cached-RR now hits, so this fires only on a genuine RR miss — rare, but it's the completeness backstop.
- Explicit user import (
from ssik.solvers.ikgeo import two_intersecting).
Why it's slow + incomplete
two_intersecting.solve runs a 200-sample 1-D grid search; each sample solves SP5 and Newton-refines it → ~1.8M tiny rotate/_cross3/_dot3 calls per solve. Those hot functions run as pure Python on every platform (only poe_fk, refinement are compiled .so). It's also incomplete — the 200-sample grid can miss zero crossings (known caveat; tests assert FK-match per returned solution, not exhaustive branch recovery).
Decision: harden, don't blindly remove
It IS reachable (last-resort + explicit import), so it's not dead code. But it shouldn't be a 2.7 s landmine. Options, by leverage:
- Route the jointlock last-resort to HP, not the named search solver (smallest change, biggest win).
husty_pfurner.general_6r is a strictly-better backstop: universal 6R, ~13–35 ms, complete, already cached-RR-eligible. Falling to HP instead of two_intersecting makes search_1d production-unreachable. Recommended first step.
- Compile the hot inner functions (
_rotation.rotate, _scalar3._cross3/_dot3) — pure-Python today. ~40x/call → search_1d 2.7 s → ~200 ms, and speeds up every SP-subproblem tier-0 solver too. Broad win. (Filed separately.)
- Replace the 200-sample grid with a proper root-finder in
search_1d (companion-matrix eigenvalues à la RR, or adaptive bracketing + Newton). Fixes both speed and the missed-crossing completeness caveat. Larger algebraic change.
- Then decide
two_intersecting/two_parallel's fate. After (1) they're reachable only via explicit import. Either keep them as a documented "specialized solver" API hardened by (2)+(3), or retire them entirely (and their search_1d dependency) and route everything through RR/HP/cached-RR — fewer code paths, all faster + complete.
Recommendation: (1) now (cheap; removes the production landmine), (2) as a broad perf win, then evaluate (3)/(4) based on whether the explicit-import API is worth keeping. If we conclude no one needs the standalone tier-1 solvers, retiring them is the clean endgame.
Part 1 — the xarm7 ~140x Linux slowdown: ROOT-CAUSED + FIXED
xarm7_ik.solve()was ~230x slower on Linux/OpenBLAS than macOS/Accelerate (5307 ms vs 23 ms/solve). Not build, not the rescue, not BLAS threads, not raw CPU.Root cause: the cached-RR caches (
_PRIMED_LINEARITY_MAP,_DERIVATION_CACHE) were keyed on exact DH float tuples.poe_to_dhproduces values that differ in the last ~1e-12 bits across BLAS backends, so the macOS-baked key ≠ the Linux-runtime key — 55/176 lock-sample lookups missed on Linux → cached-RR returnedNone→ jointlock fell through to the slowtwo_intersecting/search_1dpath.Fix (done): quantize the DH key to 1e-6 at every store/lookup site. Verified on the Linux runner: 176/176 primed, 0 misses, 38 ms/solve. Regression test added (a primed sub-chain stays found under ~1e-12 jitter). This is a real user-facing fix — every cached-RR arm on Linux was affected.
Part 2 — the real exposure: the
search_1dfallback is brutal (this issue going forward)The bug above was only catastrophic because the path it fell into is pathologically slow. Measured cost of
two_intersecting/search_1d: 2725 ms/solve on macOS-M3, ~5.3 s on Linux (130x the cached-RR path on the same machine).When is
search_1dactually reached?dispatcher.pydocstring.)two_intersecting/two_parallel(e.g. xarm7), that named solver issearch_1d. With the key fix, cached-RR now hits, so this fires only on a genuine RR miss — rare, but it's the completeness backstop.from ssik.solvers.ikgeo import two_intersecting).Why it's slow + incomplete
two_intersecting.solveruns a 200-sample 1-D grid search; each sample solves SP5 and Newton-refines it → ~1.8M tinyrotate/_cross3/_dot3calls per solve. Those hot functions run as pure Python on every platform (onlypoe_fk,refinementare compiled.so). It's also incomplete — the 200-sample grid can miss zero crossings (known caveat; tests assert FK-match per returned solution, not exhaustive branch recovery).Decision: harden, don't blindly remove
It IS reachable (last-resort + explicit import), so it's not dead code. But it shouldn't be a 2.7 s landmine. Options, by leverage:
husty_pfurner.general_6ris a strictly-better backstop: universal 6R, ~13–35 ms, complete, already cached-RR-eligible. Falling to HP instead oftwo_intersectingmakessearch_1dproduction-unreachable. Recommended first step._rotation.rotate,_scalar3._cross3/_dot3) — pure-Python today. ~40x/call →search_1d2.7 s → ~200 ms, and speeds up every SP-subproblem tier-0 solver too. Broad win. (Filed separately.)search_1d(companion-matrix eigenvalues à la RR, or adaptive bracketing + Newton). Fixes both speed and the missed-crossing completeness caveat. Larger algebraic change.two_intersecting/two_parallel's fate. After (1) they're reachable only via explicit import. Either keep them as a documented "specialized solver" API hardened by (2)+(3), or retire them entirely (and theirsearch_1ddependency) and route everything through RR/HP/cached-RR — fewer code paths, all faster + complete.Recommendation: (1) now (cheap; removes the production landmine), (2) as a broad perf win, then evaluate (3)/(4) based on whether the explicit-import API is worth keeping. If we conclude no one needs the standalone tier-1 solvers, retiring them is the clean endgame.