Selected work · 01 / 08Numerical optimization

High-Performance Mehrotra Lasso Solver

A custom interior-point solver for constrained Lasso, derived from the KKT conditions and written from scratch. It exploits the structure of the problem to run up to 264.9× faster than general-purpose solvers, with near-identical solutions.

Mehrotra, live. Click for a new problem.iteration 0
iteratespredictorbudget ‖x‖₁ ≤ tloss contours
264.9×faster than CVXPY on California Housing
7–12Newton iterations on three of four datasets
<10−7distance from the CVXPY solution on real data
9 / 9true features recovered among 2,000
148ksamples in the largest dataset, IMDb
01 / The problem

Sparse regression with a hard budget.

Lasso regression fits a linear model while keeping only the features that matter: its ℓ₁ constraint pushes the weights of irrelevant features to exactly zero. In the constrained form, the budget t is a hard limit on the total size of the coefficients, so the model can be controlled directly.

minx∈ℝn12‖Ax−b‖22subject to‖x‖1≤t
Constrained Lasso

Two things make it harder than it looks. The ℓ₁ norm is not differentiable, so plain gradient methods do not apply. And general-purpose solvers such as OSQP and SCS see it as just another quadratic program, ignoring the structure that could make it fast.

02 / The idea

Remove the kink, keep the structure.

Writing every coefficient as the difference of two non-negative parts, x=u−v, turns the ℓ₁ norm into a linear function. The problem becomes an equivalent convex quadratic program with linear constraints only:

minu,v≥012‖A(u−v)−b‖22subject to𝟏⊤(u+v)≤t
Equivalent quadratic program

From its Lagrangian I derived the dual problem and the KKT conditions. An interior-point method relaxes their complementarity conditions with a barrier parameter μ>0 and follows the central path, the curve of solutions of the perturbed system, while μ shrinks to zero:

ui(zu)i=μ,vi(zv)i=μ,stλt=μ,μ→0
The central path

The demo at the top of this page runs the same scheme on random problems with two features. The iterates start at the center of the budget and walk to the solution, which is often a corner of the diamond: one coefficient exactly zero, the sparsity that makes Lasso useful.

03 / The algorithm

Mehrotra's predictor-corrector, from scratch.

Each iteration of the solver follows five steps:

  1. Measure. Compute the primal and dual residuals and the duality measure μ.
  2. Predict. Solve a Newton system aimed straight at μ = 0, the affine-scaling direction.
  3. Correct. Set the centering parameter σk=(μaff/μk)3 from the prediction, then add the second-order terms the linearization ignored.
  4. Update. Take separate primal and dual steps, cut at 99.5% of the distance to the boundary, so every variable stays strictly positive.
  5. Monitor. Stop when residuals and μ fall below the tolerance; record step sizes, regularization and numerical events.

The expensive part is the Newton system, of size (4n + 2) × (4n + 2). Eliminating the slack and dual directions reduces it to a symmetric positive definite system of size 2n × 2n:

Mk=[A⊤A−A⊤A−A⊤AA⊤A]+Dk+λtst𝟏𝟏⊤
The reduced Newton matrix

The first block is constant and computed once, Dk is diagonal and the last term is rank one: the trace of the budget constraint. Predictor and corrector share the same matrix and differ only in the right-hand side. This is the step, taken straight from lasso_ipm_solver.py:

lasso_ipm_solver.py · predictor → corrector
# STEP 1: PREDICTOR (affine-scaling direction), a pure Newton step towards mu = 0rhs_comp_t_aff = -rc_tcorrection_from_t_aff = (rhs_comp_t_aff + lambda_t * r_p) / (s_t + eps)rhs_u_aff = -r_d1 - zu - correction_from_t_affrhs_v_aff = -r_d2 - zv - correction_from_t_affd_uv_aff = solve_linear_system_debug(np.concatenate([rhs_u_aff, rhs_v_aff]), "predictor")du_aff, dv_aff = d_uv_aff[:n], d_uv_aff[n:]ds_t_aff = -r_p - np.sum(du_aff + dv_aff)dzu_aff = (-rc_u - zu * du_aff) / (u + eps)dzv_aff = (-rc_v - zv * dv_aff) / (v + eps)dlambda_t_aff = (-rc_t - lambda_t * ds_t_aff) / (s_t + eps)# Centering parameter sigma (Mehrotra's heuristic)mu_aff = max(0, mu_aff_num / (2 * n + 1))sigma = (mu_aff / mu) ** 3 if mu > eps else 0.0# STEP 2: CORRECTOR, centering + second-order correctionrc_corr_u, rc_corr_v, rc_corr_t = du_aff * dzu_aff, dv_aff * dzv_aff, ds_t_aff * dlambda_t_affrhs_comp_t_corr = sigma * mu - rc_t - rc_corr_tcorrection_from_t_corr = (rhs_comp_t_corr + lambda_t * r_p) / (s_t + eps)rhs_u_corr = -r_d1 - (rc_u + rc_corr_u - sigma * mu) / (u + eps) - correction_from_t_corrrhs_v_corr = -r_d2 - (rc_v + rc_corr_v - sigma * mu) / (v + eps) - correction_from_t_corrd_uv = solve_linear_system_debug(np.concatenate([rhs_u_corr, rhs_v_corr]), "corrector")
Excerpt from the solver, with the debug checks removed. The full method, from initialization to the final coefficients, is Algorithm 1 in the report.
Show the full pseudocode Algorithm 1
Algorithm 1: Mehrotra's predictor-corrector interior-point method for the Lasso QP, from the initial point to the final coefficients
04 / Robustness

Built not to break near the solution.

Close to the optimum the barrier terms explode and Mk becomes ill-conditioned: a black-box call such as numpy.linalg.solve can stop on a singular matrix. The solver has two engines and a safety net:

  • Cholesky factorization for small and medium problems: O(n³) per factorization, then two cheap triangular solves.
  • Matrix-free conjugate gradient for high-dimensional problems, with a diagonal (Jacobi) preconditioner. Mk is never formed: it is applied to vectors on the fly, which reduces the memory footprint.
  • Adaptive regularization. A small δI is added to the system: it grows tenfold after a failed solve and decays by 1.5× after every successful iteration, so the original Newton system is perturbed only when it has to be.
lasso_ipm_solver.py · solve_linear_system_debug()
# DIRECT METHOD (Cholesky, O(n^3)): M = G_qp + D_k + lambda/s * 11^T + delta * IM = G_qp + np.diag(diag_Dk_vec) + lambda_s_ratio * np.outer(e_2n, e_2n)current_reg = max(delta, base_reg_factor)M = M + current_reg * np.eye(2 * n)L, lower = cho_factor(M, lower=False, check_finite=False)solution = cho_solve((L, lower), rhs)# ITERATIVE METHOD (matrix-free CG): M is applied to vectors, never formeddef matvec(p):    """Matrix-vector product for the KKT system."""    p_u, p_v = p[:n], p[n:]    common_term = AtA_base @ (p_u - p_v)    res_g = np.concatenate([common_term, -common_term])    res_d = diag_Dk_vec * p    res_rank1 = (lambda_s_ratio * e_2n.dot(p)) * e_2n    res_delta = current_reg * p    return res_g + res_d + res_rank1 + res_deltaM_op = LinearOperator((2 * n, 2 * n), matvec=matvec)solution, info = cg(M_op, rhs, rtol=cg_rtol, maxiter=cg_maxiter, M=precond)# a failed solve multiplies delta by 10 and retries; every successful iteration lets it decay:delta = max(initial_delta, delta / 1.5)
The reduced system in code: the same three terms as the formula above, built explicitly for Cholesky or applied on the fly for conjugate gradient. Retry logic and checks removed.
Where it mattered. On the 2,000-feature benchmark, conjugate gradient stalled at loose budgets. δ rose from 10⁻⁸ up to 10⁻¹, the system became solvable again, and the solver still converged in 12 iterations, 44.4× faster than CVXPY.
05 / Benchmark

Same problem, fair comparison.

Libraries solve different versions of the Lasso, so a fair benchmark needs one problem for everyone. I fitted scikit-learn's penalized Lasso (α = 0.1), took the ℓ₁ norm of its solution as the budget t, and gave that same t to my solver and to CVXPY, with OSQP first and SCS as a fallback, both at tolerance 10⁻⁸. The two forms describe the same solution when λ=mα, a scaling derived in the report.

DatasetTrainingTestFeaturesκ(AᵀA)
Diabetes331111104.18 × 10²
California Housing15,4805,16084.30 × 10¹
IMDb111,32437,108193.59 × 10⁴
Synthetic2,2507502,0001.04 × 10³

Four datasets from small to high-dimensional: 75/25 split with fixed seeds, standardized on training statistics only. The synthetic set hides 9 informative features among 2,000.

Runtime, on a log scale

Wall-clock time for one solve at the common budget, from Table 5 of the report. OSQP solved Diabetes; on the other three datasets it failed or declared the problem infeasible, and SCS took over.

The same answer

DatasetMy iterationsCVXPY iterations‖x − xCVXPY‖₂Active features
Diabetes71,4251.08 × 10⁻⁸4 / 10
California Housing108752.37 × 10⁻⁸2 / 8
IMDb2001,9753.48 × 10⁻⁸3 / 19
Synthetic124,9251.16 × 10⁻⁷8 / 2,000

Both solvers select exactly the same features, and test MSE, MAE and R² are identical on every dataset. On the synthetic set, all 9 true features are recovered along the regularization path, with no false positives at the best budget.

06 / Convergence

Few iterations, fast finish.

I swept ten budgets per dataset, from 1% to 150% of the unconstrained ℓ₁ norm. The duality gap follows the pattern the theory predicts: a centering phase, a transition, then a superlinear or quadratic finish. Of the 40 runs, 45% end quadratically and 37.5% superlinearly.

Duality gap versus iteration, log scale, for seven budgets on each of the four datasets: most curves fall below the 1e-8 tolerance within 6 to 12 iterations
Duality gap per iteration, log scale, for seven budgets on each dataset. The red dashed line is the 10⁻⁸ tolerance: most runs cross it within 6 to 12 iterations.
Left: iterations versus normalized budget, stable around 7 to 13 except IMDb at tight budgets. Right: speedup versus CVXPY across budgets, above 1 everywhere
Iterations and speedup across the budget sweep. The solver stays faster than CVXPY at every budget tested (the 2,000-feature set is left out of this sweep). IMDb, the worst-conditioned dataset (κ ≈ 3.6 × 10⁴), is the hard case: tight budgets push it to 1,000 iterations.

Every derivation is in the report.

Lagrangian and dual problem, convergence and complexity analysis, regularization paths and all the tables behind this page.

Next case study · 02 / 08IMDb: Advanced Data Mining, Explainable AI & Time Series