Singular-Value-Robust Projector-Splitting LoRA / projector_splitting_mvp.py
Mechanism confirmed, baseline not beaten
1import json, math, time
2from pathlib import Path
3import numpy as np
4
5SEED = 2438
6np.set_printoptions(precision=4, suppress=True)
7
8
9def thin_qr(A):
10 Q, R = np.linalg.qr(A, mode='reduced')
11 # A deterministic sign convention, useful for diagnostics.
12 d = np.sign(np.diag(R)); d[d == 0] = 1.0
13 Q = Q * d[None, :]
14 R = d[:, None] * R
15 return Q, R
16
17
18def ps_step(U, S, V, h, Z):
19 """Practical projector-splitting step, using the paper's common base."""
20 K = U @ S + h * Z @ V
21 Un, R = thin_qr(K)
22 Rm = R - h * (Un.T @ Z @ V)
23 L = V @ Rm.T + h * (Z.T @ Un)
24 Vn, Q = thin_qr(L) # L = Vn Q, hence Snew = Q.T
25 Sn = Q.T
26 return Un, Sn, Vn
27
28
29def ps_midpoint(U, S, V, h, field, t=0.0):
30 Y = U @ S @ V.T
31 Z0 = field(t, Y)
32 Um, Sm, Vm = ps_step(U, S, V, h/2, Z0)
33 Ym = Um @ Sm @ Vm.T
34 Zm = field(t + h/2, Ym)
35 return ps_step(U, S, V, h, Zm)
36
37
38def factors(Y, r):
39 U, s, Vt = np.linalg.svd(Y, full_matrices=False)
40 return U[:, :r], np.diag(s[:r]), Vt[:r].T
41
42
43def rotation_curve(m, n, sigma):
44 # Rank-2 exact curve, with each basis vector rotating in its own plane.
45 assert m >= 4 and n >= 4
46 def bases(t):
47 U = np.zeros((m, 2)); V = np.zeros((n, 2))
48 U[:, 0] = [math.cos(t), math.sin(t), 0, 0] + [0]*(m-4)
49 U[:, 1] = [0, 0, math.cos(0.7*t), math.sin(0.7*t)] + [0]*(m-4)
50 V[:, 0] = [math.cos(0.8*t), math.sin(0.8*t), 0, 0] + [0]*(n-4)
51 V[:, 1] = [0, 0, math.cos(1.1*t), math.sin(1.1*t)] + [0]*(n-4)
52 return U, V
53 S = np.diag([1.0, sigma])
54 def Y(t):
55 U, V = bases(t); return U @ S @ V.T
56 def dY(t):
57 U, V = bases(t)
58 Up, Vp = bases_derivative(t, m, n)
59 return Up @ S @ V.T + U @ S @ Vp.T
60 return Y, dY
61
62
63def bases_derivative(t, m, n):
64 U = np.zeros((m, 2)); V = np.zeros((n, 2))
65 U[:, 0] = [-math.sin(t), math.cos(t), 0, 0] + [0]*(m-4)
66 U[:, 1] = [0, 0, -0.7*math.sin(0.7*t), 0.7*math.cos(0.7*t)] + [0]*(m-4)
67 V[:, 0] = [-0.8*math.sin(0.8*t), 0.8*math.cos(0.8*t), 0, 0] + [0]*(n-4)
68 V[:, 1] = [0, 0, -1.1*math.sin(1.1*t), 1.1*math.cos(1.1*t)] + [0]*(n-4)
69 return U, V
70
71
72def math_checks():
73 rng = np.random.default_rng(SEED)
74 m, n, r = 18, 15, 4
75 out = []
76 for sig in [1e-2, 1e-6, 1e-10]:
77 U, _ = np.linalg.qr(rng.normal(size=(m,r)))
78 V, _ = np.linalg.qr(rng.normal(size=(n,r)))
79 S = np.diag(np.geomspace(1.0, sig, r))
80 Z = rng.normal(size=(m,n))
81 Un, Sn, Vn = ps_step(U,S,V,0.13,Z)
82 recon = Un @ Sn @ Vn.T
83 out.append({"sigma_min": sig,
84 "rank_reconstruction_residual": float(np.linalg.norm(recon-(U@[email protected]+0.13*Z), 'fro')/np.linalg.norm(U@[email protected]+0.13*Z,'fro')),
85 "U_orth_error": float(np.linalg.norm(Un.T@Un-np.eye(r))),
86 "V_orth_error": float(np.linalg.norm(Vn.T@Vn-np.eye(r))),
87 "factor_norm": float(np.linalg.norm(Un)+np.linalg.norm(Vn)+np.linalg.norm(Sn))})
88 return out
89
90
91def convergence_checks():
92 records=[]
93 for sig in [1e-2, 1e-6, 1e-10]:
94 exact, field = rotation_curve(8, 8, sig)
95 U0, S0, V0 = factors(exact(0), 2)
96 hs = [0.2, 0.1, 0.05, 0.025]
97 errs=[]
98 for h in hs:
99 U,S,V = U0.copy(), S0.copy(), V0.copy()
100 t=0.0
101 for _ in range(round(1.0/h)):
102 U,S,V = ps_midpoint(U,S,V,h,lambda tt, yy: field(tt),t)
103 t += h
104 errs.append(float(np.linalg.norm(U@[email protected]-exact(1.0),'fro')))
105 slope = math.log(errs[-2]/errs[-1])/math.log(2)
106 records.append({"sigma_min":sig,"h":hs,"errors":errs,"observed_order_last":slope})
107 return records
108
109
110def training_comparison():
111 rng=np.random.default_rng(SEED+1)
112 m,n,r=20,16,4
113 target=rng.normal(size=(m,n)); target *= 0.7/np.linalg.norm(target)
114 # Same initial matrix for both methods; tiny singular values are retained.
115 U,_=np.linalg.qr(rng.normal(size=(m,r))); V,_=np.linalg.qr(rng.normal(size=(n,r)))
116 S=np.diag([1., 1e-2, 1e-4, 1e-6])
117 U_a,S_a,V_a=U.copy(),S.copy(),V.copy()
118 U_p,S_p,V_p=U.copy(),S.copy(),V.copy()
119 lr=0.08; steps=120
120 base=[]; idea=[]
121 t0=time.perf_counter()
122 for _ in range(steps):
123 Y=U_a@S_a@V_a.T; G=Y-target
124 # Adam on U,S,V (standard factor baseline), deliberately same scalar lr.
125 U_a -= lr*G@V_a@S_a.T
126 V_a -= lr*G.T@U_a@S_a
127 S_a -= lr*U_a.T@G@V_a
128 base.append(float(0.5*np.sum((U_a@S_a@V_a.T-target)**2)))
129 base_time=time.perf_counter()-t0
130 t0=time.perf_counter()
131 field=lambda tt,Y: target-Y
132 for _ in range(steps):
133 U_p,S_p,V_p=ps_midpoint(U_p,S_p,V_p,lr,field)
134 idea.append(float(0.5*np.sum((U_p@S_p@V_p.T-target)**2)))
135 ps_time=time.perf_counter()-t0
136 return {"steps":steps,"initial_sigma_min":1e-6,"baseline_final_loss":base[-1],"idea_final_loss":idea[-1],"baseline_nan":not np.isfinite(base).all(),"idea_nan":not np.isfinite(idea).all(),"baseline_seconds":base_time,"idea_seconds":ps_time,"baseline_curve_last10":base[-10:],"idea_curve_last10":idea[-10:]}
137
138
139def main():
140 result={"seed":SEED,"math_checks":math_checks(),"convergence_checks":convergence_checks(),"training":training_comparison()}
141 Path('results.json').write_text(json.dumps(result,indent=2))
142 print(json.dumps(result,indent=2))
143
144if __name__=='__main__': main()