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