trnsparse 0.3.1__tar.gz → 0.3.2__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 (60) hide show
  1. {trnsparse-0.3.1 → trnsparse-0.3.2}/CHANGELOG.md +29 -0
  2. {trnsparse-0.3.1/trnsparse.egg-info → trnsparse-0.3.2}/PKG-INFO +1 -1
  3. trnsparse-0.3.2/benchmarks/bench_iterative.py +50 -0
  4. trnsparse-0.3.2/docs/iterative_solvers.md +115 -0
  5. {trnsparse-0.3.1 → trnsparse-0.3.2}/pyproject.toml +1 -1
  6. trnsparse-0.3.2/tests/test_iterative.py +135 -0
  7. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse/__init__.py +6 -1
  8. trnsparse-0.3.2/trnsparse/iterative.py +190 -0
  9. {trnsparse-0.3.1 → trnsparse-0.3.2/trnsparse.egg-info}/PKG-INFO +1 -1
  10. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse.egg-info/SOURCES.txt +4 -0
  11. {trnsparse-0.3.1 → trnsparse-0.3.2}/.github/workflows/ci.yml +0 -0
  12. {trnsparse-0.3.1 → trnsparse-0.3.2}/.github/workflows/notify-umbrella.yml +0 -0
  13. {trnsparse-0.3.1 → trnsparse-0.3.2}/.github/workflows/publish.yml +0 -0
  14. {trnsparse-0.3.1 → trnsparse-0.3.2}/.gitignore +0 -0
  15. {trnsparse-0.3.1 → trnsparse-0.3.2}/.pre-commit-config.yaml +0 -0
  16. {trnsparse-0.3.1 → trnsparse-0.3.2}/CLAUDE.md +0 -0
  17. {trnsparse-0.3.1 → trnsparse-0.3.2}/CODE_OF_CONDUCT.md +0 -0
  18. {trnsparse-0.3.1 → trnsparse-0.3.2}/CONTRIBUTING.md +0 -0
  19. {trnsparse-0.3.1 → trnsparse-0.3.2}/LICENSE +0 -0
  20. {trnsparse-0.3.1 → trnsparse-0.3.2}/README.md +0 -0
  21. {trnsparse-0.3.1 → trnsparse-0.3.2}/benchmarks/bench_bsr_spmm.py +0 -0
  22. {trnsparse-0.3.1 → trnsparse-0.3.2}/benchmarks/bench_screening.py +0 -0
  23. {trnsparse-0.3.1 → trnsparse-0.3.2}/benchmarks/bench_spmm.py +0 -0
  24. {trnsparse-0.3.1 → trnsparse-0.3.2}/benchmarks/bench_spmv.py +0 -0
  25. {trnsparse-0.3.1 → trnsparse-0.3.2}/benchmarks/conftest.py +0 -0
  26. {trnsparse-0.3.1 → trnsparse-0.3.2}/docs/api.md +0 -0
  27. {trnsparse-0.3.1 → trnsparse-0.3.2}/docs/architecture.md +0 -0
  28. {trnsparse-0.3.1 → trnsparse-0.3.2}/docs/aws_setup.md +0 -0
  29. {trnsparse-0.3.1 → trnsparse-0.3.2}/docs/benchmarks.md +0 -0
  30. {trnsparse-0.3.1 → trnsparse-0.3.2}/docs/index.md +0 -0
  31. {trnsparse-0.3.1 → trnsparse-0.3.2}/docs/installation.md +0 -0
  32. {trnsparse-0.3.1 → trnsparse-0.3.2}/docs/migration_scipy.md +0 -0
  33. {trnsparse-0.3.1 → trnsparse-0.3.2}/docs/quickstart.md +0 -0
  34. {trnsparse-0.3.1 → trnsparse-0.3.2}/examples/sparse_fock.py +0 -0
  35. {trnsparse-0.3.1 → trnsparse-0.3.2}/infra/terraform/.terraform.lock.hcl +0 -0
  36. {trnsparse-0.3.1 → trnsparse-0.3.2}/infra/terraform/README.md +0 -0
  37. {trnsparse-0.3.1 → trnsparse-0.3.2}/infra/terraform/main.tf +0 -0
  38. {trnsparse-0.3.1 → trnsparse-0.3.2}/mkdocs.yml +0 -0
  39. {trnsparse-0.3.1 → trnsparse-0.3.2}/scripts/bench_to_md.py +0 -0
  40. {trnsparse-0.3.1 → trnsparse-0.3.2}/scripts/run_benchmarks.sh +0 -0
  41. {trnsparse-0.3.1 → trnsparse-0.3.2}/scripts/run_neuron_tests.sh +0 -0
  42. {trnsparse-0.3.1 → trnsparse-0.3.2}/scripts/run_simulator_tests.sh +0 -0
  43. {trnsparse-0.3.1 → trnsparse-0.3.2}/setup.cfg +0 -0
  44. {trnsparse-0.3.1 → trnsparse-0.3.2}/tests/conftest.py +0 -0
  45. {trnsparse-0.3.1 → trnsparse-0.3.2}/tests/test_bsr.py +0 -0
  46. {trnsparse-0.3.1 → trnsparse-0.3.2}/tests/test_formats.py +0 -0
  47. {trnsparse-0.3.1 → trnsparse-0.3.2}/tests/test_nki_bsr.py +0 -0
  48. {trnsparse-0.3.1 → trnsparse-0.3.2}/tests/test_nki_sim.py +0 -0
  49. {trnsparse-0.3.1 → trnsparse-0.3.2}/tests/test_nki_spmm.py +0 -0
  50. {trnsparse-0.3.1 → trnsparse-0.3.2}/tests/test_ops.py +0 -0
  51. {trnsparse-0.3.1 → trnsparse-0.3.2}/tests/test_screening.py +0 -0
  52. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse/formats.py +0 -0
  53. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse/nki/__init__.py +0 -0
  54. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse/nki/dispatch.py +0 -0
  55. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse/nki/kernels.py +0 -0
  56. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse/ops.py +0 -0
  57. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse/screening.py +0 -0
  58. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse.egg-info/dependency_links.txt +0 -0
  59. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse.egg-info/requires.txt +0 -0
  60. {trnsparse-0.3.1 → trnsparse-0.3.2}/trnsparse.egg-info/top_level.txt +0 -0
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.2] — 2026-04-14
9
+
10
+ ### Added
11
+
12
+ - **`cg_bsr`** and **`power_iteration_bsr`** — Conjugate Gradient and
13
+ power iteration on block-sparse row matrices. Plumbing on top of
14
+ `bsr_spmm` (one kernel dispatch per iteration). Closes Phase 1 of
15
+ #22 on-chip iterative solvers.
16
+ - **`jacobi_preconditioner_bsr(A)`** — builds a diagonal preconditioner
17
+ for `cg_bsr`'s `M=` argument.
18
+ - **`bsr_diagonal(A)`** — extracts the main diagonal from a BSR matrix.
19
+ - **`docs/iterative_solvers.md`** — design note covering the v0.3.2
20
+ plumbing and the v0.4.0 fused-kernel goal (#24). Explains the
21
+ architectural win Trainium offers (A SBUF-resident across iterations)
22
+ vs the current per-iteration HBM round-trip.
23
+ - **`tests/test_iterative.py`** — 8 CPU tests including scipy parity at
24
+ `atol=1e-4` on a 128×128 SPD system.
25
+ - **`benchmarks/bench_iterative.py`** — cg_bsr vs scipy.sparse.linalg.cg.
26
+ At 128×128 SPD: scipy 310 μs, trnsparse 369 μs (1.19×).
27
+
28
+ ### Notes
29
+
30
+ - Algorithm body for CG is a local copy of `trnsolver.iterative.cg`;
31
+ kept local to avoid a cross-repo runtime dependency for one function.
32
+ - v0.4.0 will layer the fused CG/power-iteration NKI kernel on top —
33
+ tracked in #24. The API stays stable across the transition; users
34
+ upgrading from v0.3.2 get the fused-kernel speedup automatically
35
+ when the fused path is available.
36
+
8
37
  ## [0.3.1] — 2026-04-14
9
38
 
10
39
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trnsparse
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Sparse matrix operations for AWS Trainium via NKI
5
5
  Author-email: Scott Friedman <scttfrdmn@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,50 @@
1
+ """Iterative-solver benchmarks: trnsparse.cg_bsr vs scipy baseline.
2
+
3
+ The v0.3.2 plumbing dispatches one `bsr_spmm` call per CG iteration.
4
+ On NKI that means one kernel launch + HBM round-trip per iteration.
5
+ Expect the current path to be dominated by dispatch overhead compared
6
+ to scipy's compiled C loop, which motivates the v0.4.0 fused-kernel
7
+ follow-up.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import pytest
13
+ import torch
14
+
15
+ import trnsparse
16
+
17
+
18
+ @pytest.fixture(params=[128, 256])
19
+ def iter_size(request):
20
+ return request.param
21
+
22
+
23
+ @pytest.fixture
24
+ def spd_bsr_and_dense(iter_size):
25
+ torch.manual_seed(0)
26
+ n = iter_size
27
+ M = torch.randn(n, n)
28
+ A_dense = M @ M.T + n * torch.eye(n)
29
+ A_bsr = trnsparse.BSRMatrix.from_dense(A_dense, block_size=128)
30
+ b = torch.randn(n)
31
+ return A_dense, A_bsr, b
32
+
33
+
34
+ def test_cg_bsr_trnsparse(benchmark, spd_bsr_and_dense):
35
+ _, A_bsr, b = spd_bsr_and_dense
36
+ benchmark(lambda: trnsparse.cg_bsr(A_bsr, b, tol=1e-8, max_iter=2 * b.shape[0]))
37
+
38
+
39
+ def test_cg_scipy(benchmark, spd_bsr_and_dense):
40
+ sp = pytest.importorskip("scipy.sparse")
41
+ spla = pytest.importorskip("scipy.sparse.linalg")
42
+ A_dense, _, b = spd_bsr_and_dense
43
+ A_scipy = sp.csr_matrix(A_dense.numpy())
44
+ b_np = b.numpy()
45
+ benchmark(lambda: spla.cg(A_scipy, b_np, rtol=1e-8, maxiter=2 * len(b_np)))
46
+
47
+
48
+ def test_power_iteration_trnsparse(benchmark, spd_bsr_and_dense):
49
+ _, A_bsr, _ = spd_bsr_and_dense
50
+ benchmark(lambda: trnsparse.power_iteration_bsr(A_bsr, max_iter=500, tol=1e-9))
@@ -0,0 +1,115 @@
1
+ # Iterative solvers over BSR
2
+
3
+ trnsparse v0.3.2 adds `cg_bsr` and `power_iteration_bsr` — Conjugate
4
+ Gradient and power iteration on block-sparse row matrices. The API is
5
+ stable; the architectural story below explains why there's a v0.4.0
6
+ follow-up.
7
+
8
+ ## Why this matters
9
+
10
+ Large SPD linear systems and dominant-eigenpair problems show up
11
+ across scientific computing:
12
+
13
+ - **Quantum chemistry**: Hamiltonian eigenvalue problems (HF, DFT,
14
+ CI), response equations (CPSCF).
15
+ - **PDE discretizations**: stiffness-matrix solves for finite element
16
+ methods, graph Laplacian systems.
17
+ - **Graph learning**: spectral embeddings, PageRank-like iterations.
18
+
19
+ The matrix `A` in each case is typically block-sparse (Fock matrices
20
+ after Schwarz screening; FEM stiffness tied to mesh connectivity;
21
+ graph adjacency). BSR is the Trainium-native representation for those
22
+ matrices (see `architecture.md`).
23
+
24
+ ## v0.3.2 — plumbing
25
+
26
+ ```python
27
+ import trnsparse
28
+
29
+ A = trnsparse.BSRMatrix.from_dense(fock_matrix, block_size=128)
30
+ b = compute_rhs()
31
+
32
+ x, iters, rel = trnsparse.cg_bsr(A, b, tol=1e-6, max_iter=1000)
33
+ # Jacobi-preconditioned variant:
34
+ M = trnsparse.jacobi_preconditioner_bsr(A)
35
+ x, iters, rel = trnsparse.cg_bsr(A, b, tol=1e-6, M=M)
36
+
37
+ lam, v, iters = trnsparse.power_iteration_bsr(A, max_iter=500)
38
+ ```
39
+
40
+ Under the hood, each CG iteration calls `bsr_spmm(A, x.unsqueeze(1))`
41
+ once. On the NKI backend that's one kernel dispatch + one HBM
42
+ round-trip per iteration. On CPU it's `torch.sparse`-backed and
43
+ roughly on par with `scipy.sparse.linalg.cg` (benchmarked: 369 μs vs
44
+ 310 μs at 128×128 SPD, 1.19× slower).
45
+
46
+ ## v0.4.0 — fused kernel with SBUF-resident A
47
+
48
+ The architectural claim from
49
+ [#22](https://github.com/trnsci/trnsparse/issues/22): Trainium's 32 GB
50
+ SBUF per NeuronCore fits a 5000×5000 BSR Hamiltonian on-chip. CG
51
+ doesn't need to round-trip `A` to HBM at all — only `x`, `r`, and `p`.
52
+
53
+ The shape of the v0.4.0 kernel:
54
+
55
+ ```python
56
+ @nki.jit
57
+ def _cg_spd_kernel(A_blocks, A_cols, A_row_ptrs, b, max_iter):
58
+ # Load A blocks once into SBUF at the top.
59
+ A_sbuf = nl.load(A_blocks)
60
+
61
+ # State: x, r, p in SBUF registers.
62
+ x = nl.zeros(...)
63
+ r = nl.copy(b)
64
+ p = nl.copy(r)
65
+ rr = nl.reduce(r * r)
66
+
67
+ for k in nl.affine_range(max_iter):
68
+ Ap = _bsr_matvec_sbuf(A_sbuf, A_cols, A_row_ptrs, p) # all SBUF
69
+ pAp = nl.reduce(p * Ap)
70
+ alpha = rr / pAp
71
+ x = x + alpha * p
72
+ r = r - alpha * Ap
73
+ rr_new = nl.reduce(r * r)
74
+ beta = rr_new / rr
75
+ p = r + beta * p
76
+ rr = rr_new
77
+
78
+ return x, residual_norm_history
79
+ ```
80
+
81
+ Fixed `max_iter`, no early exit — returning the full residual history
82
+ lets the host pick the convergence point post-hoc (standard
83
+ NKI-inside-loop constraint: no dynamic control flow).
84
+
85
+ **Expected regime where this wins**: when
86
+ `max_iter × dispatch_overhead > fused_kernel_cost + hbm_load_A_once`.
87
+ For a 4000×4000 BSR Hamiltonian with ~100 iterations to convergence,
88
+ that's roughly an order of magnitude of wall-time reduction.
89
+
90
+ **What needs to land first**:
91
+
92
+ 1. Simulator-iterated kernel skeleton (now tractable thanks to the
93
+ `nki-simulator` CI gate — see the NKI 0.3.0 migration in v0.3.1).
94
+ 2. SBUF sizing model for `A` — some workloads overflow 32 GB; then
95
+ fall back to the v0.3.2 plumbing.
96
+ 3. Dispatcher logic in `cg_bsr` that picks the fused kernel when
97
+ `max_iter * n` exceeds a threshold.
98
+
99
+ Tracked in a dedicated sub-issue on `trnsci/trnsparse`.
100
+
101
+ ## Why CG and power iteration, not GMRES / Lanczos / Davidson
102
+
103
+ v0.3.2 covers the two most common algorithm families:
104
+
105
+ - **CG** for SPD systems — the workhorse for Hamiltonian solves,
106
+ stiffness systems, and many PDE discretizations.
107
+ - **Power iteration** for dominant eigenpairs — the starting point
108
+ for spectral methods, PageRank-like fixed points, and the iteration
109
+ core inside Lanczos / Arnoldi / Davidson.
110
+
111
+ GMRES (general non-symmetric systems), Lanczos / Arnoldi (full
112
+ spectrum), and Davidson (interior eigenvalues) are follow-ups when
113
+ users ask for them. The v0.4.0 fused kernel's structure (load A once,
114
+ iterate on-chip) generalizes trivially to those variants — it's the
115
+ same architectural pattern.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trnsparse"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Sparse matrix operations for AWS Trainium via NKI"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -0,0 +1,135 @@
1
+ """Iterative solver tests on BSR matrices (CPU plumbing — #22 Phase 1)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import pytest
7
+ import torch
8
+
9
+ import trnsparse
10
+
11
+
12
+ def _random_spd_bsr(
13
+ n: int, block_size: int = 128, seed: int = 0
14
+ ) -> tuple[torch.Tensor, trnsparse.BSRMatrix]:
15
+ """Build a dense SPD matrix of size (n, n), return (dense, BSR view).
16
+
17
+ `A = M @ M.T + n*I` guarantees SPD. Block-storing a dense SPD matrix
18
+ is valid — BSR just chooses which blocks to keep; SPD-ness is a
19
+ per-element property that survives the block partition.
20
+ """
21
+ torch.manual_seed(seed)
22
+ M = torch.randn(n, n)
23
+ A_dense = M @ M.T + n * torch.eye(n)
24
+ return A_dense, trnsparse.BSRMatrix.from_dense(A_dense, block_size=block_size)
25
+
26
+
27
+ class TestCgBsr:
28
+ def test_identity_converges_immediately(self):
29
+ """A = I → x = b in one iteration (or zero for zero RHS)."""
30
+ n = 128
31
+ I_dense = torch.eye(n)
32
+ I_bsr = trnsparse.BSRMatrix.from_dense(I_dense, block_size=128)
33
+ b = torch.randn(n)
34
+
35
+ x, iters, rel = trnsparse.cg_bsr(I_bsr, b, tol=1e-8, max_iter=50)
36
+ torch.testing.assert_close(x, b, atol=1e-5, rtol=1e-5)
37
+ assert iters <= 2, f"identity should converge in 1 iter, took {iters}"
38
+ assert rel < 1e-8
39
+
40
+ def test_parity_vs_scipy(self):
41
+ """Solution from cg_bsr matches scipy.sparse.linalg.cg within tol."""
42
+ sp = pytest.importorskip("scipy.sparse")
43
+ spla = pytest.importorskip("scipy.sparse.linalg")
44
+
45
+ n = 128
46
+ A_dense, A_bsr = _random_spd_bsr(n, block_size=128, seed=1)
47
+ b = torch.randn(n)
48
+
49
+ x_ours, iters_ours, rel_ours = trnsparse.cg_bsr(A_bsr, b, tol=1e-8, max_iter=2 * n)
50
+ # scipy's cg takes rtol (relative to ||b||) by default in modern scipy
51
+ A_scipy = sp.csr_matrix(A_dense.numpy())
52
+ x_scipy, info = spla.cg(A_scipy, b.numpy(), rtol=1e-8, maxiter=2 * n)
53
+
54
+ assert info == 0, "scipy CG should converge on this well-conditioned SPD system"
55
+ np.testing.assert_allclose(x_ours.numpy(), x_scipy, atol=1e-4, rtol=1e-4)
56
+ assert rel_ours < 1e-7
57
+
58
+ def test_jacobi_preconditioner_fewer_iters(self):
59
+ """A strongly diagonally-dominant system should converge faster
60
+ with Jacobi preconditioning than without.
61
+ """
62
+ torch.manual_seed(2)
63
+ n = 256
64
+ # Diagonally dominant: large diagonal, small off-diag.
65
+ A_dense = 0.01 * torch.randn(n, n)
66
+ A_dense = 0.5 * (A_dense + A_dense.T) # symmetric
67
+ A_dense = A_dense + torch.diag(1.0 + 10.0 * torch.rand(n)) # SPD
68
+ A_bsr = trnsparse.BSRMatrix.from_dense(A_dense, block_size=128)
69
+ b = torch.randn(n)
70
+
71
+ _, iters_plain, _ = trnsparse.cg_bsr(A_bsr, b, tol=1e-8, max_iter=500)
72
+ M = trnsparse.jacobi_preconditioner_bsr(A_bsr)
73
+ _, iters_precond, _ = trnsparse.cg_bsr(A_bsr, b, tol=1e-8, max_iter=500, M=M)
74
+
75
+ assert iters_precond <= iters_plain, (
76
+ f"Jacobi preconditioner should not make it worse "
77
+ f"(plain={iters_plain}, precond={iters_precond})"
78
+ )
79
+
80
+ def test_zero_rhs_returns_zero(self):
81
+ """b = 0 → x = 0 in 0 iterations."""
82
+ _, A_bsr = _random_spd_bsr(128, block_size=128, seed=3)
83
+ b = torch.zeros(128)
84
+ x, iters, rel = trnsparse.cg_bsr(A_bsr, b)
85
+ torch.testing.assert_close(x, b)
86
+ assert iters == 0
87
+ assert rel == 0.0
88
+
89
+
90
+ class TestPowerIterationBsr:
91
+ def test_dominant_eigenvalue_diagonal(self):
92
+ """Diagonal matrix: dominant eigenvalue is max(diag); eigenvector
93
+ is the corresponding basis vector.
94
+ """
95
+ n = 128
96
+ diag = torch.linspace(0.5, 10.0, n) # strictly increasing; max = 10
97
+ A_dense = torch.diag(diag)
98
+ A_bsr = trnsparse.BSRMatrix.from_dense(A_dense, block_size=128)
99
+
100
+ lam, v, iters = trnsparse.power_iteration_bsr(A_bsr, max_iter=500, tol=1e-10)
101
+ assert abs(lam - 10.0) < 1e-3, f"expected ~10.0, got {lam}"
102
+ # Eigenvector aligns with e_{n-1} (corresponding to max diag entry).
103
+ assert abs(abs(v[-1].item()) - 1.0) < 1e-3
104
+
105
+ def test_dominant_eigenvalue_vs_torch(self):
106
+ """Match torch.linalg.eigvalsh's largest eigenvalue within 1e-3."""
107
+ torch.manual_seed(4)
108
+ n = 128
109
+ A_dense, A_bsr = _random_spd_bsr(n, block_size=128, seed=4)
110
+
111
+ lam_ours, _, _ = trnsparse.power_iteration_bsr(A_bsr, max_iter=2000, tol=1e-10)
112
+ lam_ref = torch.linalg.eigvalsh(A_dense).max().item()
113
+
114
+ # Power iteration on a random SPD matrix has a spectral gap;
115
+ # 1e-3 relative tolerance is generous.
116
+ assert abs(lam_ours - lam_ref) / lam_ref < 1e-3, f"ours={lam_ours}, ref={lam_ref}"
117
+
118
+
119
+ class TestBsrDiagonal:
120
+ def test_diagonal_of_identity(self):
121
+ I_bsr = trnsparse.BSRMatrix.from_dense(torch.eye(128), block_size=128)
122
+ d = trnsparse.bsr_diagonal(I_bsr)
123
+ torch.testing.assert_close(d, torch.ones(128))
124
+
125
+ def test_diagonal_of_off_diagonal_blocks_only(self):
126
+ """A matrix whose only stored blocks are off-diagonal returns
127
+ zeros on the diagonal.
128
+ """
129
+ torch.manual_seed(5)
130
+ A_dense = torch.zeros(256, 256)
131
+ A_dense[:128, 128:] = torch.randn(128, 128)
132
+ A_dense[128:, :128] = torch.randn(128, 128)
133
+ A_bsr = trnsparse.BSRMatrix.from_dense(A_dense, block_size=128)
134
+ d = trnsparse.bsr_diagonal(A_bsr)
135
+ torch.testing.assert_close(d, torch.zeros(256))
@@ -5,9 +5,10 @@ CSR/COO formats, SpMV, SpMM, and integral screening for
5
5
  sparse scientific computing. Part of the trnsci scientific computing suite.
6
6
  """
7
7
 
8
- __version__ = "0.3.1"
8
+ __version__ = "0.3.2"
9
9
 
10
10
  from .formats import BSRMatrix, COOMatrix, CSRMatrix, eye_sparse, from_dense, from_scipy
11
+ from .iterative import bsr_diagonal, cg_bsr, jacobi_preconditioner_bsr, power_iteration_bsr
11
12
  from .nki import HAS_NKI, get_backend, set_backend
12
13
  from .ops import (
13
14
  bsr_spmm,
@@ -40,6 +41,10 @@ __all__ = [
40
41
  "screen_quartets",
41
42
  "density_screen",
42
43
  "sparsity_stats",
44
+ "cg_bsr",
45
+ "power_iteration_bsr",
46
+ "jacobi_preconditioner_bsr",
47
+ "bsr_diagonal",
43
48
  "HAS_NKI",
44
49
  "set_backend",
45
50
  "get_backend",
@@ -0,0 +1,190 @@
1
+ """Iterative linear solvers for BSR sparse matrices.
2
+
3
+ v0.3.2 — Phase 1 plumbing (#22). Python-level CG and power iteration
4
+ on top of `bsr_spmm` as the matvec. A gets reloaded from HBM on every
5
+ iteration; v0.4.0 (follow-up) delivers the SBUF-resident fused NKI
6
+ kernel that keeps A on-chip across iterations — the architectural win.
7
+
8
+ Algorithm body for CG mirrors `trnsolver.iterative.cg` at commit-time,
9
+ kept local to avoid a trnsolver runtime dep.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Callable
15
+
16
+ import torch
17
+
18
+ from .formats import BSRMatrix
19
+ from .ops import bsr_spmm
20
+
21
+
22
+ def bsr_diagonal(A: BSRMatrix) -> torch.Tensor:
23
+ """Extract the main diagonal of a BSR matrix as a dense vector.
24
+
25
+ The diagonal lives inside the diagonal blocks (block row i at block
26
+ column i). Blocks that aren't diagonal contribute nothing. Returns
27
+ a zero vector if no diagonal block is stored (a fully zero-diagonal
28
+ matrix would be degenerate as a preconditioner anyway).
29
+ """
30
+ m, _ = A.shape
31
+ b = A.block_size
32
+ M_tiles = (m + b - 1) // b
33
+ out = torch.zeros(M_tiles * b, dtype=A.dtype)
34
+ for i in range(M_tiles):
35
+ start = A.block_row_ptrs[i].item()
36
+ end = A.block_row_ptrs[i + 1].item()
37
+ for k in range(start, end):
38
+ if A.block_col_indices[k].item() == i:
39
+ out[i * b : (i + 1) * b] = torch.diagonal(A.blocks[k])
40
+ break
41
+ return out[:m]
42
+
43
+
44
+ def _bsr_matvec(A: BSRMatrix) -> Callable[[torch.Tensor], torch.Tensor]:
45
+ """Wrap `bsr_spmm` so CG / power iteration can pass it a vector."""
46
+
47
+ def matvec(x: torch.Tensor) -> torch.Tensor:
48
+ return bsr_spmm(A, x.unsqueeze(1)).squeeze(1)
49
+
50
+ return matvec
51
+
52
+
53
+ def cg_bsr(
54
+ A: BSRMatrix,
55
+ b: torch.Tensor,
56
+ x0: torch.Tensor | None = None,
57
+ tol: float = 1e-6,
58
+ max_iter: int = 1000,
59
+ M: Callable[[torch.Tensor], torch.Tensor] | torch.Tensor | None = None,
60
+ ) -> tuple[torch.Tensor, int, float]:
61
+ """Conjugate Gradient solver for `A @ x = b` with A SPD and BSR-stored.
62
+
63
+ Thin specialization of the standard CG loop: the matvec goes through
64
+ `bsr_spmm`, so on the NKI backend each iteration dispatches one
65
+ kernel call. That amortizes poorly across many iterations — the
66
+ v0.4.0 fused kernel fuses the loop and keeps A SBUF-resident.
67
+
68
+ Args:
69
+ A: SPD BSR matrix (N, N).
70
+ b: Right-hand side, shape (N,).
71
+ x0: Initial guess (default: zeros).
72
+ tol: Relative-residual tolerance on `||r|| / ||b||`.
73
+ max_iter: Iteration cap.
74
+ M: Preconditioner — callable `M(r) → M^{-1} r` or a dense
75
+ inverse tensor. Build a Jacobi preconditioner from
76
+ `bsr_diagonal(A)` for the common diagonally-dominant case.
77
+
78
+ Returns:
79
+ `(x, iters, rel_residual)` — solution, iteration count, final
80
+ relative residual.
81
+ """
82
+ n = b.shape[0]
83
+ matvec = _bsr_matvec(A)
84
+
85
+ if M is None:
86
+ precond: Callable[[torch.Tensor], torch.Tensor] | None = None
87
+ elif callable(M):
88
+ precond = M
89
+ else:
90
+ M_tensor = M
91
+
92
+ def precond(r: torch.Tensor) -> torch.Tensor:
93
+ return torch.mv(M_tensor, r)
94
+
95
+ x = x0.clone() if x0 is not None else torch.zeros(n, dtype=b.dtype, device=b.device)
96
+ r = b - matvec(x)
97
+ b_norm = torch.linalg.norm(b).item()
98
+ if b_norm < 1e-15:
99
+ return x, 0, 0.0
100
+
101
+ z = precond(r) if precond is not None else r.clone()
102
+ p = z.clone()
103
+ rz = torch.dot(r, z).item()
104
+
105
+ for k in range(max_iter):
106
+ Ap = matvec(p)
107
+ pAp = torch.dot(p, Ap).item()
108
+ if abs(pAp) < 1e-30:
109
+ break
110
+
111
+ alpha = rz / pAp
112
+ x = x + alpha * p
113
+ r = r - alpha * Ap
114
+
115
+ r_norm = torch.linalg.norm(r).item()
116
+ rel = r_norm / b_norm
117
+ if rel < tol:
118
+ return x, k + 1, rel
119
+
120
+ z = precond(r) if precond is not None else r
121
+ rz_new = torch.dot(r, z).item()
122
+ beta = rz_new / rz
123
+ p = z + beta * p
124
+ rz = rz_new
125
+
126
+ return x, max_iter, r_norm / b_norm
127
+
128
+
129
+ def jacobi_preconditioner_bsr(
130
+ A: BSRMatrix,
131
+ ) -> Callable[[torch.Tensor], torch.Tensor]:
132
+ """Build a Jacobi (diagonal) preconditioner for a BSR matrix.
133
+
134
+ Returns a callable `M(r) = r / diag(A)`. Cheap + effective when A
135
+ is diagonally dominant.
136
+ """
137
+ d = bsr_diagonal(A)
138
+ if torch.any(d.abs() < 1e-15):
139
+ raise ValueError("Jacobi preconditioner: diagonal has near-zero entries")
140
+ inv_d = 1.0 / d
141
+
142
+ def precond(r: torch.Tensor) -> torch.Tensor:
143
+ return r * inv_d
144
+
145
+ return precond
146
+
147
+
148
+ def power_iteration_bsr(
149
+ A: BSRMatrix,
150
+ v0: torch.Tensor | None = None,
151
+ max_iter: int = 100,
152
+ tol: float = 1e-9,
153
+ ) -> tuple[float, torch.Tensor, int]:
154
+ """Find the dominant eigenpair (λ, v) of a BSR matrix via power iteration.
155
+
156
+ Converges to the eigenvector with largest `|λ|`. Tolerance is on
157
+ the Rayleigh-quotient change between iterations.
158
+
159
+ Args:
160
+ A: Square BSR matrix (N, N). Doesn't need to be SPD — power
161
+ iteration works for any matrix with a unique dominant
162
+ eigenvalue, but convergence slows as the spectral gap
163
+ shrinks.
164
+ v0: Initial vector (default: random unit vector).
165
+ max_iter: Iteration cap.
166
+ tol: Convergence on `|λ_k - λ_{k-1}|`.
167
+
168
+ Returns:
169
+ `(eigenvalue, eigenvector, iters)`.
170
+ """
171
+ n = A.shape[0]
172
+ matvec = _bsr_matvec(A)
173
+
174
+ v = v0.clone() if v0 is not None else torch.randn(n, dtype=A.dtype)
175
+ v = v / torch.linalg.norm(v)
176
+
177
+ lam_prev = 0.0
178
+ for k in range(max_iter):
179
+ Av = matvec(v)
180
+ lam = torch.dot(v, Av).item()
181
+ norm = torch.linalg.norm(Av).item()
182
+ if norm < 1e-30:
183
+ return 0.0, v, k + 1
184
+ v = Av / norm
185
+
186
+ if abs(lam - lam_prev) < tol:
187
+ return lam, v, k + 1
188
+ lam_prev = lam
189
+
190
+ return lam, v, max_iter
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trnsparse
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Sparse matrix operations for AWS Trainium via NKI
5
5
  Author-email: Scott Friedman <scttfrdmn@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -12,6 +12,7 @@ pyproject.toml
12
12
  .github/workflows/notify-umbrella.yml
13
13
  .github/workflows/publish.yml
14
14
  benchmarks/bench_bsr_spmm.py
15
+ benchmarks/bench_iterative.py
15
16
  benchmarks/bench_screening.py
16
17
  benchmarks/bench_spmm.py
17
18
  benchmarks/bench_spmv.py
@@ -22,6 +23,7 @@ docs/aws_setup.md
22
23
  docs/benchmarks.md
23
24
  docs/index.md
24
25
  docs/installation.md
26
+ docs/iterative_solvers.md
25
27
  docs/migration_scipy.md
26
28
  docs/quickstart.md
27
29
  examples/sparse_fock.py
@@ -35,6 +37,7 @@ scripts/run_simulator_tests.sh
35
37
  tests/conftest.py
36
38
  tests/test_bsr.py
37
39
  tests/test_formats.py
40
+ tests/test_iterative.py
38
41
  tests/test_nki_bsr.py
39
42
  tests/test_nki_sim.py
40
43
  tests/test_nki_spmm.py
@@ -42,6 +45,7 @@ tests/test_ops.py
42
45
  tests/test_screening.py
43
46
  trnsparse/__init__.py
44
47
  trnsparse/formats.py
48
+ trnsparse/iterative.py
45
49
  trnsparse/ops.py
46
50
  trnsparse/screening.py
47
51
  trnsparse.egg-info/PKG-INFO
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes