| import torch |
| import torch.nn as nn |
| from torch.optim import Optimizer |
| import numpy as np |
| import time |
| import cvxpy as cp |
| import scipy.sparse as sp |
|
|
| from .utils import forward_single_np_eq_cst, forward_batch_np, extract_nBatch, expandParam |
|
|
| torch.backends.cuda.matmul.allow_tf32 = True |
| torch.set_float32_matmul_precision("high") |
|
|
| |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| def _bpqp_np(x): |
| return x.detach().cpu().numpy() |
|
|
| def _bpqp_sym(P): |
| return 0.5 * (P + P.T) |
|
|
| def _bpqp_osqp_solve(P_csc, q_np, A_csc, l_np, u_np): |
| import osqp |
| prob = osqp.OSQP() |
| prob.setup(P_csc, q_np, A_csc, l_np, u_np, |
| verbose=False, |
| eps_abs=1e-5, |
| eps_rel=1e-5, |
| eps_prim_inf=1e-5, |
| eps_dual_inf=1e-5) |
| res = prob.solve() |
| if res.x is None: |
| raise RuntimeError(res.info.status) |
| return res.x.astype(np.float64), res.y.astype(np.float64) |
|
|
| def _bpqp_pack_osqp(P, q, G, h, A, b): |
| Pn, qn, Gn, hn, An, bn = [_bpqp_np(x) for x in [P, q, G, h, A, b]] |
| Pn = _bpqp_sym(Pn).astype(np.float64) |
| qn = qn.reshape(-1).astype(np.float64) |
| Gn = Gn.astype(np.float64) |
| hn = hn.reshape(-1).astype(np.float64) |
| An = An.astype(np.float64) |
| bn = bn.reshape(-1).astype(np.float64) |
| m, p = Gn.shape[0], An.shape[0] |
| if p > 0: |
| Aos = sp.csc_matrix(np.vstack([Gn, An])) |
| l = np.hstack([-np.inf * np.ones(m), bn]) |
| u = np.hstack([hn, bn]) |
| else: |
| Aos = sp.csc_matrix(Gn) |
| l = -np.inf * np.ones(m) |
| u = hn |
| return sp.csc_matrix(Pn), qn, Aos, l.astype(np.float64), u.astype(np.float64), m, p, Gn, hn, An |
|
|
|
|
| def add_diag_(M, eps): |
| if eps and eps > 0: |
| d = M.diagonal(dim1=-2, dim2=-1) |
| d.add_(eps) |
|
|
| def compact_active_rows(A): |
| B, m, n = A.shape |
| As, idx = [], [] |
| for b in range(B): |
| rowmask = (A[b].abs().amax(dim=-1) > 0) |
| Ab = A[b][rowmask] |
| As.append(Ab) |
| idx.append(rowmask.nonzero(as_tuple=False).squeeze(-1)) |
| return As, idx |
|
|
| def kkt_schur_complement(Q, A, delta): |
| eps_q = 1e-8 |
| eps_s = 1e-12 |
| if delta.dim() == 2: |
| delta = delta.unsqueeze(-1) |
|
|
| B, n, _ = Q.shape |
| m = A.shape[1] if A.numel() > 0 else 0 |
|
|
| I_n = torch.eye(n, dtype=Q.dtype, device=Q.device) |
| L = torch.linalg.cholesky(Q + eps_q * I_n) |
|
|
| if m == 0: |
| dz = -torch.cholesky_solve(delta, L) |
| return dz.squeeze(-1), Q.new_zeros(B, 0) |
|
|
| AT = A.transpose(-1, -2) |
|
|
| Winv = torch.cholesky_solve(AT, L) |
| y = torch.cholesky_solve(delta, L) |
|
|
| S = A @ Winv |
| if eps_s is not None and eps_s > 0: |
| I_m = torch.eye(m, dtype=Q.dtype, device=Q.device) |
| S = S + eps_s * I_m |
|
|
| rhs = -(A @ y) |
| try: |
| Ls = torch.linalg.cholesky(S) |
| dlam = torch.cholesky_solve(rhs, Ls) |
| except RuntimeError: |
| |
| dlam = torch.linalg.lstsq(S, rhs, driver='gels').solution |
|
|
| dz = -torch.cholesky_solve(delta + AT @ dlam, L) |
|
|
| return dz.squeeze(-1), dlam.squeeze(-1) |
|
|
| def make_schur_op(A, L, eps_s): |
| AT = [a.transpose(-1, -2).contiguous() for a in A] |
| def Aop(v_list): |
| outs = [] |
| for a, at, vb in zip(A, AT, v_list): |
| |
| w = torch.cholesky_solve(at @ vb, L) |
| out = a @ w |
| if eps_s and eps_s > 0: |
| out = out + eps_s * vb |
| outs.append(out) |
| return outs |
| return Aop |
|
|
| def cg_solve_list(Aop, b_list, x0_list=None, maxit=50, tol=1e-6): |
| xs = [] |
| for i, b in enumerate(b_list): |
| m = b.shape[0] |
| x = torch.zeros_like(b) if (x0_list is None or x0_list[i] is None) else x0_list[i] |
| r = b - Aop([x])[0] |
| p = r.clone() |
| rsold = (r*r).sum() |
| bnrm = b.norm() |
| for _ in range(maxit): |
| Ap = Aop([p])[0] |
| denom = (p*Ap).sum() |
| alpha = rsold / (denom + 1e-40) |
| x = x + alpha * p |
| r = r - alpha * Ap |
| rsnew = (r*r).sum() |
| if rsnew.sqrt() <= tol * (bnrm + 1e-40): |
| break |
| p = r + (rsnew/rsold) * p |
| rsold = rsnew |
| xs.append(x) |
| return xs |
|
|
| def kkt_schur_fast(Q, A, delta, L_cached=None, eps_q=1e-8, eps_s=1e-10, |
| cg_threshold=2560, cg_maxit=50, cg_tol=1e-6, warm_dlam_list=None): |
| if delta.dim() == 3 and delta.size(-1) == 1: |
| delta = delta.squeeze(-1) |
| B, n, _ = Q.shape |
|
|
| Q = Q.contiguous() |
| A = A.contiguous() |
| delta = delta.contiguous() |
|
|
| if L_cached is None: |
| Q_ = Q.clone() |
| add_diag_(Q_, eps_q) |
| L = torch.linalg.cholesky(Q_) |
| else: |
| L = L_cached |
|
|
|
|
| if A.numel() == 0: |
| dz = -torch.cholesky_solve(delta.unsqueeze(-1), L).squeeze(-1) |
| return dz, [Q.new_zeros(0) for _ in range(B)] |
|
|
| Alist, idxlist = compact_active_rows(A) |
|
|
| y = -torch.cholesky_solve(delta.unsqueeze(-1), L) |
|
|
| dlam_list, dz_list = [], [] |
| Aop = make_schur_op(Alist, L, eps_s) |
|
|
| rhs_list = [(a @ y[b]) for b, a in enumerate(Alist)] |
|
|
| for b, Ab in enumerate(Alist): |
| m_b = Ab.shape[0] |
| if m_b == 0: |
| dlam_b = Ab.new_zeros(0, 1) |
| elif m_b <= cg_threshold: |
| ATb = Ab.transpose(-1, -2).contiguous() |
| Winv_b = torch.cholesky_solve(ATb, L[b:b+1]) |
| Sb = Ab @ Winv_b.squeeze(0) |
| add_diag_(Sb, eps_s) |
| Ls = torch.linalg.cholesky(Sb) |
| dlam_b = torch.cholesky_solve(rhs_list[b], Ls) |
| else: |
| raise NotImplementedError("CG not implemented") |
| |
| |
| dlam_list.append(dlam_b) |
|
|
| dz = y.clone() |
| for b, (Ab, dlam_b) in enumerate(zip(Alist, dlam_list)): |
| if dlam_b.numel() == 0: |
| continue |
| ATd = Ab.transpose(-1, -2) @ dlam_b |
| dz[b:b+1] -= torch.cholesky_solve(ATd.unsqueeze(0), L[b:b+1]) |
|
|
| dz = dz.squeeze(-1) |
|
|
| M = A.shape[1] |
| dlam = Q.new_zeros((B, M)) |
| for b, (dl_b, idx_b) in enumerate(zip(dlam_list, idxlist)): |
| if dl_b.numel(): |
| |
| dlam[b, idx_b] = dl_b.squeeze(-1) |
|
|
| return dz, dlam |
|
|
| def FFOQPLayer(eps=1e-12, verbose=0, notImprovedLim=3, maxIter=20, alpha=100, check_Q_spd=False, chunk_size=100, |
| solver='qpsolvers', solver_opts={"verbose": False}, |
| exact_bwd_sol=True, slack_cutoff=1e-8, cvxpy_instance=None): |
| class QPFunctionFn(torch.autograd.Function): |
| @staticmethod |
| @torch.no_grad() |
| def forward(ctx, Q_, p_, G_, h_, A_, b_): |
| |
| start_time = time.time() |
| nBatch = extract_nBatch(Q_, p_, G_, h_, A_, b_) |
| Q, _ = expandParam(Q_, nBatch, 3) |
| p, _ = expandParam(p_, nBatch, 2) |
| G, _ = expandParam(G_, nBatch, 3) |
| h, _ = expandParam(h_, nBatch, 2) |
| if A_.numel() > 0: |
| A, _ = expandParam(A_, nBatch, 3) |
| else: |
| A = None |
| if b_.numel() > 0: |
| b, _ = expandParam(b_, nBatch, 2) |
| else: |
| b = None |
|
|
| if check_Q_spd: |
| try: |
| torch.linalg.cholesky(Q) |
| except: |
| raise RuntimeError('Q is not SPD.') |
|
|
| _, nineq, nz = G.size() |
| neq = A.size(1) if A is not None else 0 |
| assert(neq > 0 or nineq > 0) |
| ctx.neq, ctx.nineq, ctx.nz = neq, nineq, nz |
|
|
| if nineq > 0 and solver == 'qpsolvers': |
| from dqp import dQP |
|
|
| dQP_settings = dQP.build_settings( |
| solve_type="dense", |
| qp_solver="gurobi", |
| |
| ) |
| dQP_layer = dQP.dQP_layer(settings=dQP_settings) |
| if nBatch == 1: |
| Q = Q.squeeze(0) |
| p = p.squeeze(0) |
| G = G.squeeze(0) |
| h = h.squeeze(0) |
| A = A.squeeze(0) if A is not None else None |
| b = b.squeeze(0) if b is not None else None |
| zhats, nus, lams, solve_time, total_forward_time = dQP_layer( |
| Q, p, G, h, A, b |
| ) |
| if isinstance(nus, list): |
| nus = torch.vstack(nus) |
| zhats = zhats.to(dtype=Q.dtype) |
| lams = lams.to(dtype=Q.dtype) |
| nus = nus.to(dtype=Q.dtype) |
| |
| if nBatch == 1: |
| G = G.unsqueeze(0) |
| h = h.unsqueeze(0) |
| Gz = torch.bmm(G, zhats.unsqueeze(-1)).squeeze(-1) |
| slacks = torch.clamp(h - Gz, min=0.0) |
|
|
| slacks = slacks.to(device=zhats.device, dtype=Q.dtype) |
| elif nineq > 0 and solver == 'PDIPM': |
| from qpth.solvers.pdipm import batch as pdipm_b |
|
|
| if cvxpy_instance is None: |
| ctx.Q_LU, ctx.S_LU, ctx.R = pdipm_b.pre_factor_kkt(Q, G, A) |
| zhats, nus, lams, slacks = pdipm_b.forward( |
| Q, p, G, h, A, b, ctx.Q_LU, ctx.S_LU, ctx.R, |
| eps, verbose, notImprovedLim, maxIter) |
| else: |
| cvxpy_params = cvxpy_instance["params"] |
| cvxpy_problem = cvxpy_instance["problem"] |
| cvxpy_variables = cvxpy_instance["variables"] |
| eq_constraints = cvxpy_instance["eq_constraints"] |
| ineq_constraints = cvxpy_instance["ineq_constraints"] |
| eq_functions = cvxpy_instance["eq_functions"] |
| ineq_functions = cvxpy_instance["ineq_functions"] |
| |
| params_torch = [Q, p, G, h] |
| params_numpy = [param.detach().cpu().numpy() for param in params_torch] |
| |
| sol_numpy = [np.empty((nBatch,) + v.shape, dtype=float) for v in cvxpy_variables] |
| eq_dual = [np.empty((nBatch,) + f.shape, dtype=float) for f in eq_functions] |
| ineq_dual = [np.empty((nBatch,) + g.shape, dtype=float) for g in ineq_functions] |
| ineq_slack_residual = [np.empty((nBatch,) + g.shape, dtype=float) for g in ineq_functions] |
| |
| for i in range(nBatch): |
| for p_val, param_obj in zip(params_numpy, cvxpy_params): |
| param_obj.value = p_val[i] |
| |
| cvxpy_problem.solve(solver=cp.OSQP, warm_start=False, verbose=False, eps_abs=1e-3, eps_rel=1e-3, max_iter=250) |
| |
| sol_i = [v.value for v in cvxpy_variables] |
| eq_i = [c.dual_value for c in eq_constraints] |
| ineq_i = [c.dual_value for c in ineq_constraints] |
| slack_i = [np.maximum(-expr.value, 0.0) for expr in ineq_functions] |
| |
| for v_id, v in enumerate(cvxpy_variables): |
| sol_numpy[v_id][i, ...] = sol_i[v_id] |
|
|
| for c_id, c in enumerate(eq_constraints): |
| eq_dual[c_id][i, ...] = eq_i[c_id] |
|
|
| for c_id, c in enumerate(ineq_constraints): |
| ineq_dual[c_id][i, ...] = ineq_i[c_id] |
|
|
| for c_id, expr in enumerate(ineq_functions): |
| g_val = expr.value |
| s_val = -g_val |
| s_val = np.maximum(s_val, 0.0) |
| ineq_slack_residual[c_id][i, ...] = slack_i[c_id] |
| |
| device = Q.device |
| dtype = Q.dtype |
|
|
| zhats = [torch.from_numpy(arr).to(device=device, dtype=dtype) for arr in sol_numpy][0] |
| lams = [torch.from_numpy(arr).to(device=device, dtype=dtype) for arr in ineq_dual][0] |
| nus = [torch.from_numpy(arr).to(device=device, dtype=dtype) for arr in eq_dual] |
| if len(nus)!=0: |
| nus = nus[0] |
| else: |
| nus=lams |
| |
| slacks = [torch.from_numpy(arr).to(device=device, dtype=dtype) for arr in ineq_slack_residual][0] |
| elif nineq > 0 and solver == 'OSQP_NATIVE': |
| import osqp |
|
|
| device = Q.device |
| dtype = Q.dtype |
| zhats = torch.empty(nBatch, nz, device=device, dtype=dtype) |
| lams = torch.empty(nBatch, nineq, device=device, dtype=dtype) |
| nus = torch.empty(nBatch, neq, device=device, dtype=dtype) if neq > 0 else torch.empty(0, device=device, dtype=dtype) |
| slacks = torch.empty(nBatch, nineq, device=device, dtype=dtype) |
|
|
| for i in range(nBatch): |
| Pi = Q[i] |
| qi = p[i] |
| Gi = G[i] |
| hi = h[i] |
| if neq > 0: |
| Ai = A[i] |
| bi = b[i] |
| else: |
| Ai = Q.new_zeros((0, nz), device=device, dtype=dtype) |
| bi = Q.new_zeros((0,), device=device, dtype=dtype) |
|
|
| P_csc, qn, Aos, l, u, m_i, p_i, Gn, hn, An = _bpqp_pack_osqp(Pi, qi, Gi, hi, Ai, bi) |
| x_np, y_np = _bpqp_osqp_solve(P_csc, qn, Aos, l, u) |
|
|
| zhats[i] = torch.from_numpy(x_np).to(device=device, dtype=dtype) |
| lam_np = y_np[:m_i] |
| lams[i] = torch.from_numpy(lam_np).to(device=device, dtype=dtype) |
|
|
| if neq > 0: |
| nu_np = y_np[m_i:m_i + p_i] |
| nus[i] = torch.from_numpy(nu_np).to(device=device, dtype=dtype) |
|
|
| Gx = Gn @ x_np |
| slack_np = np.maximum(hn - Gx, 0.0) |
| slacks[i] = torch.from_numpy(slack_np).to(device=device, dtype=dtype) |
| elif nineq > 0: |
| print("Using {} solver".format(solver)) |
| zhats = torch.Tensor(nBatch, ctx.nz).type_as(Q) |
| lams = torch.Tensor(nBatch, ctx.nineq).type_as(Q) |
| nus = torch.Tensor(nBatch, ctx.neq).type_as(Q) \ |
| if ctx.neq > 0 else torch.Tensor() |
| slacks = torch.Tensor(nBatch, ctx.nineq).type_as(Q) |
|
|
| for i in range(0, nBatch, chunk_size): |
| if chunk_size > 1: |
| size = min(chunk_size, nBatch - i) |
| Ai, bi = (A[i:i+size], b[i:i+size]) if neq > 0 else (None, None) |
| _, zhati, nui, lami, si = forward_batch_np( |
| *[x.cpu().numpy() if x is not None else None |
| for x in (Q[i:i+size], p[i:i+size], G[i:i+size], h[i:i+size], Ai, bi)], |
| solver=solver, solver_opts=solver_opts) |
| i = slice(i, i + size) |
| else: |
| Ai, bi = (A[i], b[i]) if neq > 0 else (None, None) |
| _, zhati, nui, lami, si = forward_single_np_eq_cst( |
| *[x.cpu().numpy() if x is not None else None |
| for x in (Q[i], p[i], G[i], h[i], Ai, bi)]) |
| |
| zhats[i] = torch.Tensor(zhati) |
| lams[i] = torch.Tensor(lami) |
| slacks[i] = torch.Tensor(si) |
| if neq > 0: |
| nus[i] = torch.Tensor(nui) |
| else: |
| raise NotImplementedError("Solver not implemented") |
|
|
| |
| ctx.lams = lams |
| ctx.nus = nus |
| ctx.slacks = slacks |
|
|
| ctx.save_for_backward(zhats, lams, nus, Q_, p_, G_, h_, A_, b_) |
| |
| |
| return zhats |
|
|
| @staticmethod |
| def backward(ctx, grad_output): |
| |
| zhats, lams, nus, Q_, p_, G_, h_, A_, b_ = ctx.saved_tensors |
| lams = torch.clamp(lams, min=0) |
| slacks = torch.clamp(ctx.slacks, min=0) |
|
|
| nBatch = extract_nBatch(Q_, p_, G_, h_, A_, b_) |
| |
| |
| Q, Q_e = expandParam(Q_, nBatch, 3) |
| p, p_e = expandParam(p_, nBatch, 2) |
| G, G_e = expandParam(G_, nBatch, 3) |
| h, h_e = expandParam(h_, nBatch, 2) |
| A, A_e = expandParam(A_, nBatch, 3) |
| b, b_e = expandParam(b_, nBatch, 2) |
|
|
| Q, p, G, h, A, b = Q.to(zhats.device), p.to(zhats.device), G.to(zhats.device), h.to(zhats.device), A.to(zhats.device), b.to(zhats.device) |
|
|
| |
| _, nineq, nz = G.size() |
| neq = A.size(1) if A.nelement() > 0 else 0 |
|
|
| delta_directions = grad_output.unsqueeze(-1) |
| zhats = zhats.unsqueeze(-1).detach() |
|
|
| start_time = time.time() |
| |
| active_constraints = (slacks <= slack_cutoff).unsqueeze(-1).to(Q.dtype) |
| G_active = G * active_constraints |
| |
| |
|
|
| dzhat = torch.Tensor(nBatch, nz, 1).type_as(Q) |
| dnu = torch.Tensor(nBatch, nineq + neq).type_as(Q) |
|
|
| if neq > 0: |
| G_active = torch.cat((G_active, A), dim=1) |
| |
|
|
| if exact_bwd_sol: |
| |
| delta_directions = delta_directions.to(Q.dtype) |
| _dzhat, _dnu = kkt_schur_fast(Q, G_active, delta_directions) |
| dzhat.copy_(_dzhat.unsqueeze(-1)) |
| dnu.copy_(_dnu) |
| else: |
| for i in range(0, nBatch, chunk_size): |
| if chunk_size > 1: |
| size = min(chunk_size, nBatch - i) |
| i = slice(i, i + size) |
| _, zhati, nui, _, _ = forward_batch_np( |
| *[x.cpu().numpy() if x is not None else None |
| for x in (Q[i], grad_output[i], None, None, G_active[i], torch.zeros(G_active[i].shape[0], G_active[i].shape[1]))], |
| solver=solver, solver_opts=solver_opts) |
| else: |
| _, zhati, nui, _, _ = forward_single_np_eq_cst( |
| *[x.cpu().numpy() if x is not None else None |
| for x in (Q[i], grad_output[i], None, None, G_active[i], torch.zeros(G_active[i].shape[0]))]) |
|
|
| dzhat[i, :, 0] = torch.Tensor(zhati) |
| dnu[i] = torch.Tensor(nui) |
|
|
| start_time = time.time() |
| with torch.enable_grad(): |
| Q_torch = Q.detach().clone().requires_grad_(True) |
| p_torch = p.detach().clone().requires_grad_(True) |
| G_torch = G.detach().clone().requires_grad_(True) |
| h_torch = h.detach().clone().requires_grad_(True) |
| A_torch = A.detach().clone().requires_grad_(True) |
| b_torch = b.detach().clone().requires_grad_(True) |
| |
| objectives = (dzhat.transpose(-1,-2) @ Q_torch @ zhats + p_torch.unsqueeze(1) @ dzhat).squeeze(-1,-2) |
| violations = G_torch @ zhats - h_torch.unsqueeze(-1) |
|
|
| ineq_penalties = dnu[:, :nineq].unsqueeze(1) @ (violations * active_constraints) |
|
|
| if neq > 0: |
| eq_violations = A_torch @ zhats - b_torch.unsqueeze(-1) |
| eq_penalties = dnu[:, nineq:].unsqueeze(1) @ eq_violations |
| else: |
| eq_penalties = 0 |
|
|
| lagrangians = objectives + ineq_penalties + eq_penalties |
| loss = torch.sum(lagrangians) |
| loss.backward() |
|
|
| Q_grad = Q_torch.grad.detach() |
| p_grad = p_torch.grad.detach() |
| G_grad = G_torch.grad.detach() |
| h_grad = h_torch.grad.detach() |
| if neq > 0: |
| A_grad = A_torch.grad.detach() |
| b_grad = b_torch.grad.detach() |
| |
| |
| else: |
| A_grad = torch.zeros_like(A) |
| b_grad = torch.zeros_like(b) |
|
|
| return (Q_grad, p_grad, G_grad, h_grad, A_grad, b_grad) |
|
|
| return QPFunctionFn.apply |
|
|
|
|