Skip to content

Multithread the order-2 likelihood kernels on the RCPP backend#10

Merged
manuhuth merged 1 commit into
mainfrom
perf/rcpp-parallel-likelihood
Jul 2, 2026
Merged

Multithread the order-2 likelihood kernels on the RCPP backend#10
manuhuth merged 1 commit into
mainfrom
perf/rcpp-parallel-likelihood

Conversation

@manuhuth

@manuhuth manuhuth commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

The RCPP backend was entirely single-threaded, and cocoReg's cores argument was dead — nothing read it. This PR parallelizes the two kernels that dominate order-2 fit time, likelihoodGP2 and likelihoodGP2cov, using C++11 std::thread (no OpenMP, no new dependencies), and makes cores live.

  • likelihoodGP2: the per-(y,z) g-vector cache — rebuilt every optimizer iteration, the dominant cost — is now built concurrently across the distinct pairs, over read-only lookup tables ensured up front.
  • likelihoodGP2cov: each observation's contribution is independent (lambda_t varies, nothing is cacheable), so per-t log-terms are computed concurrently with thread-private tables into a preallocated vector.
  • In both, the final accumulation stays serial in the original order, so results are bit-identical for any thread count — verified against a pre-change build (identical() on NLL values at 1 and 14 threads, plus fitted parameters and log-likelihoods of full fits).
  • The Poisson order-2 variants use the same kernels (eta = 0) and benefit equally. Order-1 kernels are microsecond-scale, where thread-spawn overhead would exceed any gain — they stay serial.
  • cocoReg(cores=) defaults to physical cores − 1 (mirroring the JULIA_NUM_THREADS convention from Default JULIA_NUM_THREADS to half the physical cores for the Julia backend #9), capped at 2 while _R_CHECK_LIMIT_CORES_ is set so CRAN checks stay policy-compliant.
  • Worker threads never touch the R API: the only stop() condition is checked on the main thread before spawning, and worker exceptions are re-raised as an R error after joining.
  • Allocations stay minimal: one job list / one T-length vector per call plus per-thread tables that reuse capacity; dynamic scheduling via a single atomic counter.
  • src/Makevars/Makevars.win add -pthread for the Linux/Windows toolchains.

Benchmarks

Measured on 14 physical cores (Apple Silicon), GP2 with T = 1000:

1 core 13 cores speedup
likelihoodGP2 (per call) 9.20 ms 1.43 ms 6.4x
likelihoodGP2cov (per call) 10.73 ms 1.27 ms 8.4x
cocoReg GP2 fit 6.30 s 1.09 s 5.8x
cocoReg GP2cov fit 11.44 s 1.50 s 7.6x

Test plan

  • Bit-identity harness: baseline values (NLL, fitted parameters, log-likelihoods) saved from a pre-change worktree build, compared with identical() at cores = 1 and cores = 14 — all exact matches.
  • devtools::test(): FAIL 0 / WARN 29 / PASS 18 — identical counts to the pre-change baseline.
  • R CMD check: 0 errors, 0 warnings, 1 environment-only NOTE (future file timestamps).

🤖 Generated with Claude Code

likelihoodGP2 and likelihoodGP2cov now take a cores argument and
parallelize via std::thread (no OpenMP, no new dependencies): the
no-covariate path builds the distinct-(y,z) g-vector cache concurrently
over read-only tables, the covariate path computes per-t log-terms with
thread-private tables into a preallocated vector. Accumulation stays
serial in the original order, so results are bit-identical for any
thread count (verified against a pre-change build: NLL values, fitted
parameters, and log-likelihoods all match exactly).

cocoReg's previously dead cores argument is now live and flows down to
the kernels; it defaults to physical cores - 1 (mirroring the
JULIA_NUM_THREADS convention) and is capped at 2 while
_R_CHECK_LIMIT_CORES_ is set so CRAN checks stay policy-compliant.
Worker threads never touch the R API; the only stop() condition is
checked on the main thread before spawning. Order-1 kernels are
microsecond-scale and stay serial. src/Makevars{,.win} add -pthread.

Measured on 14 physical cores (T = 1000, GP2): full cocoReg fits drop
from 6.3 s to 1.1 s (no covariates) and from 11.4 s to 1.5 s (with
covariates); single NLL calls scale 6.4x / 8.4x at 13 threads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@manuhuth manuhuth merged commit 67db28e into main Jul 2, 2026
14 checks passed
@manuhuth manuhuth deleted the perf/rcpp-parallel-likelihood branch July 2, 2026 15:00
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