ProCreations's picture
Publish six-claim native-scale FFOLayer reproduction
9860743 verified
Raw
History Blame Contribute Delete
9.25 kB
import numpy as np
import torch
from torch.autograd import Function
import cvxpy as cp
def _np(x): return x.detach().cpu().numpy()
def _sym(P): return 0.5 * (P + P.T)
CACHE = True
_QP_CACHE = {}
_EQ_CACHE = {}
def _qp_cvx_osqp(Pn, qn, Gn, hn, An, bn, eps_abs=1e-5, eps_rel=1e-5, max_iter=10000):
Pn = _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)
n = Pn.shape[0]
m = Gn.shape[0]
p = An.shape[0]
key = (n, m, p)
if (not CACHE) or (key not in _QP_CACHE):
x = cp.Variable(n)
Pp = cp.Parameter((n, n), symmetric=True)
qp = cp.Parameter(n)
cons = []
ineq = eq = None
Gp = hp = Ap = bp = None
if m > 0:
Gp = cp.Parameter((m, n))
hp = cp.Parameter(m)
ineq = (Gp @ x <= hp)
cons.append(ineq)
if p > 0:
Ap = cp.Parameter((p, n))
bp = cp.Parameter(p)
eq = (Ap @ x == bp)
cons.append(eq)
obj = cp.Minimize(0.5 * cp.quad_form(x, cp.psd_wrap(Pp)) + qp @ x)
prob = cp.Problem(obj, cons)
bundle = {"x": x, "Pp": Pp, "qp": qp, "Gp": Gp, "hp": hp, "Ap": Ap, "bp": bp, "ineq": ineq, "eq": eq, "prob": prob}
if CACHE:
_QP_CACHE[key] = bundle
else:
bundle = _QP_CACHE[key]
bundle["Pp"].value = Pn
bundle["qp"].value = qn
if m > 0:
bundle["Gp"].value = Gn
bundle["hp"].value = hn
if p > 0:
bundle["Ap"].value = An
bundle["bp"].value = bn
bundle["prob"].solve(
solver=cp.OSQP,
warm_start=True,
verbose=False,
eps_abs=eps_abs,
eps_rel=eps_rel,
max_iter=max_iter,
polish=True,
)
st = bundle["prob"].status
if st not in (cp.OPTIMAL, cp.OPTIMAL_INACCURATE):
raise RuntimeError(f"CVXPY/OSQP status: {st}")
xval = np.asarray(bundle["x"].value, dtype=np.float64).reshape(-1)
lam = np.asarray(bundle["ineq"].dual_value, dtype=np.float64).reshape(-1) if m > 0 else np.zeros((0,), dtype=np.float64)
nu = np.asarray(bundle["eq"].dual_value, dtype=np.float64).reshape(-1) if p > 0 else np.zeros((0,), dtype=np.float64)
y = np.concatenate([lam, nu], axis=0)
return xval, y
def _eq_qp_cvx_osqp(Pn, gnp, Ab, eps_abs=1e-5, eps_rel=1e-5, max_iter=10000):
Pn = _sym(Pn).astype(np.float64)
gnp = gnp.reshape(-1).astype(np.float64)
Ab = Ab.astype(np.float64)
k, n = Ab.shape
key = (n, k)
if (not CACHE) or (key not in _EQ_CACHE):
z = cp.Variable(n)
Pp = cp.Parameter((n, n), symmetric=True)
gp = cp.Parameter(n)
Ap = cp.Parameter((k, n))
con = (Ap @ z == 0)
obj = cp.Minimize(0.5 * cp.quad_form(z, cp.psd_wrap(Pp)) + gp @ z)
prob = cp.Problem(obj, [con])
bundle = {"z": z, "Pp": Pp, "gp": gp, "Ap": Ap, "con": con, "prob": prob}
if CACHE:
_EQ_CACHE[key] = bundle
else:
bundle = _EQ_CACHE[key]
bundle["Pp"].value = Pn
bundle["gp"].value = gnp
bundle["Ap"].value = Ab
bundle["prob"].solve(
solver=cp.OSQP,
warm_start=True,
verbose=False,
eps_abs=eps_abs,
eps_rel=eps_rel,
max_iter=max_iter,
polish=True,
)
st = bundle["prob"].status
if st not in (cp.OPTIMAL, cp.OPTIMAL_INACCURATE):
raise RuntimeError(f"CVXPY/OSQP status: {st}")
zval = np.asarray(bundle["z"].value, dtype=np.float64).reshape(-1)
yb = np.asarray(bundle["con"].dual_value, dtype=np.float64).reshape(-1)
return zval, yb
def bpqp_backward(xn, yn, Pn, Gn, hn, An, gnp, m, p, act_tol):
lam = yn[:m] if m > 0 else np.zeros((0,), dtype=np.float64)
if m > 0:
resid = Gn @ xn - hn
active = np.where((resid > -act_tol) | (lam > act_tol))[0].astype(np.int64)
else:
active = np.zeros((0,), dtype=np.int64)
rows = []
if active.size > 0:
rows.append(Gn[active, :])
if p > 0:
rows.append(An)
n = xn.size
if len(rows) == 0:
try:
z = -np.linalg.solve(Pn, gnp)
except np.linalg.LinAlgError:
z = -np.linalg.solve(Pn + 1e-8 * np.eye(n), gnp)
return z, np.zeros((0,), dtype=np.float64), active
Ab = np.vstack(rows)
z, yb = _eq_qp_cvx_osqp(Pn, gnp.astype(np.float64), Ab)
return z, yb, active
def BPQPLayer(sign=1, act_tol=1e-6):
class _Layer(Function):
@staticmethod
def forward(ctx, P, q, G, h, A, b):
batched = (P.dim() == 3)
B = P.shape[0] if batched else 1
xs, ys, ms, ps = [], [], [], []
for i in range(B):
Pi = P[i] if batched else P
qi = q[i] if q.dim() == 2 else q
Gi = G[i] if G.dim() == 3 else G
hi = h[i] if h.dim() == 2 else h
Ai = A[i] if A.dim() == 3 else A
bi = b[i] if b.dim() == 2 else b
Pn = _sym(_np(Pi)).astype(np.float64)
qn = (sign * _np(qi)).reshape(-1).astype(np.float64)
Gn = _np(Gi).astype(np.float64)
hn = _np(hi).reshape(-1).astype(np.float64)
An = _np(Ai).astype(np.float64)
bn = _np(bi).reshape(-1).astype(np.float64)
m, p = Gn.shape[0], An.shape[0]
x_np, y_np = _qp_cvx_osqp(Pn, qn, Gn, hn, An, bn)
xs.append(torch.from_numpy(x_np).to(device=Pi.device, dtype=Pi.dtype))
ys.append(torch.from_numpy(y_np).to(device=Pi.device, dtype=Pi.dtype))
ms.append(m); ps.append(p)
x_out = torch.stack(xs, 0) if batched else xs[0]
y_out = torch.stack(ys, 0) if batched else ys[0]
ctx.save_for_backward(P, q, G, h, A, b, x_out, y_out)
ctx.meta = (batched, B, sign, act_tol, ms, ps)
return x_out
@staticmethod
def backward(ctx, grad_output):
P, q, G, h, A, b, x, y = ctx.saved_tensors
batched, B, sign, act_tol, ms, ps = ctx.meta
gP = torch.zeros_like(P); gq = torch.zeros_like(q); gG = torch.zeros_like(G)
gh = torch.zeros_like(h); gA = torch.zeros_like(A); gb = torch.zeros_like(b)
for i in range(B):
Pi = P[i] if batched else P
qi = q[i] if q.dim() == 2 else q
hi = h[i] if h.dim() == 2 else h
Gi = G[i] if G.dim() == 3 else G
Ai = A[i] if A.dim() == 3 else A
bi = b[i] if b.dim() == 2 else b
xi = x[i] if batched else x
yi = y[i] if batched else y
gi = grad_output[i] if batched else grad_output
Pn = _sym(_np(Pi)).astype(np.float64)
Gn = _np(Gi).astype(np.float64)
hn = _np(hi).reshape(-1).astype(np.float64)
An = _np(Ai).astype(np.float64)
m, p = Gn.shape[0], An.shape[0]
z, yb, active = bpqp_backward(
_np(xi), _np(yi), Pn, Gn, hn, An, _np(gi), m, p, act_tol
)
zt = torch.from_numpy(z).to(device=Pi.device, dtype=Pi.dtype)
gq_i = sign * zt
gP_i = 0.5 * (torch.outer(zt, xi) + torch.outer(xi, zt))
lam = yi[:m] if m > 0 else torch.empty((0,), device=Pi.device, dtype=Pi.dtype)
nu = yi[m:m+p] if p > 0 else torch.empty((0,), device=Pi.device, dtype=Pi.dtype)
k = int(active.size)
mu = torch.from_numpy(yb[:k]).to(device=Pi.device, dtype=Pi.dtype) if k > 0 else torch.empty((0,), device=Pi.device, dtype=Pi.dtype)
eta = torch.from_numpy(yb[k:k+p]).to(device=Pi.device, dtype=Pi.dtype) if p > 0 else torch.empty((0,), device=Pi.device, dtype=Pi.dtype)
gG_i = torch.zeros_like(Gi); gh_i = torch.zeros_like(hi)
if m > 0 and k > 0:
at = torch.tensor(active, device=Pi.device, dtype=torch.long)
lam_act = lam.index_select(0, at)
block = mu[:, None] * xi[None, :] + lam_act[:, None] * zt[None, :]
gG_i.index_copy_(0, at, block)
gh_i.index_copy_(0, at, -mu)
if p > 0:
gb_i = -eta
gA_i = eta[:, None] * xi[None, :] + nu[:, None] * zt[None, :]
else:
gb_i = torch.zeros_like(bi)
gA_i = torch.zeros_like(Ai)
if batched: gP[i] = gP_i
else: gP = gP + gP_i
if q.dim() == 2: gq[i] = gq_i
else: gq = gq + gq_i
if G.dim() == 3: gG[i] = gG_i
else: gG = gG + gG_i
if h.dim() == 2: gh[i] = gh_i
else: gh = gh + gh_i
if A.dim() == 3: gA[i] = gA_i
else: gA = gA + gA_i
if b.dim() == 2: gb[i] = gb_i
else: gb = gb + gb_i
return gP, gq, gG, gh, gA, gb
return _Layer.apply