Solution-Map Layer (SolMapLayer)¶
SolMapLayer evaluates the solution map
\(\hat z^\star=\mathcal{O}(\hat y)\) and connects the returned primal variables
to PyTorch autograd. Here, \(\hat y\) denotes the parameters supplied to the
optimization problem and \(\hat z^\star\) denotes the corresponding optimal
decisions. Layer remains an exact alias for backward compatibility:
from diffapqp import Layer, SolMapLayer
assert SolMapLayer is Layer
Accepted problems¶
The constructor accepts an ordinary cvxpy.Problem when it can be
canonicalized to a standard APQP/APLP problem with all of the following
properties:
- a minimization problem with at least one named parameter and one named decision variable
- DCP and QP compliant, and DPP compliant when parameters are present
- continuous variables only
- a constant positive-semidefinite quadratic matrix \(P\)
- constant equality and inequality coefficient matrices \(A\) and \(G\)
- parameters entering affinely through the linear objective, objective offset, or constraint right-hand sides
DiffAPQP rejects parameterized \(P\), \(A\), or \(G\), and formulations for which CVXPY canonicalization introduces, removes, or resizes decision variables. Names should therefore be unique and should be assigned explicitly to every parameter and variable used through the public dictionaries. See APQP and Canonicalization for the supported APQP/APLP form.
No manual standardization required
Write the model naturally with CVXPY and pass the original
cvxpy.Problem to SolMapLayer. DiffAPQP performs the compatible
canonicalization and standard-form extraction internally.
Constructor¶
SolMapLayer(
problem,
max_n_cores=os.cpu_count(),
regularization_eps=0.0,
act_ineq_tol=None,
act_ineq_tol_rel=None,
)
problem : cvxpy.Problem
: Supported continuous APQP/APLP problem described above.
max_n_cores : int, default os.cpu_count()
: Maximum number of outer forward and backward sample workers. 1 selects
serial execution and -1 is converted to os.cpu_count(). Requested worker
counts are capped at the CPU count reported by the operating system. This
setting does not establish CPU affinity.
regularization_eps : float, default 0.0
: Nonnegative coefficient used to replace the supplied objective
\(f_0(z,\hat y)\) with
This can select a better-conditioned or unique solution, especially for an LP, but the returned solution \(\hat z_\epsilon^\star\) and its derivative then belong to the regularized problem. See Quadratic regularization for its mathematical interpretation.
act_ineq_tol : float | None, default None
: Nonnegative absolute tolerance used to identify active inequalities for the
reduced-KKT adjoint.
act_ineq_tol_rel : float | None, default None
: Nonnegative relative tolerance used to identify active inequalities for the
reduced-KKT adjoint.
See Adjoint configuration for more details.
Construct a layer once and reuse it across calls. Construction canonicalizes the
problem and creates the configured forward worker pool. Linux retains the
fork-based backward process pool used by the benchmarked implementation.
Windows and macOS use a shared-memory thread pool for parallel adjoint solves,
avoiding process startup and problem-data serialization. The thread backend
does not require an if __name__ == "__main__": guard.
Persistent worker pools can be released explicitly with layer.close().
SolMapLayer can also be used as a context manager when its lifetime is
limited to a block.
Forward call¶
primal_dict, dual_eq, dual_ineq, raw_solutions, time_batch = layer(
param_dict,
idx_list=None,
warm_start=False,
update=False,
solver="OSQP",
verbose=False,
solver_args={},
reduced_kkt=False,
mode=None,
mode_args={},
)
Batched parameters¶
param_dict maps every original CVXPY parameter name to a floating-point
torch.Tensor. Every tensor must have the same leading batch dimension.
Mathematically, one batch row supplies a value of \(\hat y\). DiffAPQP
canonicalizes that instance, solves for the standard-form decision
\(\tilde z^\star\), and retrieves the original named decisions
\(\hat z^\star=\mathcal{R}(\tilde z^\star)\) in primal_dict.
| Original CVXPY shape | Accepted batched shape |
|---|---|
scalar () |
(batch,) or (batch, 1) |
vector (n,) |
(batch, n) |
matrix (m, n) |
(batch, m, n) |
| higher-dimensional shape | (batch, *parameter.shape) |
Matrix and higher-dimensional values are flattened internally using CVXPY's column-major convention and restored to their original shapes during backward. The current solver path is CPU based: inputs are converted to NumPy for CVXPY, and public outputs are CPU objects. Use a consistent floating dtype for all parameters.
The first forward call creates one reusable CVXPY problem per batch slot and therefore establishes the largest supported batch size for that layer instance. Later calls may use the same or a smaller batch, but should not exceed the first batch size.
Forward options¶
idx_list : list[int] | None, default None
: Stable dataset ids corresponding one-to-one with the current batch. It is
required when warm_start=True for OSQP, SCS, QPALM, or PROXQP and must have
the same length and order as the batch.
warm_start : bool, default False
: Request a warm start. For the custom solver paths, DiffAPQP stores
solver-native iterates by sample id after each successful call.
update : bool, default False
: Request in-place solver-data updates. The first call for a batch slot builds
the solver workspace; update becomes effective on later calls.
solver : str, default "OSQP"
: Case-insensitive CVXPY solver name.
verbose : bool, default False
: Forward solver verbosity.
solver_args : dict, default {}
: Solver-specific options. Values supplied here override DiffAPQP defaults key
by key. See Solvers for the configured profiles.
reduced_kkt : bool, default False
: Use the reduced active-set KKT adjoint instead of the full complementarity
KKT adjoint.
mode : {None, "lsqr", "spsolve", "minres"}, default None
: Linear solver used by the backward adjoint.
mode_args : dict, default {}
: Keyword arguments overriding the selected adjoint solver defaults.
See Adjoint configuration for more details.
Warm-start and update lifecycle¶
DiffAPQP's explicit sample-indexed cache and in-place update hooks cover OSQP, SCS, QPALM, and PROXQP when the custom CVXPY fork is installed. The distinction between sample-indexed warm starts and slot-indexed solver-data updates is explained in the forward-pass theory.
- On the first call with
warm_start=True, cache entries are empty, so the solve initializes both the workspace and the per-sample iterates. - A later call with the same
idx_listreuses the iterates associated with each sample id. update=Truereuses a batch slot's solver workspace after that slot has completed its first solve.- Update-only calls do not require
idx_list;idx_listis needed for the sample-indexed warm-start cache.
For other solvers, warm_start is passed through the standard CVXPY interface;
idx_list is not used and update does not activate a DiffAPQP-specific data
update path. See Installation
for the custom fork.
Adjoint configuration¶
mode=None chooses a method from the KKT formulation:
See Solution-Map Differentiation for the full and reduced systems and their adjoint equations.
| KKT formulation | Default mode | Default mode arguments |
|---|---|---|
Full KKT (reduced_kkt=False) |
lsqr |
atol=1e-6, btol=1e-6, iter_lim=20000, show=False |
Reduced KKT (reduced_kkt=True) |
minres |
rtol=1e-9 |
spsolve is available for either formulation and has no DiffAPQP-specific
default arguments. minres is accepted only with reduced KKT. Using lsqr
with reduced KKT is allowed but emits a warning because minres is normally
preferred for that symmetric system.
When constructor tolerances are None, active-set tolerances are inferred from
values explicitly supplied in solver_args. DiffAPQP checks
eps_abs, tol_gap_abs, tol_feas, or tol_feas_abs for an absolute solver
tolerance and eps_rel, tol_gap_rel, or tol_feas_rel for a relative
tolerance. For either component \(\bullet\in\{\mathrm{abs},\mathrm{rel}\}\),
when a matching solver tolerance is supplied, and \(\epsilon_\bullet=10^{-4}\) otherwise. Explicit constructor values take precedence. For a sample with canonical solution \(\tilde z^\star\), DiffAPQP identifies the active set
Only losses constructed from primal_dict are differentiated by
SolMapLayer; derivatives with respect to returned dual variables are not
supported.
Returns¶
primal_dict : dict[str, torch.Tensor]
: Autograd-connected retrieved decisions \(\hat z^\star\), keyed by original
CVXPY variable name. A variable with original shape shape is returned with
shape (batch, *shape); scalar variables have shape (batch,).
dual_eq : numpy.ndarray, shape \((\mathrm{batch},n_{\mathrm{eq}})\)
: Canonical equality duals \(\tilde\nu^\star\). This output is not autograd
connected.
dual_ineq : numpy.ndarray, shape \((\mathrm{batch},n_{\mathrm{in}})\)
: Canonical inequality duals \(\tilde\lambda^\star\). This output is not
autograd connected.
raw_solutions : list
: One solver-native result object per sample. Its type and fields depend on the
selected CVXPY solver interface and should be treated as backend specific.
time_batch : dict
: Batch wall-time measurements and per-sample pipeline timings:
{
"canon_time": float,
"solve_time": float,
"sample_times": [
{
"worker_total_time": float,
"get_problem_data_time": float,
"solve_via_data_time": float,
"unpack_time": float,
},
...
],
}
sample_times contains one dictionary per sample in batch order:
| Field | Meaning |
|---|---|
worker_total_time |
Wall time inside the solve worker, covering solve_via_data, result unpacking, status checks, and small Python overhead. It excludes get_problem_data_time. |
get_problem_data_time |
Wall time for CVXPY's get_problem_data() call, which prepares the solver data and reduction-chain metadata for that sample during the serial preparation phase. |
solve_via_data_time |
Wall time for CVXPY's solve_via_data() call, including solver-interface overhead and the backend's native optimization solve. |
unpack_time |
Wall time for CVXPY's unpack_results() call, which maps the raw solver result back to the standardized CVXPY problem and populates primal, dual, status, and objective values. |
canon_time is the serial wall time used to prepare CVXPY data for all
samples. solve_time is the wall time of the serial or parallel solve phase,
not the sum of sample times.
Errors and warnings¶
ValueErroris raised for unsupported problems, negative constructor tolerances, missing parameters, invalid parameter types or shapes, inconsistent batch sizes, incompatible canonical variables, failed solver status, ormode="minres"with full KKT.AssertionErroris raised when a custom-path warm start is requested withoutidx_list.optimal_inaccurateis accepted with a warning; other non-optimal statuses raiseValueError.- A
UserWarningreports custom warm-start/update requests when the custom CVXPY fork is not detected. - A
UserWarningis emitted formode="lsqr"with reduced KKT.
Example¶
import cvxpy as cp
import torch
from diffapqp import SolMapLayer
x = cp.Variable(2, name="x")
q = cp.Parameter(2, name="q")
problem = cp.Problem(
cp.Minimize(0.5 * cp.sum_squares(x) + q @ x),
[x >= 0, x <= 10],
) # no standardization required
layer = SolMapLayer(problem, max_n_cores=1)
q_batch = torch.tensor(
[[-1.0, -2.0], [0.5, -1.5]],
dtype=torch.double,
requires_grad=True,
)
primal_dict, *_ = layer(
{"q": q_batch},
solver="OSQP",
reduced_kkt=True,
)
loss = primal_dict["x"].square().mean()
loss.backward()
In this example, q_batch supplies \(\hat y\), while primal_dict["x"]
contains the corresponding retrieved decisions \(\hat z^\star\). The Python
names follow the original CVXPY model and need not match the mathematical
symbols.
Repeated warm-start/update calls:
sample_ids = [0, 1]
# First call initializes workspaces and sample caches.
layer(
{"q": q_batch},
idx_list=sample_ids,
warm_start=True,
update=True,
solver="OSQP",
reduced_kkt=True,
)
# Later calls with the same ids can reuse both.
primal_dict, *_ = layer(
{"q": q_batch},
idx_list=sample_ids,
warm_start=True,
update=True,
solver="OSQP",
reduced_kkt=True,
)
See also¶
- Installation
- Solvers
- Value-Function Layer
- API Overview
- Efficient Forward Pass
- Solution-Map Differentiation Theory
- Numerical Considerations