trnsparse 0.7.0__tar.gz → 0.9.0__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 (77) hide show
  1. {trnsparse-0.7.0 → trnsparse-0.9.0}/CHANGELOG.md +121 -0
  2. {trnsparse-0.7.0 → trnsparse-0.9.0}/CLAUDE.md +4 -2
  3. {trnsparse-0.7.0 → trnsparse-0.9.0}/PKG-INFO +31 -1
  4. {trnsparse-0.7.0 → trnsparse-0.9.0}/README.md +26 -0
  5. trnsparse-0.9.0/benchmarks/bench_native_trn2.py +444 -0
  6. {trnsparse-0.7.0 → trnsparse-0.9.0}/docs/api.md +19 -0
  7. {trnsparse-0.7.0 → trnsparse-0.9.0}/docs/architecture.md +32 -0
  8. trnsparse-0.9.0/docs/benchmarks.md +294 -0
  9. trnsparse-0.9.0/docs/installation.md +116 -0
  10. {trnsparse-0.7.0 → trnsparse-0.9.0}/docs/migration_scipy.md +4 -1
  11. {trnsparse-0.7.0 → trnsparse-0.9.0}/pyproject.toml +13 -1
  12. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/conftest.py +8 -0
  13. trnsparse-0.9.0/tests/test_core_split.py +53 -0
  14. trnsparse-0.9.0/tests/test_native_dispatch.py +1011 -0
  15. trnsparse-0.9.0/tests/test_nki_compile.py +301 -0
  16. trnsparse-0.9.0/tests/test_nki_native.py +340 -0
  17. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse/__init__.py +1 -1
  18. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse/formats.py +35 -0
  19. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse/iterative.py +19 -7
  20. trnsparse-0.9.0/trnsparse/nki/_native.py +332 -0
  21. trnsparse-0.9.0/trnsparse/nki/_native_attn.py +129 -0
  22. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse/nki/dispatch.py +526 -42
  23. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse/nki/kernels.py +387 -23
  24. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse/ops.py +104 -16
  25. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse.egg-info/PKG-INFO +31 -1
  26. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse.egg-info/SOURCES.txt +6 -0
  27. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse.egg-info/requires.txt +5 -0
  28. trnsparse-0.7.0/docs/benchmarks.md +0 -137
  29. trnsparse-0.7.0/docs/installation.md +0 -21
  30. trnsparse-0.7.0/tests/test_nki_compile.py +0 -143
  31. {trnsparse-0.7.0 → trnsparse-0.9.0}/.github/workflows/ci.yml +0 -0
  32. {trnsparse-0.7.0 → trnsparse-0.9.0}/.github/workflows/notify-umbrella.yml +0 -0
  33. {trnsparse-0.7.0 → trnsparse-0.9.0}/.github/workflows/publish.yml +0 -0
  34. {trnsparse-0.7.0 → trnsparse-0.9.0}/.gitignore +0 -0
  35. {trnsparse-0.7.0 → trnsparse-0.9.0}/.pre-commit-config.yaml +0 -0
  36. {trnsparse-0.7.0 → trnsparse-0.9.0}/CODE_OF_CONDUCT.md +0 -0
  37. {trnsparse-0.7.0 → trnsparse-0.9.0}/CONTRIBUTING.md +0 -0
  38. {trnsparse-0.7.0 → trnsparse-0.9.0}/LICENSE +0 -0
  39. {trnsparse-0.7.0 → trnsparse-0.9.0}/benchmarks/bench_bsr_spmm.py +0 -0
  40. {trnsparse-0.7.0 → trnsparse-0.9.0}/benchmarks/bench_iterative.py +0 -0
  41. {trnsparse-0.7.0 → trnsparse-0.9.0}/benchmarks/bench_screening.py +0 -0
  42. {trnsparse-0.7.0 → trnsparse-0.9.0}/benchmarks/bench_spmm.py +0 -0
  43. {trnsparse-0.7.0 → trnsparse-0.9.0}/benchmarks/bench_spmv.py +0 -0
  44. {trnsparse-0.7.0 → trnsparse-0.9.0}/benchmarks/conftest.py +0 -0
  45. {trnsparse-0.7.0 → trnsparse-0.9.0}/docs/aws_setup.md +0 -0
  46. {trnsparse-0.7.0 → trnsparse-0.9.0}/docs/index.md +0 -0
  47. {trnsparse-0.7.0 → trnsparse-0.9.0}/docs/iterative_solvers.md +0 -0
  48. {trnsparse-0.7.0 → trnsparse-0.9.0}/docs/quickstart.md +0 -0
  49. {trnsparse-0.7.0 → trnsparse-0.9.0}/docs/sparse_attention.md +0 -0
  50. {trnsparse-0.7.0 → trnsparse-0.9.0}/examples/block_sparse_attention.py +0 -0
  51. {trnsparse-0.7.0 → trnsparse-0.9.0}/examples/sparse_fock.py +0 -0
  52. {trnsparse-0.7.0 → trnsparse-0.9.0}/infra/terraform/.terraform.lock.hcl +0 -0
  53. {trnsparse-0.7.0 → trnsparse-0.9.0}/infra/terraform/README.md +0 -0
  54. {trnsparse-0.7.0 → trnsparse-0.9.0}/infra/terraform/main.tf +0 -0
  55. {trnsparse-0.7.0 → trnsparse-0.9.0}/mkdocs.yml +0 -0
  56. {trnsparse-0.7.0 → trnsparse-0.9.0}/scripts/bench_to_md.py +0 -0
  57. {trnsparse-0.7.0 → trnsparse-0.9.0}/scripts/run_benchmarks.sh +0 -0
  58. {trnsparse-0.7.0 → trnsparse-0.9.0}/scripts/run_neuron_tests.sh +0 -0
  59. {trnsparse-0.7.0 → trnsparse-0.9.0}/scripts/run_simulator_tests.sh +0 -0
  60. {trnsparse-0.7.0 → trnsparse-0.9.0}/setup.cfg +0 -0
  61. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_attention.py +0 -0
  62. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_attn_gather.py +0 -0
  63. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_bsr.py +0 -0
  64. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_examples.py +0 -0
  65. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_formats.py +0 -0
  66. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_iterative.py +0 -0
  67. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_nki_attn.py +0 -0
  68. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_nki_bsr.py +0 -0
  69. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_nki_screened_spmm.py +0 -0
  70. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_nki_sim.py +0 -0
  71. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_nki_spmm.py +0 -0
  72. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_ops.py +0 -0
  73. {trnsparse-0.7.0 → trnsparse-0.9.0}/tests/test_screening.py +0 -0
  74. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse/nki/__init__.py +0 -0
  75. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse/screening.py +0 -0
  76. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse.egg-info/dependency_links.txt +0 -0
  77. {trnsparse-0.7.0 → trnsparse-0.9.0}/trnsparse.egg-info/top_level.txt +0 -0
@@ -7,12 +7,133 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] — 2026-10-30
11
+
12
+ Validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0, native TorchNeuron; LNC=1 and LNC=2) — see trnsci/trnsci#35.
13
+
14
+ Headline performance: static BSR SpMM and block-sparse attention split across both LNC=2 cores, with bf16 operands on fp32 PSUM; the fp32 BSR crossover against dense aten at n=8192, width 512 rises from ~27% to ~49% block density, and at seq 8192 attention forward plus backward beats unmasked dense SDPA for all three masks (#36).
15
+
16
+ ### Changed
17
+
18
+ #### Native path: static kernels on both LNC=2 cores, bf16 operands (#30)
19
+
20
+ - `bsr_spmm_static`, `attn_fwd_static` and `attn_bwd_static` split their
21
+ output tiles between the two physical cores of an LNC=2 logical core
22
+ (`program_id`), balanced by stored blocks; each output tile has one
23
+ writer. `TRNSPARSE_CORE_SPLIT=0` turns the split off.
24
+ - bf16 inputs (all operands bf16) run bf16 matmuls with fp32 PSUM
25
+ accumulation; scores and softmax statistics stay fp32, and the attention
26
+ statistics returned by `trnsparse::bsr_attn` are fp32 for bf16 / fp16 Q.
27
+ Mixed fp32/bf16 operands run in fp32.
28
+ - `benchmarks/bench_native_trn2.py`: `--dtype bf16` (matched-precision
29
+ dense aten baselines) and `--fast` (device columns only).
30
+
31
+ ## [0.8.0] — 2026-10-30
32
+
33
+ Native TorchNeuron dispatch (Trn2/Trn3) alongside the PyTorch/XLA path (Trn1, maintenance); validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0) — see trnsci/trnsci#35.
34
+
35
+ Headline performance: static-structure BSR SpMM, 6.8× faster than dense aten `matmul` at 1% block density, n=8192 (#34).
36
+
37
+ ### Changed
38
+
39
+ #### Native path: static-structure kernels (#30)
40
+
41
+ On TorchNeuron the sparse structure is host-resident, so the native path now
42
+ hands it to the kernels as compile-time constants (`int[]` op arguments,
43
+ tuples inside the kernel) instead of padded, gathered device tensors.
44
+ Measured on Trn2 (`benchmarks/bench_native_trn2.py`; tables on #30):
45
+
46
+ - `bsr_spmm` → `trnsparse::bsr_spmm_static`: walks exactly the stored
47
+ blocks and addresses B by block column; no `(M_tiles, K_max)` padding and
48
+ no device-side gather. 8192×8192 @ 8192×512: 3.1–12.5 ms → 0.7–3.9 ms;
49
+ beats dense aten `matmul` on the device up to ~13% (width 128) to ~34%
50
+ (width 512) block density at n = 8192.
51
+ - Block-sparse attention forward → `trnsparse::attn_fwd_static`: one launch,
52
+ two passes with the softmax statistics on chip; no K/V gather, no host
53
+ reduction, no per-block score round trip through HBM. Backward →
54
+ `trnsparse::attn_bwd_static`: one launch for dQ (row-first) and dK/dV
55
+ (column-first). seq 8192 global mask, fwd+bwd: 127–195 ms → ~4 ms.
56
+ - Screened SpMM masks each tile in the transposed frame, so the masked tile
57
+ is directly the stationary operand (no HBM round trip per tile).
58
+ - `spmm` with host CSR scatters the nnz triplets on the device when that is
59
+ cheaper than copying a host-built dense matrix.
60
+
61
+ One compile per distinct structure (seconds; cached in-process by the
62
+ structure). `TRNSPARSE_STATIC_STRUCTURE=0` restores the gather path;
63
+ structures above `TRNSPARSE_STATIC_MAX_UNROLL` (default 16384 unrolled
64
+ block matmuls) use it automatically.
65
+
66
+ ### Added
67
+
68
+ - `benchmarks/bench_native_trn2.py`: warm-latency benchmark for the native
69
+ path against dense aten on the device, CPU, and `torch.sparse` on CPU.
70
+
71
+ #### Native PyTorch dispatch on TorchNeuron (#29, #30)
72
+
73
+ On TorchNeuron (Neuron SDK 2.33+, `torch-neuronx` 2.14, PyTorch 2.14,
74
+ Trn2/Trn3), every NKI kernel is now registered as a PyTorch custom op with
75
+ `torch_neuronx.nki_op` and dispatched on `device="neuron"`, with no
76
+ `torch_xla` import. Ops: `trnsparse::spmm_dense`, `bsr_spmm`,
77
+ `screened_spmm`, `attn_stats`, `attn_out`, `attn_bwd_dq`, `attn_bwd_dkdv`,
78
+ plus the composite `trnsparse::bsr_attn` / `bsr_attn_bwd`. Kernel bodies
79
+ are unchanged.
80
+
81
+ - Autograd on the native path uses `register_autograd` on the ops instead
82
+ of `torch.autograd.Function`: `spmm_dense`, `bsr_spmm` and
83
+ `screened_spmm` carry their analytic adjoints, and `bsr_attn` carries the
84
+ NKI dQ / dK-dV backward (via `bsr_attn_bwd`). Gradients reach
85
+ `CSRMatrix.values`, `BSRMatrix.blocks` and the dense operands through the
86
+ host-side densify / pad / gather. `_SpMMFunction`, `_BSRSpMMFunction`,
87
+ `_ScreenedSpMMFunction` and `_AttnTiledFunction` remain for the XLA path,
88
+ the simulator and the PyTorch backend. The native BSR backward has no
89
+ per-block Python loop.
90
+ - Placement (#30): the `neuron` device has no sparse CSR tensors
91
+ (aws-neuron-sdk#1442), so sparse structure stays on the host and only
92
+ dense data moves. `spmm`, `spmv`, `spmv_symmetric`, `bsr_spmm`,
93
+ `screened_spmm`, `block_sparse_attention_tiled`, `cg_bsr` and
94
+ `jacobi_preconditioner_bsr` accept host CSR/BSR structure with dense
95
+ operands (and BSR `blocks`) on `neuron`, and return results on the dense
96
+ operand's device. A host CSR against a device operand is densified on the
97
+ host without a torch sparse tensor (`formats._csr_to_dense_host`, static
98
+ shapes), so `spmm` with host-CSR structure traces under
99
+ `torch.compile(..., fullgraph=True)`. Index tensors found on a device are
100
+ read through host copies. Documented in `docs/installation.md` and the
101
+ format and op docstrings.
102
+ - The dispatch path is detected on the first kernel launch: native when
103
+ `torch_neuronx` exports `nki_op` and a `neuron` device is available, XLA
104
+ when `torch_xla` is installed, otherwise none. The XLA path (Trn1) is
105
+ unchanged, as are the PyTorch fallback and the `TRNSPARSE_USE_SIMULATOR`
106
+ path. All kernel launches share one helper (`dispatch._launch`, or its
107
+ halves `_to_device` / `_run` where several attention kernels share
108
+ operands).
109
+ - Custom ops are registered lazily (`trnsparse/nki/_native.py` and
110
+ `_native_attn.py` are imported only when the native path is selected).
111
+ - `TRNSCI_NKI_DISPATCH=auto|native|xla|cpu` forces the dispatch path (a
112
+ suite-wide variable). Forcing an unavailable path raises instead of
113
+ falling back. `TRNSCI_XLA_DEPRECATION=1` makes the XLA path emit a
114
+ one-time `DeprecationWarning` (off by default).
115
+ - New `[torchneuron]` extra (`torch>=2.14`, `torch-neuronx>=2.14`,
116
+ `nki>=0.6`); `[neuron]` stays the PyTorch/XLA lineage.
117
+ - Tests: `conftest.py` sets `NKI_ENABLE_TRACE_CACHE=0` by default.
118
+ `tests/test_native_dispatch.py` (CPU) covers path selection, launch
119
+ routing, lazy registration, op schemas and fake shapes, the registered
120
+ backward formulas, the native wrappers end to end on CPU with real custom
121
+ ops, and a CPU `fullgraph=True` trace of host-CSR `spmm`.
122
+ `tests/test_nki_native.py` (`@pytest.mark.neuron`, native only) covers
123
+ host-structure / device-operand calls, gradients, and
124
+ `torch.compile(backend="neuron", fullgraph=True)` of `spmm` with
125
+ host-CSR structure. Not yet run on hardware.
126
+
10
127
  ## [0.7.0] — 2026-10-28
11
128
 
12
129
  Validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0, native TorchNeuron device) — see trnsci/trnsci#35.
13
130
 
14
131
  ### Fixed
15
132
 
133
+ - PyTorch reference attention (`_block_sparse_attn_pytorch` and the
134
+ recompute branch of `_block_sparse_attn_backward`) allocates on Q's
135
+ device instead of the default device.
136
+
16
137
  - **NKI 0.7.0 / neuronx-cc 2.28 kernel compatibility (#29).** Kernels that
17
138
  passed `nki.simulate` failed to compile on Trn2 (Neuron SDK 2.33):
18
139
  - `_attn_out_kernel`, `_attn_bwd_dq_kernel`, `_attn_bwd_dkdv_kernel`:
@@ -25,8 +25,10 @@ trnsparse/
25
25
  │ ├── iterative.py # cg_bsr, power_iteration_bsr, jacobi_preconditioner_bsr
26
26
  │ └── nki/
27
27
  │ ├── __init__.py
28
- │ ├── kernels.py # _bsr_spmm_kernel, _screened_spmm_kernel
29
- │ └── dispatch.py # torch.autograd.Function wrappers + NKI/PyTorch routing
28
+ │ ├── kernels.py # _bsr_spmm_kernel, _screened_spmm_kernel, attention kernels
29
+ │ ├── dispatch.py # dispatch-path selection, _launch, autograd.Function wrappers (XLA/CPU)
30
+ │ ├── _native.py # TorchNeuron nki_op custom ops + register_autograd, loaded lazily
31
+ │ └── _native_attn.py # trnsparse::bsr_attn / bsr_attn_bwd composite ops (native path)
30
32
  ├── tests/
31
33
  ├── examples/
32
34
  │ ├── sparse_fock.py # Schwarz-screened Fock build (3 paths + trnblas)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trnsparse
3
- Version: 0.7.0
3
+ Version: 0.9.0
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
@@ -22,6 +22,10 @@ Provides-Extra: neuron
22
22
  Requires-Dist: nki>=0.3.0; extra == "neuron"
23
23
  Requires-Dist: neuronxcc>=2.24; extra == "neuron"
24
24
  Requires-Dist: torch-neuronx>=2.9; extra == "neuron"
25
+ Provides-Extra: torchneuron
26
+ Requires-Dist: torch>=2.14; extra == "torchneuron"
27
+ Requires-Dist: torch-neuronx>=2.14; extra == "torchneuron"
28
+ Requires-Dist: nki>=0.6; extra == "torchneuron"
25
29
  Provides-Extra: dev
26
30
  Requires-Dist: pytest>=7.0; extra == "dev"
27
31
  Requires-Dist: pytest-benchmark>=4.0; extra == "dev"
@@ -65,8 +69,34 @@ Suite-wide tracker: [trnsci/trnsci#1](https://github.com/trnsci/trnsci/issues/1)
65
69
 
66
70
  ```bash
67
71
  pip install trnsparse
72
+
73
+ # Trn2/Trn3 with TorchNeuron (native PyTorch, Neuron SDK 2.33+)
74
+ pip install "trnsparse[torchneuron]" --extra-index-url https://pip.repos.neuron.amazonaws.com
75
+
76
+ # Trn1 / PyTorch-XLA lineage
77
+ pip install "trnsparse[neuron]" --extra-index-url https://pip.repos.neuron.amazonaws.com
68
78
  ```
69
79
 
80
+ Kernels dispatch on whichever Neuron PyTorch lineage is installed:
81
+
82
+ - **TorchNeuron (native)** — Trn2/Trn3, Neuron SDK 2.33+, Python ≥ 3.12,
83
+ `torch` 2.14, `torch-neuronx` 2.14.*. Each kernel is a PyTorch custom op
84
+ (`torch.ops.trnsparse.*`) on the `neuron` device, with its backward
85
+ attached via `register_autograd`; no `torch_xla`.
86
+ - **PyTorch/XLA** — Trn1 (and any host on the `torch-neuronx` 2.9 lineage).
87
+ Kernels dispatch through `torch_xla`, unchanged.
88
+
89
+ **Placement on the native path:** the `neuron` device has no sparse CSR
90
+ tensors, so keep sparse *structure* (CSR `row_ptrs` / `col_indices`, BSR
91
+ `block_row_ptrs` / `block_col_indices`) on the host and put dense operands
92
+ (and BSR `blocks`) on `device="neuron"`. Results come back on the dense
93
+ operand's device. See [installation](docs/installation.md#placement-sparse-structure-on-the-host).
94
+
95
+ Set `TRNSCI_NKI_DISPATCH=native|xla|cpu` (shared by all trnsci libraries)
96
+ to force a path. With an editable install (`pip install -e`), set
97
+ `NKI_ENABLE_TRACE_CACHE=0`: the NKI kernel cache only versions installed
98
+ wheels and can serve stale kernels.
99
+
70
100
  ## Usage
71
101
 
72
102
  ```python
@@ -34,8 +34,34 @@ Suite-wide tracker: [trnsci/trnsci#1](https://github.com/trnsci/trnsci/issues/1)
34
34
 
35
35
  ```bash
36
36
  pip install trnsparse
37
+
38
+ # Trn2/Trn3 with TorchNeuron (native PyTorch, Neuron SDK 2.33+)
39
+ pip install "trnsparse[torchneuron]" --extra-index-url https://pip.repos.neuron.amazonaws.com
40
+
41
+ # Trn1 / PyTorch-XLA lineage
42
+ pip install "trnsparse[neuron]" --extra-index-url https://pip.repos.neuron.amazonaws.com
37
43
  ```
38
44
 
45
+ Kernels dispatch on whichever Neuron PyTorch lineage is installed:
46
+
47
+ - **TorchNeuron (native)** — Trn2/Trn3, Neuron SDK 2.33+, Python ≥ 3.12,
48
+ `torch` 2.14, `torch-neuronx` 2.14.*. Each kernel is a PyTorch custom op
49
+ (`torch.ops.trnsparse.*`) on the `neuron` device, with its backward
50
+ attached via `register_autograd`; no `torch_xla`.
51
+ - **PyTorch/XLA** — Trn1 (and any host on the `torch-neuronx` 2.9 lineage).
52
+ Kernels dispatch through `torch_xla`, unchanged.
53
+
54
+ **Placement on the native path:** the `neuron` device has no sparse CSR
55
+ tensors, so keep sparse *structure* (CSR `row_ptrs` / `col_indices`, BSR
56
+ `block_row_ptrs` / `block_col_indices`) on the host and put dense operands
57
+ (and BSR `blocks`) on `device="neuron"`. Results come back on the dense
58
+ operand's device. See [installation](docs/installation.md#placement-sparse-structure-on-the-host).
59
+
60
+ Set `TRNSCI_NKI_DISPATCH=native|xla|cpu` (shared by all trnsci libraries)
61
+ to force a path. With an editable install (`pip install -e`), set
62
+ `NKI_ENABLE_TRACE_CACHE=0`: the NKI kernel cache only versions installed
63
+ wheels and can serve stale kernels.
64
+
39
65
  ## Usage
40
66
 
41
67
  ```python
@@ -0,0 +1,444 @@
1
+ """Warm-latency benchmark for the native TorchNeuron path (Trn2/Trn3).
2
+
3
+ Not a pytest-benchmark file: a standalone script, one section per process,
4
+ so each run stays short and a compile failure does not take the rest down.
5
+ Every row is printed as one JSON line (and appended to ``--out``).
6
+
7
+ NKI_ENABLE_TRACE_CACHE=0 python benchmarks/bench_native_trn2.py --section bsr --size 4096
8
+
9
+ Sections:
10
+ bsr BSR (128x128) SpMM vs dense aten matmul on the device, CPU dense,
11
+ and torch.sparse BSR/CSR on CPU. Block densities x RHS widths.
12
+ csr CSR SpMM (host structure, densify, NKI dense kernel) at element
13
+ densities, vs the same baselines.
14
+ screen Screened SpMM vs dense masked matmul.
15
+ attn Block-sparse attention (local / dilated / global masks), forward
16
+ and forward+backward, vs dense SDPA on the device and on CPU.
17
+
18
+ Options for the bsr and attn sections:
19
+ --dtype bf16 run the operands in bf16 (fp32 PSUM inside the kernels)
20
+ and the dense aten baselines in bf16 too, so each side has
21
+ matched precision. Errors are against fp64 on the
22
+ bf16-rounded inputs.
23
+ --fast only the device columns (trnsparse e2e, static kernel,
24
+ dense aten); skips the gather path and the CPU baselines.
25
+ TRNSPARSE_CORE_SPLIT=0 in the environment keeps each static kernel on
26
+ one physical core's partition (the whole kernel on both cores, as
27
+ before the split), for A/B runs.
28
+
29
+ Timing: each callable is run ``--warmup`` times (the first call compiles),
30
+ then ``--reps`` times with ``torch.neuron.synchronize()`` after every call;
31
+ the median is reported in milliseconds. Structure stays on the host, dense
32
+ data (blocks, operands) lives on the device, as `docs/installation.md`
33
+ recommends for the native path.
34
+ """
35
+
36
+ # ruff: noqa: B023 (every closure is benchmarked before its loop variables change)
37
+ from __future__ import annotations
38
+
39
+ import argparse
40
+ import json
41
+ import math
42
+ import statistics
43
+ import sys
44
+ import time
45
+ from pathlib import Path
46
+
47
+ import torch
48
+
49
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
50
+
51
+ import trnsparse # noqa: E402
52
+ from trnsparse.nki import dispatch as _d # noqa: E402
53
+
54
+ DEV = "neuron"
55
+ P = 128
56
+
57
+
58
+ def _sync():
59
+ if hasattr(torch, "neuron"):
60
+ torch.neuron.synchronize()
61
+
62
+
63
+ def bench(fn, warmup=2, reps=10):
64
+ for _ in range(warmup):
65
+ fn()
66
+ _sync()
67
+ ts = []
68
+ for _ in range(reps):
69
+ t0 = time.perf_counter()
70
+ fn()
71
+ _sync()
72
+ ts.append((time.perf_counter() - t0) * 1e3)
73
+ return statistics.median(ts)
74
+
75
+
76
+ def safe_bench(fn, **kw):
77
+ try:
78
+ return bench(fn, **kw)
79
+ except Exception as e: # report, don't abort the section
80
+ print(f"# bench failed: {type(e).__name__}: {str(e)[:200]}", file=sys.stderr)
81
+ return None
82
+
83
+
84
+ def rel_err(x, ref):
85
+ x = x.detach().cpu().double()
86
+ ref = ref.detach().cpu().double()
87
+ return float((x - ref).norm() / ref.norm().clamp_min(1e-30))
88
+
89
+
90
+ def emit(row, out):
91
+ line = json.dumps(row)
92
+ print(line, flush=True)
93
+ if out:
94
+ with open(out, "a") as f:
95
+ f.write(line + "\n")
96
+
97
+
98
+ # ── BSR ──────────────────────────────────────────────────────────────────────
99
+
100
+
101
+ def random_bsr(n, density, seed=0):
102
+ g = torch.Generator().manual_seed(seed)
103
+ nb = n // P
104
+ total = nb * nb
105
+ k = max(1, round(density * total))
106
+ flat = torch.randperm(total, generator=g)[:k].sort().values
107
+ rows, cols = flat // nb, flat % nb
108
+ row_ptrs = torch.zeros(nb + 1, dtype=torch.long)
109
+ row_ptrs[1:] = torch.bincount(rows, minlength=nb).cumsum(0)
110
+ blocks = torch.randn(k, P, P, generator=g) / math.sqrt(P)
111
+ return trnsparse.BSRMatrix(
112
+ blocks=blocks,
113
+ block_col_indices=cols.to(torch.long),
114
+ block_row_ptrs=row_ptrs,
115
+ shape=(n, n),
116
+ block_size=P,
117
+ )
118
+
119
+
120
+ _DTYPES = {"fp32": torch.float32, "bf16": torch.bfloat16}
121
+
122
+
123
+ def section_bsr(args):
124
+ densities = [float(x) for x in args.densities.split(",")]
125
+ widths = [int(x) for x in args.widths.split(",")]
126
+ n = args.size
127
+ dt = _DTYPES[args.dtype]
128
+ for width in widths:
129
+ g = torch.Generator().manual_seed(1)
130
+ Bc = torch.randn(n, width, generator=g).to(dt).float() # values exact in dt
131
+ Bd = Bc.to(DEV, dt)
132
+ dense_dev_ms = None
133
+ cpu_dense_ms = None
134
+ for dens in densities:
135
+ A = random_bsr(n, dens)
136
+ A.blocks = A.blocks.to(dt).float()
137
+ Ac = A.to_dense()
138
+ Ad_dev = trnsparse.BSRMatrix(
139
+ blocks=A.blocks.to(DEV, dt),
140
+ block_col_indices=A.block_col_indices,
141
+ block_row_ptrs=A.block_row_ptrs,
142
+ shape=A.shape,
143
+ block_size=P,
144
+ )
145
+ counts = (A.block_row_ptrs[1:] - A.block_row_ptrs[:-1]).tolist()
146
+ K_max = max(counts)
147
+ row = {
148
+ "section": "bsr",
149
+ "dtype": args.dtype,
150
+ "core_split": _core_split(),
151
+ "n": n,
152
+ "width": width,
153
+ "block_density": dens,
154
+ "n_blocks": A.n_blocks,
155
+ "K_max": K_max,
156
+ "mean_blocks_per_row": sum(counts) / len(counts),
157
+ }
158
+ ref = Ac.double() @ Bc.double()
159
+ out = trnsparse.bsr_spmm(Ad_dev, Bd)
160
+ row["rel_err"] = rel_err(out, ref)
161
+ row["trnsparse_ms"] = safe_bench(lambda: trnsparse.bsr_spmm(Ad_dev, Bd), reps=args.reps)
162
+ ops = _d._get_native_ops()
163
+ if args.fast:
164
+ rp, cl = _d._bsr_structure_lists(A)
165
+ bl = Ad_dev.blocks
166
+ row["static_kernel_ms"] = safe_bench(
167
+ lambda: ops["bsr_spmm_static"](bl, Bd, rp, cl), reps=args.reps
168
+ )
169
+ Ad = Ac.to(DEV, dt)
170
+ if dense_dev_ms is None:
171
+ dense_dev_ms = safe_bench(lambda: Ad @ Bd, reps=args.reps)
172
+ row["dense_dev_ms"] = dense_dev_ms
173
+ Ad = None
174
+ if row["trnsparse_ms"] and dense_dev_ms:
175
+ row["speedup_vs_dense_dev"] = dense_dev_ms / row["trnsparse_ms"]
176
+ emit(row, args.out)
177
+ continue
178
+
179
+ # Split: device-side prep (gather) vs the kernel launch.
180
+ def prep():
181
+ return _d._bsr_pad_and_gather(_d._BlockView(Ad_dev, Ad_dev.blocks), Bd)
182
+
183
+ bp, bg, *_ = prep()
184
+ # Gather path (main before trnsparse#30 static kernels): device-side
185
+ # pad + gather, then the K_max-padded kernel.
186
+ row["gather_prep_ms"] = safe_bench(prep, reps=args.reps)
187
+ row["gather_kernel_ms"] = safe_bench(lambda: ops["bsr_spmm"](bp, bg), reps=args.reps)
188
+ bp = bg = None # free the gathered tiles
189
+ if "bsr_spmm_static" in ops:
190
+ rp, cl = _d._bsr_structure_lists(A)
191
+ bl = Ad_dev.blocks
192
+ row["static_kernel_ms"] = safe_bench(
193
+ lambda: ops["bsr_spmm_static"](bl, Bd, rp, cl), reps=args.reps
194
+ )
195
+
196
+ Ad = Ac.to(DEV, dt)
197
+ if dense_dev_ms is None:
198
+ dense_dev_ms = safe_bench(lambda: Ad @ Bd, reps=args.reps)
199
+ row["dense_dev_ms"] = dense_dev_ms
200
+ Ad = None
201
+ if cpu_dense_ms is None:
202
+ cpu_dense_ms = safe_bench(lambda: Ac @ Bc, warmup=1, reps=args.cpu_reps)
203
+ row["cpu_dense_ms"] = cpu_dense_ms
204
+ try:
205
+ Absr = Ac.to_sparse_bsr((P, P))
206
+ row["cpu_torch_bsr_ms"] = safe_bench(
207
+ lambda: Absr @ Bc, warmup=1, reps=args.cpu_reps
208
+ )
209
+ except Exception as e:
210
+ row["cpu_torch_bsr_ms"] = f"unsupported: {type(e).__name__}"
211
+ Acsr = Ac.to_sparse_csr()
212
+ row["cpu_torch_csr_ms"] = safe_bench(lambda: Acsr @ Bc, warmup=1, reps=args.cpu_reps)
213
+ # trnsparse on CPU (pytorch backend): the loop-over-blocks fallback.
214
+ prev = trnsparse.get_backend()
215
+ trnsparse.set_backend("pytorch")
216
+ row["cpu_trnsparse_ms"] = safe_bench(
217
+ lambda: trnsparse.bsr_spmm(A, Bc), warmup=1, reps=args.cpu_reps
218
+ )
219
+ trnsparse.set_backend(prev)
220
+ if row["trnsparse_ms"] and row["dense_dev_ms"]:
221
+ row["speedup_vs_dense_dev"] = row["dense_dev_ms"] / row["trnsparse_ms"]
222
+ emit(row, args.out)
223
+
224
+
225
+ # ── CSR ──────────────────────────────────────────────────────────────────────
226
+
227
+
228
+ def section_csr(args):
229
+ densities = [float(x) for x in args.densities.split(",")]
230
+ widths = [int(x) for x in args.widths.split(",")]
231
+ n = args.size
232
+ for width in widths:
233
+ Bc = torch.randn(n, width, generator=torch.Generator().manual_seed(1))
234
+ Bd = Bc.to(DEV)
235
+ for dens in densities:
236
+ g = torch.Generator().manual_seed(0)
237
+ Ac = torch.randn(n, n, generator=g) * (torch.rand(n, n, generator=g) < dens)
238
+ A = trnsparse.from_dense(Ac)
239
+ row = {"section": "csr", "n": n, "width": width, "density": dens, "nnz": A.nnz}
240
+ out = trnsparse.spmm(A, Bd)
241
+ row["rel_err"] = rel_err(out, Ac.double() @ Bc.double())
242
+ row["trnsparse_ms"] = safe_bench(lambda: trnsparse.spmm(A, Bd), reps=args.reps)
243
+ Ad = Ac.to(DEV)
244
+ row["dense_dev_ms"] = safe_bench(lambda: Ad @ Bd, reps=args.reps)
245
+ row["densify_host_h2d_ms"] = safe_bench(
246
+ lambda: trnsparse.formats._csr_to_dense_host(A).to(DEV), reps=args.reps
247
+ )
248
+ if hasattr(_d, "_csr_to_dense_on"):
249
+ row["densify_on_device_ms"] = safe_bench(
250
+ lambda: _d._csr_to_dense_on(A, DEV), reps=args.reps
251
+ )
252
+ row["cpu_dense_ms"] = safe_bench(lambda: Ac @ Bc, warmup=1, reps=args.cpu_reps)
253
+ Acsr = Ac.to_sparse_csr()
254
+ row["cpu_torch_csr_ms"] = safe_bench(lambda: Acsr @ Bc, warmup=1, reps=args.cpu_reps)
255
+ emit(row, args.out)
256
+ Ad = None
257
+
258
+
259
+ # ── Screened SpMM ────────────────────────────────────────────────────────────
260
+
261
+
262
+ def section_screen(args):
263
+ widths = [int(x) for x in args.widths.split(",")]
264
+ n = args.size
265
+ g = torch.Generator().manual_seed(0)
266
+ # Exponentially decaying Schwarz bounds, so a threshold keeps a band.
267
+ pos = torch.arange(n, dtype=torch.float32)
268
+ diag = torch.exp(-pos / (n / 8)) + 1e-12
269
+ Ac = torch.randn(n, n, generator=g)
270
+ for width in widths:
271
+ Bc = torch.randn(n, width, generator=g)
272
+ Bd, Ad, diag_d = Bc.to(DEV), Ac.to(DEV), diag.to(DEV)
273
+ for thr in [float(x) for x in args.thresholds.split(",")]:
274
+ Q = diag.sqrt()
275
+ mask = (Q[:, None] * Q[None, :]) > math.sqrt(thr)
276
+ ref = (Ac.double() * mask) @ Bc.double()
277
+ row = {
278
+ "section": "screen",
279
+ "n": n,
280
+ "width": width,
281
+ "threshold": thr,
282
+ "kept_fraction": float(mask.float().mean()),
283
+ }
284
+ out = trnsparse.screened_spmm(Ad, diag_d, Bd, thr)
285
+ row["rel_err"] = rel_err(out, ref)
286
+ row["trnsparse_ms"] = safe_bench(
287
+ lambda: trnsparse.screened_spmm(Ad, diag_d, Bd, thr), reps=args.reps
288
+ )
289
+ mask_d = mask.to(DEV, torch.float32)
290
+ row["dense_masked_dev_ms"] = safe_bench(lambda: (Ad * mask_d) @ Bd, reps=args.reps)
291
+ row["dense_dev_ms"] = safe_bench(lambda: Ad @ Bd, reps=args.reps)
292
+ mf = mask.float()
293
+ row["cpu_masked_ms"] = safe_bench(lambda: (Ac * mf) @ Bc, warmup=1, reps=args.cpu_reps)
294
+ emit(row, args.out)
295
+
296
+
297
+ # ── Attention ────────────────────────────────────────────────────────────────
298
+
299
+
300
+ def block_mask(kind, nb):
301
+ m = torch.zeros(nb, nb, dtype=torch.bool)
302
+ w = 1 # local window radius in blocks (3 blocks per row)
303
+ if kind == "local":
304
+ for i in range(nb):
305
+ m[i, max(0, i - w) : i + w + 1] = True
306
+ elif kind == "dilated":
307
+ stride = 4
308
+ for i in range(nb):
309
+ m[i, (torch.arange(nb) - i) % stride == 0] = True
310
+ elif kind == "global":
311
+ for i in range(nb):
312
+ m[i, max(0, i - w) : i + w + 1] = True
313
+ m[0, :] = True
314
+ m[:, 0] = True
315
+ else:
316
+ raise ValueError(kind)
317
+ return m
318
+
319
+
320
+ def mask_bsr_from_block_mask(bm):
321
+ nb = bm.shape[0]
322
+ row_ptrs = torch.zeros(nb + 1, dtype=torch.long)
323
+ row_ptrs[1:] = bm.sum(1).cumsum(0)
324
+ cols = bm.nonzero()[:, 1].to(torch.long)
325
+ return trnsparse.BSRMatrix(
326
+ blocks=torch.ones(len(cols), 1, 1), # values unused by attention
327
+ block_col_indices=cols,
328
+ block_row_ptrs=row_ptrs,
329
+ shape=(nb * P, nb * P),
330
+ block_size=P,
331
+ )
332
+
333
+
334
+ def section_attn(args):
335
+ seq = args.size
336
+ nb = seq // P
337
+ F = torch.nn.functional
338
+ dt = _DTYPES[args.dtype]
339
+ for hd in [int(x) for x in args.head_dims.split(",")]:
340
+ g = torch.Generator().manual_seed(0)
341
+ Qc, Kc, Vc = (torch.randn(seq, hd, generator=g).to(dt).float() for _ in range(3))
342
+ dOc = torch.randn(seq, hd, generator=g).to(dt).float()
343
+ Qd, Kd, Vd, dOd = (t.to(DEV, dt) for t in (Qc, Kc, Vc, dOc))
344
+
345
+ def sdpa_fwd(q, k, v, m=None):
346
+ return F.scaled_dot_product_attention(q[None], k[None], v[None], attn_mask=m)[0]
347
+
348
+ dense_fwd = safe_bench(lambda: sdpa_fwd(Qd, Kd, Vd), reps=args.reps)
349
+
350
+ def dense_fwd_bwd(m=None):
351
+ q, k, v = (t.detach().requires_grad_() for t in (Qd, Kd, Vd))
352
+ o = sdpa_fwd(q, k, v, m)
353
+ o.backward(dOd)
354
+ return q.grad
355
+
356
+ dense_fb = safe_bench(dense_fwd_bwd, reps=args.reps)
357
+ cpu_fwd = None
358
+ if not args.fast:
359
+ cpu_fwd = safe_bench(lambda: sdpa_fwd(Qc, Kc, Vc), warmup=1, reps=args.cpu_reps)
360
+ for kind in args.masks.split(","):
361
+ bm = block_mask(kind, nb)
362
+ mb = mask_bsr_from_block_mask(bm)
363
+ elem_mask = bm.repeat_interleave(P, 0).repeat_interleave(P, 1)
364
+ counts = bm.sum(1).tolist()
365
+ row = {
366
+ "section": "attn",
367
+ "dtype": args.dtype,
368
+ "core_split": _core_split(),
369
+ "seq": seq,
370
+ "head_dim": hd,
371
+ "mask": kind,
372
+ "block_density": float(bm.float().mean()),
373
+ "K_max": max(counts),
374
+ "mean_blocks_per_row": sum(counts) / len(counts),
375
+ }
376
+ ref = sdpa_fwd(Qc.double(), Kc.double(), Vc.double(), elem_mask)
377
+ out = trnsparse.block_sparse_attention_tiled(Qd, Kd, Vd, mb)
378
+ row["fwd_rel_err"] = rel_err(out, ref)
379
+ row["trnsparse_fwd_ms"] = safe_bench(
380
+ lambda: trnsparse.block_sparse_attention_tiled(Qd, Kd, Vd, mb), reps=args.reps
381
+ )
382
+
383
+ def ts_fwd_bwd():
384
+ q, k, v = (t.detach().requires_grad_() for t in (Qd, Kd, Vd))
385
+ o = trnsparse.block_sparse_attention_tiled(q, k, v, mb)
386
+ o.backward(dOd)
387
+ return q.grad, k.grad, v.grad
388
+
389
+ if not args.no_bwd:
390
+ try:
391
+ gq, gk, gv = ts_fwd_bwd()
392
+ qr, kr, vr = (t.double().requires_grad_() for t in (Qc, Kc, Vc))
393
+ sdpa_fwd(qr, kr, vr, elem_mask).backward(dOc.double())
394
+ row["bwd_rel_err"] = max(
395
+ rel_err(gq, qr.grad), rel_err(gk, kr.grad), rel_err(gv, vr.grad)
396
+ )
397
+ row["trnsparse_fwd_bwd_ms"] = safe_bench(ts_fwd_bwd, reps=args.reps)
398
+ except Exception as e:
399
+ row["trnsparse_fwd_bwd_ms"] = f"failed: {type(e).__name__}: {str(e)[:120]}"
400
+ row["sdpa_dense_dev_fwd_ms"] = dense_fwd
401
+ row["sdpa_dense_dev_fwd_bwd_ms"] = dense_fb
402
+ em_d = elem_mask.to(DEV)
403
+ row["sdpa_masked_dev_fwd_ms"] = safe_bench(
404
+ lambda: sdpa_fwd(Qd, Kd, Vd, em_d), reps=args.reps
405
+ )
406
+ if not args.fast:
407
+ row["sdpa_cpu_fwd_ms"] = cpu_fwd
408
+ row["sdpa_masked_cpu_fwd_ms"] = safe_bench(
409
+ lambda: sdpa_fwd(Qc, Kc, Vc, elem_mask), warmup=1, reps=args.cpu_reps
410
+ )
411
+ emit(row, args.out)
412
+ em_d = None
413
+
414
+
415
+ def _core_split():
416
+ from trnsparse.nki import kernels
417
+
418
+ return kernels._core_split_enabled()
419
+
420
+
421
+ def main():
422
+ ap = argparse.ArgumentParser()
423
+ ap.add_argument("--section", required=True, choices=["bsr", "csr", "screen", "attn"])
424
+ ap.add_argument("--size", type=int, default=1024)
425
+ ap.add_argument("--densities", default="0.01,0.05,0.1,0.25,0.5")
426
+ ap.add_argument("--widths", default="128,512")
427
+ ap.add_argument("--thresholds", default="1e-12,1e-4,1e-2")
428
+ ap.add_argument("--head-dims", default="64,128")
429
+ ap.add_argument("--masks", default="local,dilated,global")
430
+ ap.add_argument("--no-bwd", action="store_true")
431
+ ap.add_argument("--dtype", default="fp32", choices=sorted(_DTYPES))
432
+ ap.add_argument("--fast", action="store_true")
433
+ ap.add_argument("--reps", type=int, default=10)
434
+ ap.add_argument("--cpu-reps", type=int, default=3)
435
+ ap.add_argument("--out", default="")
436
+ args = ap.parse_args()
437
+ trnsparse.set_backend("nki")
438
+ {"bsr": section_bsr, "csr": section_csr, "screen": section_screen, "attn": section_attn}[
439
+ args.section
440
+ ](args)
441
+
442
+
443
+ if __name__ == "__main__":
444
+ main()