torchsparsegradutils 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {torchsparsegradutils-0.2.4/torchsparsegradutils.egg-info → torchsparsegradutils-0.2.6}/PKG-INFO +11 -8
  2. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/README.md +10 -7
  3. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/docs/source/conf.py +2 -2
  4. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/pyproject.toml +7 -1
  5. torchsparsegradutils-0.2.6/torchsparsegradutils/_compat.py +48 -0
  6. torchsparsegradutils-0.2.6/torchsparsegradutils/benchmarks/linear_cg_convergence.py +224 -0
  7. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_generic_solve_rand.py +2 -2
  8. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_generic_solve_suite.py +3 -4
  9. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_triangular_solve_rand.py +16 -12
  10. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_triangular_solve_suitesparse.py +15 -12
  11. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/sparse_solve.py +8 -10
  12. torchsparsegradutils-0.2.6/torchsparsegradutils/tests/test_deprecated_torch_apis.py +62 -0
  13. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_integration_pairwise_sparse_mvn.py +7 -7
  14. torchsparsegradutils-0.2.6/torchsparsegradutils/tests/test_linear_cg.py +366 -0
  15. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_minres.py +1 -1
  16. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_quickstart_guide.py +2 -7
  17. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_random.py +1 -1
  18. torchsparsegradutils-0.2.6/torchsparsegradutils/tests/test_release_version.py +94 -0
  19. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_sparse_triangular_solve.py +104 -79
  20. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/utils/__init__.py +2 -1
  21. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/utils/linear_cg.py +228 -67
  22. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/utils/lsmr.py +6 -6
  23. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/utils/minres.py +3 -3
  24. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6/torchsparsegradutils.egg-info}/PKG-INFO +11 -8
  25. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils.egg-info/SOURCES.txt +4 -0
  26. torchsparsegradutils-0.2.4/torchsparsegradutils/tests/test_linear_cg.py +0 -104
  27. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/LICENSE +0 -0
  28. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/MANIFEST.in +0 -0
  29. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/setup.cfg +0 -0
  30. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/setup.py +0 -0
  31. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/__init__.py +0 -0
  32. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/__init__.py +0 -0
  33. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/batched_sparse_mm_rand.py +0 -0
  34. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/benchmark_suite.py +0 -0
  35. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/benchmark_utils.py +0 -0
  36. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_bidir_logsumexp_rand.py +0 -0
  37. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_bidir_logsumexp_suitesparse.py +0 -0
  38. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_logsumexp_rand.py +0 -0
  39. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_logsumexp_suitesparse.py +0 -0
  40. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_mm_rand.py +0 -0
  41. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/sparse_mm_suite.py +0 -0
  42. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/benchmarks/visualize_benchmark_results.py +0 -0
  43. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/cupy/__init__.py +0 -0
  44. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/cupy/cupy_bindings.py +0 -0
  45. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/cupy/cupy_sparse_solve.py +0 -0
  46. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/distributions/__init__.py +0 -0
  47. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/distributions/constraints.py +0 -0
  48. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/distributions/sparse_multivariate_normal.py +0 -0
  49. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/encoders/__init__.py +0 -0
  50. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/encoders/pairwise_encoder.py +0 -0
  51. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/encoders/pairwise_voxel_encoder.py +0 -0
  52. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/indexed_matmul.py +0 -0
  53. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/jax/__init__.py +0 -0
  54. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/jax/jax_bindings.py +0 -0
  55. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/jax/jax_sparse_solve.py +0 -0
  56. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/sparse_logsumexp.py +0 -0
  57. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/sparse_lstsq.py +0 -0
  58. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/sparse_matmul.py +0 -0
  59. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/__init__.py +0 -0
  60. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/conftest.py +0 -0
  61. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_bicgstab.py +0 -0
  62. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_config.py +0 -0
  63. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_cupy_bindings.py +0 -0
  64. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_cupy_sparse_solve.py +0 -0
  65. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_dist_stats_helpers.py +0 -0
  66. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_distributions.py +0 -0
  67. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_doctests.py +0 -0
  68. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_encoders.py +0 -0
  69. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_indexed_matmul.py +0 -0
  70. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_jax_bindings.py +0 -0
  71. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_jax_sparse_solve.py +0 -0
  72. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_lsmr.py +0 -0
  73. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_params/czyx_shifts.yaml +0 -0
  74. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_params/pairwise_coo_indices.yaml +0 -0
  75. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_params/xyz_coords.yaml +0 -0
  76. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_sparse_bidir_logsumexp.py +0 -0
  77. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_sparse_logsumexp.py +0 -0
  78. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_sparse_lstsq.py +0 -0
  79. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_sparse_matmul.py +0 -0
  80. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_sparse_solve.py +0 -0
  81. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/tests/test_utils.py +0 -0
  82. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/utils/bicgstab.py +0 -0
  83. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/utils/dist_stats_helpers.py +0 -0
  84. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/utils/random_sparse.py +0 -0
  85. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils/utils/utils.py +0 -0
  86. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils.egg-info/dependency_links.txt +0 -0
  87. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils.egg-info/not-zip-safe +0 -0
  88. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils.egg-info/requires.txt +0 -0
  89. {torchsparsegradutils-0.2.4 → torchsparsegradutils-0.2.6}/torchsparsegradutils.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: torchsparsegradutils
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: A collection of utility functions to work with PyTorch sparse tensors
5
5
  Author-email: CAI4CAI research group <contact@cai4cai.uk>
6
6
  License-Expression: Apache-2.0
@@ -239,15 +239,18 @@ batch_size, event_size = 2, 1000
239
239
  loc = torch.zeros(batch_size, event_size)
240
240
 
241
241
  # Example 1: LDL^T parameterization (numerically stable for precision matrices)
242
- # Create sparse lower triangular matrix (unit triangular, no diagonal)
242
+ # Create sparse lower triangular matrix (strictly lower triangular, no diagonal)
243
+
243
244
  scale_tril = rand_sparse_tri(
244
245
  (batch_size, event_size, event_size),
245
- nnz=5000, # 5000 non-zeros for 1M parameters (0.5% sparsity)
246
+ nnz=5000, # 5000 non-zeros for 1000x1000 dense matrix (99.5% sparsity)
246
247
  layout=torch.sparse_csr,
247
248
  upper=False,
248
- unit_triangular=True # Unit triangular for LDL^T
249
+ strict=True # Strict triangular (exclude diagonal)
249
250
  )
250
251
 
252
+ scale_tril.requires_grad_(True)
253
+
251
254
  # Diagonal component for LDL^T parameterization
252
255
  diagonal = torch.ones(batch_size, event_size) * 0.5
253
256
 
@@ -255,7 +258,7 @@ diagonal = torch.ones(batch_size, event_size) * 0.5
255
258
  dist_ldlt = SparseMultivariateNormal(
256
259
  loc=loc,
257
260
  diagonal=diagonal,
258
- scale_tril=scale_tril # Unit lower triangular
261
+ scale_tril=scale_tril # Strictly lower triangular
259
262
  )
260
263
 
261
264
  # Example 2: LL^T parameterization (standard Cholesky)
@@ -264,7 +267,7 @@ scale_tril_chol = rand_sparse_tri(
264
267
  nnz=5000,
265
268
  layout=torch.sparse_csr,
266
269
  upper=False,
267
- unit_triangular=False # Include diagonal for LL^T
270
+ strict=False # Include diagonal
268
271
  )
269
272
 
270
273
  # Create distribution with LL^T parameterization
@@ -279,7 +282,7 @@ precision_tril = rand_sparse_tri(
279
282
  nnz=5000,
280
283
  layout=torch.sparse_csr,
281
284
  upper=False,
282
- unit_triangular=True
285
+ strict=True
283
286
  )
284
287
 
285
288
  precision_diagonal = torch.ones(batch_size, event_size) * 2.0
@@ -287,7 +290,7 @@ precision_diagonal = torch.ones(batch_size, event_size) * 2.0
287
290
  dist_precision = SparseMultivariateNormal(
288
291
  loc=loc,
289
292
  diagonal=precision_diagonal,
290
- precision_tril=precision_tril # Unit triangular precision factor
293
+ precision_tril=precision_tril # Strictly triangular precision factor
291
294
  )
292
295
 
293
296
  # Sample with gradient support
@@ -192,15 +192,18 @@ batch_size, event_size = 2, 1000
192
192
  loc = torch.zeros(batch_size, event_size)
193
193
 
194
194
  # Example 1: LDL^T parameterization (numerically stable for precision matrices)
195
- # Create sparse lower triangular matrix (unit triangular, no diagonal)
195
+ # Create sparse lower triangular matrix (strictly lower triangular, no diagonal)
196
+
196
197
  scale_tril = rand_sparse_tri(
197
198
  (batch_size, event_size, event_size),
198
- nnz=5000, # 5000 non-zeros for 1M parameters (0.5% sparsity)
199
+ nnz=5000, # 5000 non-zeros for 1000x1000 dense matrix (99.5% sparsity)
199
200
  layout=torch.sparse_csr,
200
201
  upper=False,
201
- unit_triangular=True # Unit triangular for LDL^T
202
+ strict=True # Strict triangular (exclude diagonal)
202
203
  )
203
204
 
205
+ scale_tril.requires_grad_(True)
206
+
204
207
  # Diagonal component for LDL^T parameterization
205
208
  diagonal = torch.ones(batch_size, event_size) * 0.5
206
209
 
@@ -208,7 +211,7 @@ diagonal = torch.ones(batch_size, event_size) * 0.5
208
211
  dist_ldlt = SparseMultivariateNormal(
209
212
  loc=loc,
210
213
  diagonal=diagonal,
211
- scale_tril=scale_tril # Unit lower triangular
214
+ scale_tril=scale_tril # Strictly lower triangular
212
215
  )
213
216
 
214
217
  # Example 2: LL^T parameterization (standard Cholesky)
@@ -217,7 +220,7 @@ scale_tril_chol = rand_sparse_tri(
217
220
  nnz=5000,
218
221
  layout=torch.sparse_csr,
219
222
  upper=False,
220
- unit_triangular=False # Include diagonal for LL^T
223
+ strict=False # Include diagonal
221
224
  )
222
225
 
223
226
  # Create distribution with LL^T parameterization
@@ -232,7 +235,7 @@ precision_tril = rand_sparse_tri(
232
235
  nnz=5000,
233
236
  layout=torch.sparse_csr,
234
237
  upper=False,
235
- unit_triangular=True
238
+ strict=True
236
239
  )
237
240
 
238
241
  precision_diagonal = torch.ones(batch_size, event_size) * 2.0
@@ -240,7 +243,7 @@ precision_diagonal = torch.ones(batch_size, event_size) * 2.0
240
243
  dist_precision = SparseMultivariateNormal(
241
244
  loc=loc,
242
245
  diagonal=precision_diagonal,
243
- precision_tril=precision_tril # Unit triangular precision factor
246
+ precision_tril=precision_tril # Strictly triangular precision factor
244
247
  )
245
248
 
246
249
  # Sample with gradient support
@@ -15,8 +15,8 @@ sys.path.insert(0, os.path.abspath("../../"))
15
15
  project = "torchsparsegradutils"
16
16
  copyright = "2026, CAI4CAI research group"
17
17
  author = "CAI4CAI research group"
18
- release = "0.2.4"
19
- version = "0.2.4"
18
+ release = "0.2.6"
19
+ version = "0.2.6"
20
20
 
21
21
  # -- General configuration ---------------------------------------------------
22
22
  # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "torchsparsegradutils"
7
- version = "0.2.4"
7
+ version = "0.2.6"
8
8
  description = "A collection of utility functions to work with PyTorch sparse tensors"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -105,6 +105,12 @@ testpaths = [
105
105
  pythonpath = [
106
106
  "torchsparsegradutils/tests",
107
107
  ]
108
+ # Fail tests when deprecated upstream APIs are exercised outside intentional compatibility paths.
109
+ filterwarnings = [
110
+ 'error:torch\.norm is deprecated.*:UserWarning',
111
+ 'error:torch\.triangular_solve is deprecated.*:UserWarning',
112
+ 'error:.*torch\.jit\.script.*is deprecated.*:DeprecationWarning',
113
+ ]
108
114
  doctest_optionflags = [
109
115
  "NORMALIZE_WHITESPACE",
110
116
  "IGNORE_EXCEPTION_DETAIL",
@@ -0,0 +1,48 @@
1
+ """Compatibility helpers for PyTorch APIs without feature-equivalent replacements."""
2
+
3
+ import warnings
4
+
5
+ import torch
6
+
7
+
8
+ def linalg_solve_triangular_compat(
9
+ A: torch.Tensor,
10
+ B: torch.Tensor,
11
+ *,
12
+ upper: bool,
13
+ unitriangular: bool = False,
14
+ transpose: bool = False,
15
+ ) -> torch.Tensor:
16
+ """Solve a triangular system with the appropriate dense or sparse backend.
17
+
18
+ Dense coefficient matrices use ``torch.linalg.solve_triangular``. Sparse
19
+ matrices continue to use the isolated legacy call because the replacement
20
+ API does not support the sparse layouts required by this package. See
21
+ https://github.com/pytorch/pytorch/issues/87358 for upstream sparse feature
22
+ parity tracking.
23
+ """
24
+ if A.layout == torch.strided:
25
+ if transpose:
26
+ A = A.transpose(-2, -1)
27
+ upper = not upper
28
+
29
+ return torch.linalg.solve_triangular(
30
+ A,
31
+ B,
32
+ upper=upper,
33
+ unitriangular=unitriangular,
34
+ )
35
+
36
+ with warnings.catch_warnings():
37
+ warnings.filterwarnings(
38
+ "ignore",
39
+ message=r"torch\.triangular_solve is deprecated.*",
40
+ category=UserWarning,
41
+ )
42
+ return torch.triangular_solve(
43
+ B,
44
+ A,
45
+ upper=upper,
46
+ transpose=transpose,
47
+ unitriangular=unitriangular,
48
+ ).solution
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env python3
2
+ """Compare linear-CG convergence before and after the diagnostics fix.
3
+
4
+ This benchmark focuses on numerical behavior rather than throughput. It loads
5
+ the historical implementation from Git so both solvers run in one Python
6
+ environment on identical deterministic inputs.
7
+
8
+ Example
9
+ -------
10
+ python -m torchsparsegradutils.benchmarks.linear_cg_convergence \
11
+ --baseline-ref ea7b8f0 --repeats 100
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import statistics
19
+ import subprocess
20
+ import time
21
+ import types
22
+ import warnings
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+ from typing import Callable
26
+
27
+ import torch
28
+
29
+ from torchsparsegradutils.utils import linear_cg
30
+
31
+ REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
32
+ LINEAR_CG_PATH = "torchsparsegradutils/utils/linear_cg.py"
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class Problem:
37
+ name: str
38
+ matrix: torch.Tensor
39
+ rhs: torch.Tensor
40
+ tolerance: float
41
+ max_iter: int
42
+ initial_guess: torch.Tensor | None = None
43
+
44
+
45
+ class CountedMatmul:
46
+ def __init__(self, matrix: torch.Tensor):
47
+ self.matrix = matrix
48
+ self.calls = 0
49
+
50
+ def __call__(self, value: torch.Tensor) -> torch.Tensor:
51
+ self.calls += 1
52
+ return self.matrix @ value
53
+
54
+
55
+ def load_historical_linear_cg(revision: str) -> Callable[..., torch.Tensor]:
56
+ """Load ``linear_cg`` from a Git revision without changing the worktree."""
57
+ command = ["git", "-C", str(REPOSITORY_ROOT), "show", f"{revision}:{LINEAR_CG_PATH}"]
58
+ try:
59
+ completed = subprocess.run(command, check=True, capture_output=True, text=True)
60
+ except FileNotFoundError as error:
61
+ raise RuntimeError("Cannot load the historical solver because the Git executable was not found") from error
62
+ except subprocess.CalledProcessError as error:
63
+ detail = error.stderr.strip() or "Git could not resolve the requested revision and file"
64
+ raise RuntimeError(
65
+ f"Cannot load the historical solver from revision {revision!r}. "
66
+ f"Run this benchmark from a Git checkout containing that revision. Git reported: {detail}"
67
+ ) from error
68
+ module = types.ModuleType("historical_linear_cg")
69
+ exec(compile(completed.stdout, f"{revision}:{LINEAR_CG_PATH}", "exec"), module.__dict__)
70
+ return module.linear_cg
71
+
72
+
73
+ def make_problems(device: torch.device) -> list[Problem]:
74
+ dtype = torch.float64
75
+
76
+ size = 36
77
+ diagonal = torch.full((size,), 2.0, dtype=dtype, device=device)
78
+ off_diagonal = torch.full((size - 1,), -0.25, dtype=dtype, device=device)
79
+ tight_matrix = torch.diag(diagonal) + torch.diag(off_diagonal, diagonal=1) + torch.diag(off_diagonal, diagonal=-1)
80
+ tight_rhs = torch.zeros(size, dtype=dtype, device=device)
81
+ tight_rhs[size // 2] = 1
82
+
83
+ size = 40
84
+ multiple_matrix = torch.diag(torch.linspace(1.0, 100.0, size, dtype=dtype, device=device))
85
+ multiple_rhs = torch.zeros((size, 101), dtype=dtype, device=device)
86
+ multiple_rhs[:, -1] = 1
87
+
88
+ zero_matrix = torch.diag(torch.tensor([1.0, 2.0, 3.0], dtype=dtype, device=device))
89
+ zero_rhs = torch.zeros(3, dtype=dtype, device=device)
90
+
91
+ return [
92
+ Problem("tight_tolerance", tight_matrix, tight_rhs, tolerance=1e-12, max_iter=200),
93
+ Problem("multiple_rhs", multiple_matrix, multiple_rhs, tolerance=1e-4, max_iter=size),
94
+ Problem(
95
+ "zero_rhs_nonzero_guess",
96
+ zero_matrix,
97
+ zero_rhs,
98
+ tolerance=1e-5,
99
+ max_iter=20,
100
+ initial_guess=torch.ones_like(zero_rhs),
101
+ ),
102
+ ]
103
+
104
+
105
+ def synchronize(device: torch.device) -> None:
106
+ if device.type == "cuda":
107
+ torch.cuda.synchronize(device)
108
+
109
+
110
+ def relative_residual_per_rhs(matrix: torch.Tensor, solution: torch.Tensor, rhs: torch.Tensor) -> torch.Tensor:
111
+ if rhs.ndim == 1:
112
+ rhs = rhs.unsqueeze(-1)
113
+ solution = solution.unsqueeze(-1)
114
+ residual_norm = torch.linalg.vector_norm(rhs - matrix @ solution, dim=-2)
115
+ rhs_norm = torch.linalg.vector_norm(rhs, dim=-2)
116
+ return residual_norm / rhs_norm.masked_fill(rhs_norm.eq(0), 1)
117
+
118
+
119
+ def run_once(
120
+ solver: Callable[..., torch.Tensor],
121
+ problem: Problem,
122
+ *,
123
+ fixed: bool,
124
+ ) -> tuple[torch.Tensor, int, int, str]:
125
+ matmul = CountedMatmul(problem.matrix)
126
+ arguments = {
127
+ "tolerance": problem.tolerance,
128
+ "max_iter": problem.max_iter,
129
+ "initial_guess": problem.initial_guess,
130
+ }
131
+ with warnings.catch_warnings():
132
+ warnings.simplefilter("ignore", UserWarning)
133
+ if fixed:
134
+ solution, info = solver(matmul, problem.rhs, return_info=True, **arguments)
135
+ return solution, info.iterations, matmul.calls, info.reason
136
+ solution = solver(matmul, problem.rhs, **arguments)
137
+ # The historical solver performs one initial matvec followed by one per
138
+ # iteration and does not recompute the true residual before returning.
139
+ return solution, max(0, matmul.calls - 1), matmul.calls, "not_reported"
140
+
141
+
142
+ def measure(
143
+ solver: Callable[..., torch.Tensor],
144
+ problem: Problem,
145
+ *,
146
+ fixed: bool,
147
+ warmup: int,
148
+ repeats: int,
149
+ ) -> dict[str, object]:
150
+ for _ in range(warmup):
151
+ run_once(solver, problem, fixed=fixed)
152
+ synchronize(problem.rhs.device)
153
+
154
+ durations = []
155
+ for _ in range(repeats):
156
+ start = time.perf_counter()
157
+ run_once(solver, problem, fixed=fixed)
158
+ synchronize(problem.rhs.device)
159
+ durations.append(time.perf_counter() - start)
160
+
161
+ solution, iterations, matvecs, reason = run_once(solver, problem, fixed=fixed)
162
+ true_residual = relative_residual_per_rhs(problem.matrix, solution, problem.rhs)
163
+ reference = torch.linalg.solve(problem.matrix, problem.rhs)
164
+ error = torch.linalg.vector_norm(solution - reference) / torch.linalg.vector_norm(reference).clamp_min(1)
165
+
166
+ return {
167
+ "iterations": iterations,
168
+ "matvecs": matvecs,
169
+ "reason": reason,
170
+ "true_relative_residual_max": float(true_residual.max()),
171
+ "unconverged_rhs": int((true_residual > problem.tolerance).sum()),
172
+ "relative_solution_error": float(error),
173
+ "median_time_ms": statistics.median(durations) * 1e3,
174
+ }
175
+
176
+
177
+ def parse_args() -> argparse.Namespace:
178
+ parser = argparse.ArgumentParser(description=__doc__)
179
+ parser.add_argument("--baseline-ref", default="ea7b8f0", help="Git revision containing the historical solver")
180
+ parser.add_argument("--device", default="cpu", help="PyTorch device (default: cpu)")
181
+ parser.add_argument("--warmup", type=int, default=5)
182
+ parser.add_argument("--repeats", type=int, default=50)
183
+ arguments = parser.parse_args()
184
+ if arguments.warmup < 0 or arguments.repeats < 1:
185
+ parser.error("--warmup must be nonnegative and --repeats must be positive")
186
+ return arguments
187
+
188
+
189
+ def main() -> None:
190
+ arguments = parse_args()
191
+ device = torch.device(arguments.device)
192
+ historical_linear_cg = load_historical_linear_cg(arguments.baseline_ref)
193
+ results = {}
194
+ for problem in make_problems(device):
195
+ results[problem.name] = {
196
+ "baseline": measure(
197
+ historical_linear_cg,
198
+ problem,
199
+ fixed=False,
200
+ warmup=arguments.warmup,
201
+ repeats=arguments.repeats,
202
+ ),
203
+ "fixed": measure(
204
+ linear_cg,
205
+ problem,
206
+ fixed=True,
207
+ warmup=arguments.warmup,
208
+ repeats=arguments.repeats,
209
+ ),
210
+ }
211
+
212
+ payload = {
213
+ "baseline_ref": arguments.baseline_ref,
214
+ "device": str(device),
215
+ "torch_version": torch.__version__,
216
+ "warmup": arguments.warmup,
217
+ "repeats": arguments.repeats,
218
+ "results": results,
219
+ }
220
+ print(json.dumps(payload, indent=2, sort_keys=True))
221
+
222
+
223
+ if __name__ == "__main__":
224
+ main()
@@ -225,8 +225,8 @@ def run_sparse_generic_solve_benchmark():
225
225
  else:
226
226
  residual = A_sparse @ x - B
227
227
 
228
- resnorm = torch.norm(residual).cpu().item()
229
- B_norm = torch.norm(B).cpu().item()
228
+ resnorm = torch.linalg.vector_norm(residual).cpu().item()
229
+ B_norm = torch.linalg.vector_norm(B).cpu().item()
230
230
  relative_resnorm = resnorm / B_norm if B_norm > 0 else 0.0
231
231
 
232
232
  # Print result with residual norm
@@ -201,10 +201,9 @@ def run_sparse_solve_benchmark():
201
201
  with torch.no_grad():
202
202
  x = alg_fn(A_sparse, B)
203
203
  residual = A_sparse @ x - B
204
- resnorm = torch.norm(residual).cpu().item()
205
- relative_resnorm = (
206
- resnorm / torch.norm(B).cpu().item() if torch.norm(B).cpu().item() > 0 else 0.0
207
- )
204
+ resnorm = torch.linalg.vector_norm(residual).cpu().item()
205
+ B_norm = torch.linalg.vector_norm(B).cpu().item()
206
+ relative_resnorm = resnorm / B_norm if B_norm > 0 else 0.0
208
207
 
209
208
  # Print result with residual norm
210
209
  print_result_row(
@@ -27,6 +27,7 @@ from cupyx.scipy.sparse.linalg._solve import spsolve_triangular
27
27
  from tqdm import tqdm
28
28
 
29
29
  from torchsparsegradutils import sparse_triangular_solve
30
+ from torchsparsegradutils._compat import linalg_solve_triangular_compat
30
31
  from torchsparsegradutils.cupy.cupy_sparse_solve import sparse_solve_c4t
31
32
  from torchsparsegradutils.utils import rand_sparse, rand_sparse_tri
32
33
 
@@ -58,16 +59,19 @@ LAYOUTS = [torch.sparse_coo, torch.sparse_csr]
58
59
 
59
60
  ALGORITHMS = [
60
61
  (
61
- "dense.triangular_solve",
62
- lambda A, B: torch.triangular_solve(
63
- B, A.to_dense(), upper=UPPER, unitriangular=UNITRIANGULAR, transpose=TRANSPOSE
64
- ).solution,
62
+ "dense.linalg.solve_triangular",
63
+ lambda A, B: torch.linalg.solve_triangular(
64
+ A.to_dense().transpose(-2, -1) if TRANSPOSE else A.to_dense(),
65
+ B,
66
+ upper=not UPPER if TRANSPOSE else UPPER,
67
+ unitriangular=UNITRIANGULAR,
68
+ ),
65
69
  ),
66
70
  (
67
- "torch_triangular_solve",
68
- lambda A, B: torch.triangular_solve(
69
- B, A, upper=UPPER, unitriangular=UNITRIANGULAR, transpose=TRANSPOSE
70
- ).solution,
71
+ "linalg_solve_triangular_compat",
72
+ lambda A, B: linalg_solve_triangular_compat(
73
+ A, B, upper=UPPER, unitriangular=UNITRIANGULAR, transpose=TRANSPOSE
74
+ ),
71
75
  ),
72
76
  (
73
77
  "sparse_triangular_solve",
@@ -153,8 +157,8 @@ def run_sparse_triangular_solve_benchmark():
153
157
  with torch.no_grad():
154
158
  x = alg_fn(A_sparse, B)
155
159
  residual = A_sparse @ x - B
156
- resnorm = torch.norm(residual).cpu().item()
157
- relative_resnorm = resnorm / torch.norm(B).cpu().item()
160
+ resnorm = torch.linalg.vector_norm(residual).cpu().item()
161
+ relative_resnorm = resnorm / torch.linalg.vector_norm(B).cpu().item()
158
162
 
159
163
  # # Calculate residual norm for solution accuracy
160
164
  # with torch.no_grad():
@@ -165,8 +169,8 @@ def run_sparse_triangular_solve_benchmark():
165
169
  # else:
166
170
  # Ax = A_sparse @ x
167
171
  # residual = Ax - B
168
- # resnorm = torch.norm(residual).cpu().item()
169
- # relative_resnorm = resnorm / torch.norm(B).cpu().item()
172
+ # resnorm = torch.linalg.vector_norm(residual).cpu().item()
173
+ # relative_resnorm = resnorm / torch.linalg.vector_norm(B).cpu().item()
170
174
 
171
175
  # Print result with residual norm
172
176
  print_result_row(
@@ -34,6 +34,7 @@ from cupyx.scipy.sparse.linalg._solve import spsolve_triangular
34
34
  from tqdm import tqdm
35
35
 
36
36
  from torchsparsegradutils import sparse_triangular_solve
37
+ from torchsparsegradutils._compat import linalg_solve_triangular_compat
37
38
  from torchsparsegradutils.cupy.cupy_sparse_solve import sparse_solve_c4t
38
39
 
39
40
  # from jax.lax.linalg import triangular_solve # NOTE: jax doesn't have a sparse triangular solve
@@ -57,16 +58,19 @@ LAYOUTS = [torch.sparse_coo, torch.sparse_csr]
57
58
 
58
59
  ALGORITHMS = [
59
60
  (
60
- "dense.triangular_solve",
61
- lambda A, B: torch.triangular_solve(
62
- B, A.to_dense(), upper=UPPER, unitriangular=UNITRIANGULAR, transpose=TRANSPOSE
63
- ).solution,
61
+ "dense.linalg.solve_triangular",
62
+ lambda A, B: torch.linalg.solve_triangular(
63
+ A.to_dense().transpose(-2, -1) if TRANSPOSE else A.to_dense(),
64
+ B,
65
+ upper=not UPPER if TRANSPOSE else UPPER,
66
+ unitriangular=UNITRIANGULAR,
67
+ ),
64
68
  ),
65
69
  (
66
- "torch_triangular_solve",
67
- lambda A, B: torch.triangular_solve(
68
- B, A, upper=UPPER, unitriangular=UNITRIANGULAR, transpose=TRANSPOSE
69
- ).solution,
70
+ "linalg_solve_triangular_compat",
71
+ lambda A, B: linalg_solve_triangular_compat(
72
+ A, B, upper=UPPER, unitriangular=UNITRIANGULAR, transpose=TRANSPOSE
73
+ ),
70
74
  ),
71
75
  (
72
76
  "sparse_triangular_solve",
@@ -185,10 +189,9 @@ def run_triangular_solve_benchmark():
185
189
  # Use the triangular matrix A_sparse (not A_full) for residual calculation
186
190
  # This ensures the residual is computed correctly for the actual triangular system solved
187
191
  residual = A_sparse @ x - B
188
- resnorm = torch.norm(residual).cpu().item()
189
- relative_resnorm = (
190
- resnorm / torch.norm(B).cpu().item() if torch.norm(B).cpu().item() > 0 else 0.0
191
- )
192
+ resnorm = torch.linalg.vector_norm(residual).cpu().item()
193
+ B_norm = torch.linalg.vector_norm(B).cpu().item()
194
+ relative_resnorm = resnorm / B_norm if B_norm > 0 else 0.0
192
195
 
193
196
  # Print result
194
197
  print_result_row(
@@ -3,6 +3,7 @@ from typing import Callable, Optional, cast
3
3
 
4
4
  import torch
5
5
 
6
+ from torchsparsegradutils._compat import linalg_solve_triangular_compat
6
7
  from torchsparsegradutils.utils import convert_coo_to_csr, sparse_block_diag, sparse_block_diag_split, stack_csr
7
8
 
8
9
 
@@ -154,7 +155,7 @@ class SparseTriangularSolve(torch.autograd.Function):
154
155
  See Also
155
156
  --------
156
157
  sparse_triangular_solve : User-facing function that calls this autograd function.
157
- torch.triangular_solve : PyTorch's native triangular solver.
158
+ torch.linalg.solve_triangular : PyTorch's native dense triangular solver.
158
159
  """
159
160
 
160
161
  @staticmethod
@@ -177,11 +178,9 @@ class SparseTriangularSolve(torch.autograd.Function):
177
178
  A = convert_coo_to_csr(A) # NOTE: triangular solve doesn't work with sparse coo
178
179
  ctx.csr = False
179
180
 
180
- # NOTE: DEPRECATED: Check if a workaround for https://github.com/pytorch/pytorch/issues/88890 is needed
181
-
182
- x = torch.triangular_solve(
183
- B.detach(), A.detach(), upper=upper, unitriangular=unitriangular, transpose=transpose
184
- ).solution
181
+ x = linalg_solve_triangular_compat(
182
+ A.detach(), B.detach(), upper=upper, unitriangular=unitriangular, transpose=transpose
183
+ )
185
184
 
186
185
  x.requires_grad = grad_flag
187
186
  ctx.save_for_backward(A, x.detach())
@@ -199,11 +198,10 @@ class SparseTriangularSolve(torch.autograd.Function):
199
198
  A, x = ctx.saved_tensors
200
199
 
201
200
  # Backprop rule: gradB = A^{-T} grad
202
- # NOTE: DEPRECATED: Check if a workaround for https://github.com/pytorch/pytorch/issues/88890 is needed
203
201
 
204
- gradB = torch.triangular_solve(
205
- grad, A, upper=ctx.upper, transpose=not ctx.transpose, unitriangular=ctx.unitriangular
206
- ).solution
202
+ gradB = linalg_solve_triangular_compat(
203
+ A, grad, upper=ctx.upper, transpose=not ctx.transpose, unitriangular=ctx.unitriangular
204
+ )
207
205
 
208
206
  # The gradient with respect to the matrix A seen as a dense matrix would
209
207
  # lead to a backprop rule as follows
@@ -0,0 +1,62 @@
1
+ """Regression checks for deprecated upstream PyTorch APIs."""
2
+
3
+ import ast
4
+ import re
5
+ from pathlib import Path
6
+
7
+ REPOSITORY_ROOT = Path(__file__).parents[2]
8
+ PACKAGE_ROOT = REPOSITORY_ROOT / "torchsparsegradutils"
9
+ COMPATIBILITY_MODULE = Path("_compat.py")
10
+ DEPRECATED_DOCUMENTATION_NORM = re.compile(r"(?<![\w.])torch\.norm\s*\(")
11
+
12
+
13
+ def attribute_name(node):
14
+ parts = []
15
+ while isinstance(node, ast.Attribute):
16
+ parts.append(node.attr)
17
+ node = node.value
18
+ if isinstance(node, ast.Name):
19
+ parts.append(node.id)
20
+ return ".".join(reversed(parts))
21
+
22
+
23
+ def test_deprecated_torch_calls_are_not_reintroduced():
24
+ violations = []
25
+ compatibility_calls = []
26
+
27
+ for path in PACKAGE_ROOT.rglob("*.py"):
28
+ relative_path = path.relative_to(PACKAGE_ROOT)
29
+ tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
30
+
31
+ for node in ast.walk(tree):
32
+ location = f"{relative_path}:{getattr(node, 'lineno', '?')}"
33
+ if isinstance(node, ast.Attribute) and attribute_name(node) == "torch.jit.script":
34
+ violations.append(location)
35
+
36
+ if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
37
+ continue
38
+
39
+ call_name = attribute_name(node.func)
40
+ if call_name == "torch.norm":
41
+ violations.append(location)
42
+ elif call_name == "torch.triangular_solve":
43
+ if relative_path == COMPATIBILITY_MODULE:
44
+ compatibility_calls.append(location)
45
+ else:
46
+ violations.append(location)
47
+
48
+ assert not violations, "Deprecated PyTorch APIs found:\n" + "\n".join(violations)
49
+ assert len(compatibility_calls) == 1, "Expected one isolated sparse triangular compatibility call"
50
+
51
+
52
+ def test_documentation_does_not_use_deprecated_norm_apis():
53
+ violations = []
54
+ documentation_paths = [REPOSITORY_ROOT / "README.md"]
55
+ documentation_paths.extend((REPOSITORY_ROOT / "docs").rglob("*.md"))
56
+ documentation_paths.extend((REPOSITORY_ROOT / "docs").rglob("*.rst"))
57
+
58
+ for path in documentation_paths:
59
+ if DEPRECATED_DOCUMENTATION_NORM.search(path.read_text(encoding="utf-8")):
60
+ violations.append(str(path.relative_to(REPOSITORY_ROOT)))
61
+
62
+ assert not violations, "Deprecated norm examples found:\n" + "\n".join(violations)