Value-Function Layer (ValueFuncLayer)¶
ValueFuncLayer evaluates the value function
\(\alpha(\hat y):=f_0(\hat z^\star,\hat y)\) and connects the returned value to
PyTorch autograd. Here, \(\hat y\) denotes the parameters supplied to the
optimization problem and \(\hat z^\star=\mathcal{O}(\hat y)\) is the
corresponding optimal decision. This layer is appropriate when the downstream
loss depends on the optimization objective value rather than on the optimizer
solution itself.
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 ValueFuncLayer. DiffAPQP performs the compatible
canonicalization and standard-form extraction internally.
Constructor¶
ValueFuncLayer(
problem,
max_n_cores=os.cpu_count(),
)
problem : cvxpy.Problem
: Supported continuous APQP/APLP problem described above.
max_n_cores : int, default os.cpu_count()
: Maximum number of outer sample workers. 1 selects serial execution and -1
is converted to os.cpu_count(). This setting does not establish CPU
affinity. For max_n_cores > 1, forward samples use a thread pool.
Construct a layer once and reuse it across calls. Construction canonicalizes the problem and, when parallel execution is enabled, creates persistent worker pools.
Platform parallelism
Forward samples use a thread pool on every supported platform. On Linux,
the current implementation retains the existing fork-based backward pool
for benchmark compatibility, although value-function differentiation does
not use it. On Windows and macOS, this unused process pool is not created,
so parallel ValueFuncLayer calls do not require a multiprocessing entry
point guard.
Persistent worker pools can be released explicitly with layer.close().
ValueFuncLayer can also be used as a context manager when its lifetime is
limited to a block.
Forward call¶
value_batch, 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={},
)
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 returns \(\alpha(\hat y)\) in value_batch and the
retrieved original 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. The current solver path is CPU based: inputs are converted to NumPy for CVXPY, and public outputs are CPU tensors or 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.
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. Their mathematical and repeated-solve motivation is described in the efficient forward pass.
- 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.
Differentiation¶
Only value_batch is autograd connected. The backward pass uses the
value-function derivative directly from the optimal primal and dual solution.
See Value-Function Differentiation for the
envelope-theorem derivation and comparison with the solution-map adjoint.
For an optimization parameter \(\hat y_i\),
where \(Q_i\), \(d_i\), \(B_i\), and \(H_i\) describe how \(\hat y_i\) enters the
canonical linear objective, objective offset, equality right-hand side, and
inequality right-hand side, respectively. The canonical equality and
inequality duals are \(\tilde\nu^\star\) and \(\tilde\lambda^\star\). PyTorch's
upstream gradient is applied sample by sample, so reductions and weighted
combinations of value_batch differentiate normally. No KKT adjoint solve is
needed.
The primal and dual outputs from ValueFuncLayer are provided for inspection
only and are not autograd connected. Use SolMapLayer when the loss depends on
the optimizer solution.
Returns¶
value_batch : torch.Tensor, shape (batch,)
: Autograd-connected value \(\alpha(\hat y)\) for each sample.
primal_dict : dict[str, torch.Tensor]
: Non-differentiable 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 : torch.Tensor, shape \((\mathrm{batch},n_{\mathrm{eq}})\)
: Non-differentiable canonical equality duals \(\tilde\nu^\star\).
dual_ineq : torch.Tensor, shape \((\mathrm{batch},n_{\mathrm{in}})\)
: Non-differentiable canonical inequality duals \(\tilde\lambda^\star\).
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. During parallel solve, nested BLAS/OpenMP threads
are limited to one to reduce oversubscription.
Errors and warnings¶
ValueErroris raised for unsupported problems, missing parameters, invalid parameter types or shapes, inconsistent batch sizes, incompatible canonical variables, or a failed solver status.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.
Example¶
import cvxpy as cp
import torch
from diffapqp import ValueFuncLayer
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],
)
layer = ValueFuncLayer(problem, max_n_cores=1)
q_batch = torch.tensor(
[[-1.0, -2.0], [0.5, -1.5]],
dtype=torch.double,
requires_grad=True,
) # no standardization required
value_batch, primal_dict, *_ = layer(
{"q": q_batch},
solver="OSQP",
)
loss = value_batch.mean()
loss.backward()
assert q_batch.grad.shape == q_batch.shape
assert not primal_dict["x"].requires_grad
In this example, q_batch supplies \(\hat y\), value_batch contains
\(\alpha(\hat y)\), and primal_dict["x"] contains the 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",
)
# Later calls with the same ids can reuse both.
value_batch, *_ = layer(
{"q": q_batch},
idx_list=sample_ids,
warm_start=True,
update=True,
solver="OSQP",
)
See also¶
- Installation
- Solvers
- Solution-Map Layer
- API Overview
- Efficient Forward Pass
- Value-Function Differentiation Theory
- Numerical Considerations