@@ -42,6 +42,132 @@ def solve_id(solve):
4242 return "default"
4343
4444
45+ def _make_differentiable_tridiag_spd (theta , layout ):
46+ """Create a small sparse SPD matrix with fixed sparsity and differentiable values."""
47+ n = theta .numel ()
48+ device = theta .device
49+
50+ diag = theta .square () + 2.0
51+ off = - 0.1 * torch .sigmoid (theta [:- 1 ])
52+
53+ rows = torch .cat (
54+ [
55+ torch .arange (n , device = device ),
56+ torch .arange (n - 1 , device = device ),
57+ torch .arange (1 , n , device = device ),
58+ ]
59+ )
60+ cols = torch .cat (
61+ [
62+ torch .arange (n , device = device ),
63+ torch .arange (1 , n , device = device ),
64+ torch .arange (n - 1 , device = device ),
65+ ]
66+ )
67+ values = torch .cat ([diag , off , off ])
68+
69+ A = torch .sparse_coo_tensor (torch .stack ([rows , cols ]), values , (n , n )).coalesce ()
70+ if layout == torch .sparse_csr :
71+ A = A .to_sparse_csr ()
72+ return A , A .to_dense ()
73+
74+
75+ def _make_differentiable_nonsymmetric_tridiag (theta , layout ):
76+ """Create a small fixed-sparsity non-symmetric diagonally dominant matrix."""
77+ n = 6
78+ device = theta .device
79+ dtype = theta .dtype
80+
81+ if theta .numel () != 3 * n - 2 :
82+ raise ValueError (f"theta should have length { 3 * n - 2 } , got { theta .numel ()} " )
83+
84+ diag_theta = theta [:n ]
85+ upper_theta = theta [n : n + n - 1 ]
86+ lower_theta = theta [n + n - 1 :]
87+
88+ diag = diag_theta .square () + 3.0
89+ upper = 0.05 * torch .tanh (upper_theta )
90+ lower = - 0.08 * torch .sigmoid (lower_theta )
91+
92+ rows = torch .cat (
93+ [
94+ torch .arange (n , device = device ),
95+ torch .arange (n - 1 , device = device ),
96+ torch .arange (1 , n , device = device ),
97+ ]
98+ )
99+ cols = torch .cat (
100+ [
101+ torch .arange (n , device = device ),
102+ torch .arange (1 , n , device = device ),
103+ torch .arange (n - 1 , device = device ),
104+ ]
105+ )
106+ values = torch .cat ([diag , upper , lower ]).to (dtype = dtype )
107+
108+ A = torch .sparse_coo_tensor (torch .stack ([rows , cols ]), values , (n , n )).coalesce ()
109+ if layout == torch .sparse_csr :
110+ A = A .to_sparse_csr ()
111+ return A , A .to_dense ()
112+
113+
114+ def _bicgstab_transpose (A , B , ** kwargs ):
115+ """Solve A.T X = B using BiCGSTAB."""
116+ if A .layout == torch .sparse_csr :
117+ # A.T currently triggers aten::as_strided for sparse CSR tensors in PyTorch,
118+ # so use transpose(...).to_sparse_csr() for the CSR test case.
119+ # A.transpose(0, 1) for csr will return csc, hence the to_sparse_csr() call.
120+ return bicgstab (A .transpose (0 , 1 ).to_sparse_csr (), B , ** kwargs )
121+ return bicgstab (A .T , B , ** kwargs )
122+
123+
124+ def _bicgstab_higher_order_kwargs (value_dtype ):
125+ from torchsparsegradutils .utils .bicgstab import BICGSTABSettings
126+
127+ if value_dtype == torch .float32 :
128+ return {"settings" : BICGSTABSettings (reltol = 1e-6 , abstol = 1e-6 , matvec_max = 1000 )}
129+ return {"settings" : BICGSTABSettings (reltol = 1e-12 , abstol = 1e-12 , matvec_max = 1000 )}
130+
131+
132+ def _higher_order_bicgstab_tolerances (value_dtype ):
133+ if value_dtype == torch .float32 :
134+ return {
135+ "output" : (1e-5 , 1e-5 ),
136+ "grad" : (5e-5 , 5e-5 ),
137+ "hess" : (5e-3 , 5e-3 ),
138+ }
139+ return {
140+ "output" : (1e-8 , 1e-8 ),
141+ "grad" : (1e-6 , 1e-6 ),
142+ "hess" : (5e-5 , 5e-5 ),
143+ }
144+
145+
146+ def _settings_for_higher_order_solve (solve , value_dtype ):
147+ if solve is linear_cg :
148+ from torchsparsegradutils .utils .linear_cg import LinearCGSettings
149+
150+ return {"settings" : LinearCGSettings (cg_tolerance = 1e-5 , max_cg_iterations = 1000 )}
151+ if solve is minres :
152+ from torchsparsegradutils .utils .minres import MINRESSettings
153+
154+ tolerance = 1e-6 if value_dtype == torch .float32 else 1e-10
155+ return {"settings" : MINRESSettings (minres_tolerance = tolerance , max_cg_iterations = 1000 )}
156+ return {}
157+
158+
159+ def _higher_order_spd_tolerances (value_dtype ):
160+ if value_dtype == torch .float32 :
161+ return {
162+ "grad" : (1e-3 , 1e-3 ),
163+ "hess" : (5e-2 , 5e-2 ),
164+ }
165+ return {
166+ "grad" : (1e-5 , 1e-5 ),
167+ "hess" : (5e-4 , 5e-4 ),
168+ }
169+
170+
45171# Define Fixtures
46172
47173
@@ -260,3 +386,99 @@ def test_kwargs_with_different_solvers_same_matrix():
260386 # All solutions should be close to each other
261387 assert torch .allclose (X_cg , X_minres , atol = atol , rtol = rtol )
262388 assert torch .allclose (X_bicgstab , X_minres , atol = atol , rtol = rtol )
389+
390+
391+ @pytest .mark .parametrize ("base_solve" , [linear_cg , minres ], ids = [solve_id (linear_cg ), solve_id (minres )])
392+ def test_sparse_generic_solve_higher_order_create_graph_no_out_error (layout , base_solve , device , value_dtype ):
393+ torch .manual_seed (0 )
394+
395+ theta = torch .randn (8 , dtype = value_dtype , device = device , requires_grad = True )
396+ A , _ = _make_differentiable_tridiag_spd (theta , layout )
397+ B = torch .randn (8 , 2 , dtype = value_dtype , device = device )
398+
399+ kwargs = _settings_for_higher_order_solve (base_solve , value_dtype )
400+ loss = sparse_generic_solve (A , B , solve = base_solve , transpose_solve = base_solve , ** kwargs ).sum ()
401+
402+ grad_theta = torch .autograd .grad (loss , theta , create_graph = True )[0 ]
403+
404+ assert grad_theta .requires_grad
405+ assert torch .isfinite (grad_theta ).all ()
406+
407+ second = torch .autograd .grad (grad_theta .sum (), theta )[0 ]
408+ assert torch .isfinite (second ).all ()
409+
410+
411+ @pytest .mark .parametrize ("base_solve" , [linear_cg , minres ], ids = [solve_id (linear_cg ), solve_id (minres )])
412+ def test_sparse_generic_solve_higher_order_matches_dense_reference (layout , base_solve , device , value_dtype ):
413+ torch .manual_seed (1 )
414+
415+ theta_sparse = torch .randn (6 , dtype = value_dtype , device = device , requires_grad = True )
416+ theta_dense = theta_sparse .detach ().clone ().requires_grad_ ()
417+
418+ B = torch .randn (6 , 2 , dtype = value_dtype , device = device )
419+
420+ A_sparse , _ = _make_differentiable_tridiag_spd (theta_sparse , layout )
421+ _ , A_dense = _make_differentiable_tridiag_spd (theta_dense , torch .sparse_coo )
422+
423+ kwargs = _settings_for_higher_order_solve (base_solve , value_dtype )
424+ tolerances = _higher_order_spd_tolerances (value_dtype )
425+
426+ out_sparse = sparse_generic_solve (A_sparse , B , solve = base_solve , transpose_solve = base_solve , ** kwargs )
427+ out_dense = torch .linalg .solve (A_dense , B )
428+
429+ loss_sparse = out_sparse .square ().sum ()
430+ loss_dense = out_dense .square ().sum ()
431+
432+ grad_sparse = torch .autograd .grad (loss_sparse , theta_sparse , create_graph = True )[0 ]
433+ grad_dense = torch .autograd .grad (loss_dense , theta_dense , create_graph = True )[0 ]
434+
435+ hess_vec_sparse = torch .autograd .grad (grad_sparse .sum (), theta_sparse )[0 ]
436+ hess_vec_dense = torch .autograd .grad (grad_dense .sum (), theta_dense )[0 ]
437+
438+ grad_atol , grad_rtol = tolerances ["grad" ]
439+ hess_atol , hess_rtol = tolerances ["hess" ]
440+ assert torch .allclose (grad_sparse , grad_dense , atol = grad_atol , rtol = grad_rtol )
441+ assert torch .allclose (hess_vec_sparse , hess_vec_dense , atol = hess_atol , rtol = hess_rtol )
442+
443+
444+ def test_sparse_generic_solve_higher_order_nonsymmetric_bicgstab_matches_dense_reference (layout , device , value_dtype ):
445+ torch .manual_seed (2 )
446+
447+ n = 6
448+ theta_sparse = torch .randn (3 * n - 2 , dtype = value_dtype , device = device , requires_grad = True )
449+ theta_dense = theta_sparse .detach ().clone ().requires_grad_ ()
450+
451+ B = torch .randn (n , 2 , dtype = value_dtype , device = device )
452+
453+ A_sparse , _ = _make_differentiable_nonsymmetric_tridiag (theta_sparse , layout )
454+ _ , A_dense = _make_differentiable_nonsymmetric_tridiag (theta_dense , torch .sparse_coo )
455+ assert not torch .allclose (A_dense , A_dense .T )
456+
457+ kwargs = _bicgstab_higher_order_kwargs (value_dtype )
458+ tolerances = _higher_order_bicgstab_tolerances (value_dtype )
459+
460+ out_sparse = sparse_generic_solve (
461+ A_sparse ,
462+ B ,
463+ solve = bicgstab ,
464+ transpose_solve = _bicgstab_transpose ,
465+ ** kwargs ,
466+ )
467+ out_dense = torch .linalg .solve (A_dense , B )
468+ output_atol , output_rtol = tolerances ["output" ]
469+ assert torch .allclose (A_dense @ out_sparse , B , atol = output_atol , rtol = output_rtol )
470+
471+ loss_sparse = out_sparse .square ().sum ()
472+ loss_dense = out_dense .square ().sum ()
473+
474+ grad_sparse = torch .autograd .grad (loss_sparse , theta_sparse , create_graph = True )[0 ]
475+ grad_dense = torch .autograd .grad (loss_dense , theta_dense , create_graph = True )[0 ]
476+
477+ hess_vec_sparse = torch .autograd .grad (grad_sparse .sum (), theta_sparse )[0 ]
478+ hess_vec_dense = torch .autograd .grad (grad_dense .sum (), theta_dense )[0 ]
479+
480+ grad_atol , grad_rtol = tolerances ["grad" ]
481+ hess_atol , hess_rtol = tolerances ["hess" ]
482+ assert torch .allclose (out_sparse , out_dense , atol = output_atol , rtol = output_rtol )
483+ assert torch .allclose (grad_sparse , grad_dense , atol = grad_atol , rtol = grad_rtol )
484+ assert torch .allclose (hess_vec_sparse , hess_vec_dense , atol = hess_atol , rtol = hess_rtol )
0 commit comments