Getting Faster Results

Author

Anqi Fu and Balasubramanian Narasimhan

Introduction

As was remarked in the introduction to CVXR, its chief advantage is flexibility: you can specify a problem in close to mathematical form and CVXR solves it for you, if it can. Behind the scenes, CVXR compiles the domain specific language and verifies the convexity of the problem before sending it off to solvers. If the problem violates the rules of Disciplined Convex Programming it is rejected.

Therefore, it is generally slower than tailor-made solutions to a given problem.

Advanced: Directly Calling the Solver

If DPP is not sufficient for your performance needs, you can bypass the DSL entirely and call the solver directly. This avoids all overhead from CVXR’s compilation and DCP verification on each solve, but requires working with low-level solver data structures.

CVXR provides a three-step public API for this, mirroring the CVXPY interface:

  1. problem_data() — compile the problem into solver-ready data
  2. solve_via_data() — call the solver on the compiled data
  3. problem_unpack_results() — invert the compilation chain and populate variable values
Warning

We recommend checking DCP-compliance at least once (via is_dcp(prob)) before bypassing verification. Not verifying DCP-compliance may result in garbage!

An Example

To understand the speed issues, let us consider the global warming data from the Carbon Dioxide Information Analysis Center (CDIAC) again. The data points are the annual temperature anomalies relative to the 1961–1990 mean. We will fit the nearly-isotonic approximation βRm by solving

Minimizeβ12i=1m(yiβi)2+λi=1m1(βiβi+1)+, where λ0 is a penalty parameter and x+=max(x,0).

This can be solved as follows.

suppressMessages(suppressWarnings(library(CVXR)))
data(cdiac)
y <- cdiac$annual
m <- length(y)
lambda <- 0.44
beta <- Variable(m)
obj <- 0.5 * sum((y - beta)^2) + lambda * sum(pos(diff(beta)))
prob <- Problem(Minimize(obj))
soln <- psolve(prob, solver = "CLARABEL")
check_solver_status(prob)
betaHat <- value(beta)

This is the recommended way to solve a problem. After solving, you can inspect timing via solver_stats():

stats <- solver_stats(prob)
stats
SolverStats(solver=CLARABEL, time=0.0011, iters=10)

The SolverStats object reports the solver’s own wall-clock time, setup time, and iteration count.

However, suppose we wished to construct bootstrap confidence intervals for the estimate using 100 resamples. It is clear that this computation time can quickly become limiting.

Below, we show how one can get at the problem data and directly call a solver to get faster results.

Profile the Code

Profiling a single fit to the model is useful to figure out where most of the time is spent.

library(profvis)
y <- cdiac$annual
profvis({
    beta <- Variable(m)
    obj <- Minimize(0.5 * sum((y - beta)^2) + lambda * sum(pos(diff(beta))))
    prob <- Problem(obj)
    soln <- psolve(prob, solver = "CLARABEL")
    check_solver_status(prob)
    betaHat <- value(beta)
})
Flame Graph
Data
Options ▾
(Sources not available)
.maincompiler:::tryCmpfuncmpfungenCodecmpcmpCalltryInlinehcmpcmpCalltryInlinehcmpcmpForBodycmpcmpCalltryInlinehhanyDotsfunEnvlapplyFUNlapplyFUNgenCodecmpcmpCalltryInlinehcmpcmpCalltryInlinehcmpcmpCalltryInlinehcmpfunEnvlapplyFUNlapplyas.listcmpfungenCodecmpcmpCalltryInlinehcmpcmpCalltryInlinehcmpcmpCalltryInlinehcmp0102030405060708090100

It is especially instructive to click on the data tab and open up the tree for psolve to see the sequence of calls and cumulative time used.

Directly Calling the Solver

We are mathematically certain that the above is convex and so we can avoid the repeated DCP checks. The three steps are:

Step 1. Extract the compiled problem data using problem_data(). This returns the low-level solver data, the compilation chain, and inverse data needed to reconstruct the solution.

prob_data <- problem_data(prob, solver = "CLARABEL")

Step 2. Call the solver directly via solve_via_data() on the compilation chain. This bypasses DCP verification entirely.

solver_output <- solve_via_data(prob_data$chain, prob_data$data,
                                warm_start = FALSE, verbose = FALSE,
                                solver_opts = list())

Step 3. Unpack the solver results back into the problem’s variables using problem_unpack_results(). This reverses the compilation chain and populates value(beta) etc.

problem_unpack_results(prob, solver_output,
                       prob_data$chain, prob_data$inverse_data)

Profile the Direct Call

We can profile this direct call now.

profvis({
    beta <- Variable(m)
    obj <- Minimize(0.5 * sum((y - beta)^2) + lambda * sum(pos(diff(beta))))
    prob <- Problem(obj)
    prob_data <- problem_data(prob, solver = "CLARABEL")
    solver_output <- solve_via_data(prob_data$chain, prob_data$data,
                                    warm_start = FALSE, verbose = FALSE,
                                    solver_opts = list())
    problem_unpack_results(prob, solver_output,
                           prob_data$chain, prob_data$inverse_data)
})
Flame Graph
Data
Options ▾
(Sources not available)
.maincompiler:::tryCmpfuncmpfungenCodecmpcmpCalltryInlinehcmpcmpCalltryInlinehcmpForBodycmpcmpCalltryInlineh.External202468101214161820

Same Answer?

Of course, we should also verify that the results obtained in both cases are the same.

identical(betaHat, value(beta))
[1] TRUE

Session Info

R version 4.5.2 (2025-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Tahoe 26.3

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Los_Angeles
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] profvis_0.4.0 CVXR_1.8.1   

loaded via a namespace (and not attached):
 [1] slam_0.1-55       cli_3.6.5         knitr_1.51        ECOSolveR_0.6.1  
 [5] rlang_1.1.7       xfun_0.56         clarabel_0.11.2   gurobi_13.0-1    
 [9] otel_0.2.0        Rglpk_0.6-5.1     highs_1.12.0-3    cccp_0.3-3       
[13] scs_3.2.7         S7_0.2.1          jsonlite_2.0.0    Rcplex_0.3-8     
[17] backports_1.5.0   Rmosek_11.1.1     rprojroot_2.1.1   htmltools_0.5.9  
[21] gmp_0.7-5.1       piqp_0.6.2        rmarkdown_2.30    grid_4.5.2       
[25] evaluate_1.0.5    fastmap_1.2.0     yaml_2.3.12       compiler_4.5.2   
[29] codetools_0.2-20  htmlwidgets_1.6.4 Rcpp_1.1.1        here_1.0.2       
[33] osqp_1.0.0        lattice_0.22-9    digest_0.6.39     checkmate_2.3.4  
[37] Matrix_1.7-4      tools_4.5.2      

References