trnsparse 0.6.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.6.0 → trnsparse-0.8.0}/CHANGELOG.md +155 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/CLAUDE.md +4 -2
- {trnsparse-0.6.0 → trnsparse-0.8.0}/PKG-INFO +31 -1
- {trnsparse-0.6.0 → trnsparse-0.8.0}/README.md +26 -0
- trnsparse-0.8.0/benchmarks/bench_native_trn2.py +397 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/docs/api.md +19 -0
- {trnsparse-0.6.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.6.0 → trnsparse-0.8.0}/docs/iterative_solvers.md +1 -1
- {trnsparse-0.6.0 → trnsparse-0.8.0}/docs/migration_scipy.md +8 -5
- {trnsparse-0.6.0 → trnsparse-0.8.0}/docs/quickstart.md +2 -2
- {trnsparse-0.6.0 → trnsparse-0.8.0}/docs/sparse_attention.md +7 -2
- {trnsparse-0.6.0 → trnsparse-0.8.0}/pyproject.toml +13 -1
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/conftest.py +8 -0
- trnsparse-0.8.0/tests/test_attn_gather.py +49 -0
- trnsparse-0.8.0/tests/test_native_dispatch.py +958 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_nki_bsr.py +5 -1
- trnsparse-0.8.0/tests/test_nki_compile.py +185 -0
- trnsparse-0.8.0/tests/test_nki_native.py +236 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_nki_screened_spmm.py +5 -1
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_nki_sim.py +79 -19
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_nki_spmm.py +5 -1
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse/__init__.py +1 -1
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse/formats.py +35 -0
- {trnsparse-0.6.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.6.0 → trnsparse-0.8.0}/trnsparse/nki/dispatch.py +592 -68
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse/nki/kernels.py +299 -21
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse/ops.py +104 -16
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse.egg-info/PKG-INFO +31 -1
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse.egg-info/SOURCES.txt +7 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse.egg-info/requires.txt +5 -0
- trnsparse-0.6.0/docs/benchmarks.md +0 -137
- trnsparse-0.6.0/docs/installation.md +0 -21
- {trnsparse-0.6.0 → trnsparse-0.8.0}/.github/workflows/ci.yml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/.github/workflows/notify-umbrella.yml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/.github/workflows/publish.yml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/.gitignore +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/.pre-commit-config.yaml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/CODE_OF_CONDUCT.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/CONTRIBUTING.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/LICENSE +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/benchmarks/bench_bsr_spmm.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/benchmarks/bench_iterative.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/benchmarks/bench_screening.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/benchmarks/bench_spmm.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/benchmarks/bench_spmv.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/benchmarks/conftest.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/docs/aws_setup.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/docs/index.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/examples/block_sparse_attention.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/examples/sparse_fock.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/infra/terraform/.terraform.lock.hcl +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/infra/terraform/README.md +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/infra/terraform/main.tf +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/mkdocs.yml +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/scripts/bench_to_md.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/scripts/run_benchmarks.sh +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/scripts/run_neuron_tests.sh +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/scripts/run_simulator_tests.sh +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/setup.cfg +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_attention.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_bsr.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_examples.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_formats.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_iterative.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_nki_attn.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_ops.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/tests/test_screening.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse/nki/__init__.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse/screening.py +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse.egg-info/dependency_links.txt +0 -0
- {trnsparse-0.6.0 → trnsparse-0.8.0}/trnsparse.egg-info/top_level.txt +0 -0
|
@@ -5,6 +5,161 @@ 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
|
+
## [Unreleased]
|
|
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
|
+
|
|
106
|
+
## [0.7.0] — 2026-10-28
|
|
107
|
+
|
|
108
|
+
Validated on Trn2 (trn2.3xlarge, Neuron SDK 2.33.0, native TorchNeuron device) — see trnsci/trnsci#35.
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
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
|
+
|
|
116
|
+
- **NKI 0.7.0 / neuronx-cc 2.28 kernel compatibility (#29).** Kernels that
|
|
117
|
+
passed `nki.simulate` failed to compile on Trn2 (Neuron SDK 2.33):
|
|
118
|
+
- `_attn_out_kernel`, `_attn_bwd_dq_kernel`, `_attn_bwd_dkdv_kernel`:
|
|
119
|
+
softmax normalisation `nl.divide(exp, row_denom)` by a `(128, 1)`
|
|
120
|
+
denominator lowered to `tensor_scalar_arith`, which no longer supports
|
|
121
|
+
`divide`. Now multiplies by `nl.reciprocal(row_denom)` (hoisted per
|
|
122
|
+
block-row in the forward and dQ kernels).
|
|
123
|
+
- `_screened_spmm_kernel`: the `(128, 1) * (1, 128)` Schwarz outer product
|
|
124
|
+
and the `(1, 1)` threshold subtract produced partition-count mismatches
|
|
125
|
+
(`operand0 partition total elements 128 != dst partition total elements
|
|
126
|
+
1`). `q_k` is now `nl.broadcast_to` `(128, 128)` and the threshold is
|
|
127
|
+
loaded once and broadcast to `(128, 1)`. The soft-mask tensor-tensor
|
|
128
|
+
divide (fails ISA validation `s3s3d3_tt_valid_op`) is now a reciprocal
|
|
129
|
+
multiply.
|
|
130
|
+
- `nki_bsr_attn_bwd`: the host-side `(..., b) → (..., b, 1)` unsqueeze
|
|
131
|
+
for per-row stats matched `q_scaled` / `k_blocks` / `v_blocks` by shape
|
|
132
|
+
whenever `head_dim == 128` (and in the simulator's padded path), feeding
|
|
133
|
+
the kernels a 4-D tensor. Stats are now selected by name.
|
|
134
|
+
- Dispatch: kernels always receive fp32 (Trainium has no fp64 datapath;
|
|
135
|
+
the native device downcast fp64 silently) and results are cast back to
|
|
136
|
+
the caller's dtype. Fixes `expected m1 and m2 to have the same dtype,
|
|
137
|
+
but got: float != double` in the PyTorch-level backward under fp64
|
|
138
|
+
gradcheck. The hardware gradcheck tests now use `eps=1e-2,
|
|
139
|
+
atol=rtol=1e-2`, sized for an fp32 forward (the ops are linear in each
|
|
140
|
+
input, so the larger eps adds no truncation error).
|
|
141
|
+
|
|
142
|
+
- **Block-sparse attention: two host-side gather bugs** (pre-existing, masked
|
|
143
|
+
on Trn2 by the compile failures above; found with neuronx-cc BIRSim):
|
|
144
|
+
- Rows with fewer than `K_max` blocks were padded with sentinel k-slots that
|
|
145
|
+
gathered column block 0, so block 0 was counted again in the softmax and
|
|
146
|
+
in `weights @ V` (local-window masks: ~49% of outputs wrong). Padded slots
|
|
147
|
+
now have K/V (forward, dQ) or Q/dO (dK/dV) zeroed, and their stats are
|
|
148
|
+
dropped in `_attn_host_reduction`.
|
|
149
|
+
- `_attn_bwd_gather` gathered `dO` by column block instead of the row block
|
|
150
|
+
`dO_m`, breaking dQ whenever a row attends to a block other than itself
|
|
151
|
+
(~99% of dQ wrong). The two simulator xfails attributed to a "PSUM
|
|
152
|
+
accumulate bug" (`test_bwd_dq_parity_kmax2`, `test_backward_head_dim_256`)
|
|
153
|
+
were this bug and now pass.
|
|
154
|
+
|
|
155
|
+
### Added
|
|
156
|
+
|
|
157
|
+
- `tests/test_attn_gather.py`: CPU regression tests for both gather bugs.
|
|
158
|
+
- `tests/test_nki_compile.py` (`nki_simulator` marker): CPU MLIR
|
|
159
|
+
verification (`source_to_mlir(...).verify()`) of every kernel for trn1
|
|
160
|
+
and trn2, attention kernels at head_dim 64, 128 and 256; plus a full NEFF
|
|
161
|
+
compile per kernel/target when `neuronx-cc` is on PATH.
|
|
162
|
+
|
|
8
163
|
## [0.6.0] — 2026-04-17
|
|
9
164
|
|
|
10
165
|
### Added
|
|
@@ -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()
|