Skip to content

Use ucminf() with forward differences in the clmm() submodel fits - #596

Merged
avehtari merged 10 commits into
masterfrom
clmm-ucminf
Nov 6, 2026
Merged

avehtari merged 10 commits into
masterfrom
clmm-ucminf

Conversation

@avehtari

@avehtari avehtari commented Nov 4, 2026

Copy link
Copy Markdown
Member

This PR was assisted by Claude.

Speedup

projpred fits multilevel submodels of brms::cumulative() reference models with ordinal::clmm(). clmm() has no analytic gradient, so its default optimizer nlminb() computes the gradient by finite differences. In a benchmark, 186 of the 230 objective evaluations of a submodel fit were for such gradients. With method = "ucminf" and grad = "forward" (see ordinal::clmm.control()), a fit needs about 133 evaluations.

Change

This PR makes fit_cumul_mlvl() use these settings when the submodel has a single group-level term (1 | <group>). clmm() can use ucminf() only for such models. For other models, clmm() switches to nlminb() with a warning, so the PR keeps nlminb() for them. A method or control that the user gives in ... replaces the default. If the ucminf() fit fails or does not converge, fit_cumul_mlvl() refits with the default settings of clmm() and drops the warnings of the first fit.

check_conv_s() needs a new rule for ucminf() fits, because ucminf() never returns the nlminb() success code 0. The new rule accepts the codes 1 (small gradient), 2 (small step) and 4 (zero step in the line search). It also requires that the largest absolute gradient after the fit is below 1e-3; clmm() stores the gradient in fit$gradient. Code 4 is common with forward differences near the optimum. A probe with 300 submodel fits had 13 code-4 fits, with a median largest absolute gradient of 7e-6. The nlminb() fits that the check on master accepts had largest absolute gradients up to 1.9e-3.

Speed benchmark

The table gives run times on simulated data with 300 observations, 4 response categories, 6 predictors and one grouping factor ((1 | g)). "LOO" is cv_varsel() with validate_search = TRUE, refit_prj = TRUE, nloo = 10 and nterms_max = 4; varsel() uses nterms_max = 6. One SLURM job ran each version twice on the same node; the speedup compares the runs with the same run number. The node had a high load from other jobs, so the times vary. Two earlier jobs with intermediate commits of this branch gave speedups of 1.5 to 1.7.

call master (s) this PR (s) speedup
varsel() 624.2, 661.1 307.6, 341.9 2.0, 1.9
LOO 4091.5, 4332.5 2584.1, 1935.0 1.6, 2.2

Tests

Both versions give the same predictor rankings (full data and CV folds) and the same suggested sizes. Over all submodel sizes, the elpd differs by at most 1e-4. In the LOO run, 52 of the 21,120 ucminf() fits did not pass check_conv_s(); the refits with nlminb() passed for 26 of them. projpred's warnings about submodel fits that "might not have converged" count 26 fits with this PR and 30 fits with master.

The new file test_fit_cumul_mlvl.R checks the default and the user settings, the models without ucminf(), the refit, and the new rule of check_conv_s(). It needs no reference model fits, so it also runs on CRAN. Without this change, 5 of its expectations fail and one test stops with an error. The full test suite passes after a merge of the current master (R 4.5.3, testthat 3.3.2, 58,933 expectations).

@avehtari avehtari added the performance Performance issues (i.e., high runtime or high memory usage). label Nov 4, 2026
@avehtari
avehtari merged commit 4e01313 into master Nov 6, 2026
4 of 5 checks passed
@aloctavodia
aloctavodia deleted the clmm-ucminf branch November 6, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance issues (i.e., high runtime or high memory usage).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants