import torch from torch.autograd import Function @torch.no_grad() def alt_qp_solve_batched( P, q, A, b, G, h, rho=1.0, eps=1e-6, max_iter=5000, reg=1e-7 ): device, dtype = q.device, q.dtype B, n = q.shape rho_t = torch.as_tensor(rho, device=device, dtype=dtype) m = A.shape[1] # A: (B,m,n) p = G.shape[1] # G: (B,p,n) x = torch.zeros((B, n), device=device, dtype=dtype) s = torch.zeros((B, p), device=device, dtype=dtype) lam = torch.zeros((B, m), device=device, dtype=dtype) nu = torch.zeros((B, p), device=device, dtype=dtype) I = torch.eye(n, device=device, dtype=dtype).expand(B, n, n) if m > 0: AtA = A.transpose(-1, -2) @ A Atb = (A.transpose(-1, -2) @ b.unsqueeze(-1)).squeeze(-1) else: AtA = torch.zeros((B, n, n), device=device, dtype=dtype) Atb = torch.zeros((B, n), device=device, dtype=dtype) if p > 0: GtG = G.transpose(-1, -2) @ G else: GtG = torch.zeros((B, n, n), device=device, dtype=dtype) H = P + rho_t * (AtA + GtG) + reg * I L = torch.linalg.cholesky(H) def solve_H(rhs_2d): # (B,n) return torch.cholesky_solve(rhs_2d.unsqueeze(-1), L).squeeze(-1) alive = torch.ones((B,), device=device, dtype=torch.bool) for _ in range(max_iter): if not alive.any(): break x0 = x s0 = s rhs = q.clone() if m > 0: rhs = rhs + (A.transpose(-1, -2) @ lam.unsqueeze(-1)).squeeze(-1) - rho_t * Atb if p > 0: rhs = rhs + (G.transpose(-1, -2) @ nu.unsqueeze(-1)).squeeze(-1) rhs = rhs + rho_t * (G.transpose(-1, -2) @ (s - h).unsqueeze(-1)).squeeze(-1) x = solve_H(-rhs) if m > 0: ax = (A @ x.unsqueeze(-1)).squeeze(-1) # (B,m) lam = lam + rho_t * (ax - b) else: ax = None if p > 0: gx = (G @ x.unsqueeze(-1)).squeeze(-1) # (B,p) s = torch.relu(-(nu / rho_t) - (gx - h)) nu = nu + rho_t * (gx + s - h) else: gx = None dx = torch.linalg.norm(x - x0, dim=-1) tol_x = eps * (1.0 + torch.linalg.norm(x0, dim=-1)) conv_x = dx <= tol_x conv_pri = torch.ones((B,), device=device, dtype=torch.bool) if m > 0: r_eq = torch.linalg.norm(ax - b, dim=-1) axn = torch.linalg.norm(ax, dim=-1) bn = torch.linalg.norm(b, dim=-1) tol_eq = eps * (1.0 + torch.maximum(axn, bn)) conv_pri = conv_pri & (r_eq <= tol_eq) if p > 0: r_in = torch.linalg.norm(gx + s - h, dim=-1) gsn = torch.linalg.norm(gx + s, dim=-1) hn = torch.linalg.norm(h, dim=-1) tol_in = eps * (1.0 + torch.maximum(gsn, hn)) conv_pri = conv_pri & (r_in <= tol_in) ds = torch.linalg.norm(s - s0, dim=-1) tol_s = eps * (1.0 + torch.linalg.norm(s0, dim=-1)) conv_s = ds <= tol_s else: conv_s = torch.ones((B,), device=device, dtype=torch.bool) conv = conv_x & conv_pri & conv_s alive = alive & (~conv) return x, lam, nu, s def _adjoint_and_param_grads_single( P, q, A, b, G, h, x, lam, nu, active_mask, g, reg=1e-9, sym_P=True ): device, dtype = g.device, g.dtype n = P.shape[0] m = A.shape[0] p = G.shape[0] if p == 0 or active_mask.numel() == 0 or active_mask.sum() == 0: Ga = G.new_zeros((0, n)) nu_a = nu.new_zeros((0,)) ha = h.new_zeros((0,)) else: Ga = G[active_mask] nu_a = nu[active_mask] ha = h[active_mask] ka = Ga.shape[0] K = torch.zeros((n + m + ka, n + m + ka), device=device, dtype=dtype) K[:n, :n] = P + reg * torch.eye(n, device=device, dtype=dtype) if m > 0: K[:n, n:n+m] = A.T K[n:n+m, :n] = A if ka > 0: K[:n, n+m:] = Ga.T K[n+m:, :n] = Ga rhs = torch.zeros((n + m + ka,), device=device, dtype=dtype) rhs[:n] = g sol = torch.linalg.solve(K, rhs) u = sol[:n] v = sol[n:n+m] if m > 0 else sol.new_zeros((0,)) w = sol[n+m:] if ka > 0 else sol.new_zeros((0,)) grad_q = -u grad_P = -torch.outer(u, x) if sym_P: grad_P = 0.5 * (grad_P + grad_P.T) if m > 0: grad_b = v grad_A = -(torch.outer(v, x) + torch.outer(lam, u)) else: grad_b = None grad_A = None if p > 0: grad_h = h.new_zeros((p,)) grad_G = G.new_zeros((p, n)) if ka > 0: grad_h[active_mask] = w grad_Ga = -(torch.outer(w, x) + torch.outer(nu_a, u)) grad_G[active_mask] = grad_Ga else: grad_h = None grad_G = None return grad_P, grad_q, grad_G, grad_h, grad_A, grad_b def AltDiffLayer( eps=1e-5, max_iter=2500, rho=1.0, reg_fwd=1e-5, active_tol=1e-6, reg_bwd=1e-5, sym_P=True, ): class L(Function): @staticmethod def forward(ctx, P, q, G=None, h=None, A=None, b=None): unbatched = (q.dim() == 1) P_in, q_in, A_in, b_in, G_in, h_in = P, q, A, b, G, h def ensure_batch(x, want_dim): if x is None: return None return x.unsqueeze(0) if x.dim() == want_dim else x P = ensure_batch(P, 2) q = ensure_batch(q, 1) B, n = q.shape A = ensure_batch(A, 2) b = ensure_batch(b, 1) G = ensure_batch(G, 2) h = ensure_batch(h, 1) if A is None or A.numel() == 0: A = torch.empty((1, 0, n), device=q.device, dtype=q.dtype) if b is None or b.numel() == 0: b = torch.empty((1, 0), device=q.device, dtype=q.dtype) if G is None or G.numel() == 0: G = torch.empty((1, 0, n), device=q.device, dtype=q.dtype) if h is None or h.numel() == 0: h = torch.empty((1, 0), device=q.device, dtype=q.dtype) P_shared = (P.shape[0] == 1 and B > 1) A_shared = (A.shape[0] == 1 and B > 1) b_shared = (b.shape[0] == 1 and B > 1) G_shared = (G.shape[0] == 1 and B > 1) h_shared = (h.shape[0] == 1 and B > 1) if P_shared: P = P.expand(B, -1, -1) if A_shared: A = A.expand(B, -1, -1) if b_shared: b = b.expand(B, -1) if G_shared: G = G.expand(B, -1, -1) if h_shared: h = h.expand(B, -1) P_d, q_d, A_d, b_d, G_d, h_d = P.detach(), q.detach(), A.detach(), b.detach(), G.detach(), h.detach() x, lam, nu, s = alt_qp_solve_batched( P_d, q_d, A_d, b_d, G_d, h_d, rho=rho, eps=eps, max_iter=max_iter, reg=reg_fwd ) if s.numel() == 0: active = torch.zeros((B, 0), device=q.device, dtype=torch.bool) else: active = (s <= active_tol) ctx.save_for_backward(P_d, q_d, A_d, b_d, G_d, h_d, x, lam, nu, active) ctx.unbatched = unbatched ctx.shared_flags = (P_shared, A_shared, b_shared, G_shared, h_shared) ctx.orig_none = (P_in is None, q_in is None, G_in is None, h_in is None, A_in is None, b_in is None) ctx.orig_shapes = ( getattr(P_in, "shape", None), getattr(q_in, "shape", None), getattr(G_in, "shape", None), getattr(h_in, "shape", None), getattr(A_in, "shape", None), getattr(b_in, "shape", None), ) ctx.sym_P = sym_P return x[0] if unbatched else x @staticmethod def backward(ctx, grad_output): P, q, A, b, G, h, x, lam, nu, active = ctx.saved_tensors P_shared, A_shared, b_shared, G_shared, h_shared = ctx.shared_flags unbatched = ctx.unbatched sym_P = ctx.sym_P g = grad_output.unsqueeze(0) if grad_output.dim() == 1 else grad_output B, n = g.shape grad_P = torch.zeros_like(P) grad_q = torch.zeros_like(q) grad_A = torch.zeros_like(A) if A.numel() > 0 else None grad_b = torch.zeros_like(b) if b.numel() > 0 else None grad_G = torch.zeros_like(G) if G.numel() > 0 else None grad_h = torch.zeros_like(h) if h.numel() > 0 else None for i in range(B): act_i = active[i] if active.numel() > 0 else active.new_zeros((0,), dtype=torch.bool) Pi = P[i] qi = q[i] Ai = A[i] if A.numel() > 0 else A.new_zeros((0, n)) bi = b[i] if b.numel() > 0 else b.new_zeros((0,)) Gi = G[i] if G.numel() > 0 else G.new_zeros((0, n)) hi = h[i] if h.numel() > 0 else h.new_zeros((0,)) xi = x[i] lami = lam[i] if lam.numel() > 0 else lam.new_zeros((0,)) nui = nu[i] if nu.numel() > 0 else nu.new_zeros((0,)) dP_i, dq_i, dG_i, dh_i, dA_i, db_i = _adjoint_and_param_grads_single( Pi, qi, Ai, bi, Gi, hi, xi, lami, nui, act_i, g[i], reg=reg_bwd, sym_P=sym_P ) grad_P[i] = dP_i grad_q[i] = dq_i if grad_A is not None and dA_i is not None: grad_A[i] = dA_i if grad_b is not None and db_i is not None: grad_b[i] = db_i if grad_G is not None and dG_i is not None: grad_G[i] = dG_i if grad_h is not None and dh_i is not None: grad_h[i] = dh_i if P_shared: grad_P = grad_P.sum(dim=0, keepdim=True) if A_shared and grad_A is not None: grad_A = grad_A.sum(dim=0, keepdim=True) if b_shared and grad_b is not None: grad_b = grad_b.sum(dim=0, keepdim=True) if G_shared and grad_G is not None: grad_G = grad_G.sum(dim=0, keepdim=True) if h_shared and grad_h is not None: grad_h = grad_h.sum(dim=0, keepdim=True) if unbatched: grad_P = grad_P[0] grad_q = grad_q[0] if grad_A is not None: grad_A = grad_A[0] if grad_b is not None: grad_b = grad_b[0] if grad_G is not None: grad_G = grad_G[0] if grad_h is not None: grad_h = grad_h[0] return grad_P, grad_q, grad_G, grad_h, grad_A, grad_b return L.apply